@guard-rail/core 0.9.1 → 0.11.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 +253 -36
- package/dist/adapters/supabase/auth.js +1 -2
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/adapters/supabase/client.js +6 -1
- package/dist/adapters/supabase/client.js.map +1 -1
- package/dist/adapters/supabase/confirm.d.ts +11 -11
- package/dist/adapters/supabase/confirm.js +14 -18
- package/dist/adapters/supabase/confirm.js.map +1 -1
- package/dist/adapters/supabase/index.d.ts +3 -1
- package/dist/adapters/supabase/index.js +5 -0
- package/dist/adapters/supabase/index.js.map +1 -1
- package/dist/adapters/supabase/rateLimit.d.ts +24 -0
- package/dist/adapters/supabase/rateLimit.js +37 -0
- package/dist/adapters/supabase/rateLimit.js.map +1 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +1046 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init-db.js +23 -5
- package/dist/cli/init-db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +71 -1
- package/dist/core/config.d.ts +12 -1
- package/dist/core/config.js +29 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/csp.d.ts +32 -0
- package/dist/core/csp.js +26 -0
- package/dist/core/csp.js.map +1 -0
- package/dist/core/validate.d.ts +47 -0
- package/dist/core/validate.js +118 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/index.client.d.ts +2 -1
- package/dist/index.client.js +3 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.d.ts +5 -3
- package/dist/react/Admin.js +29 -41
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthDrawer.js +22 -34
- package/dist/react/AuthDrawer.js.map +1 -1
- package/dist/react/AuthRedirectSync.js +28 -22
- package/dist/react/AuthRedirectSync.js.map +1 -1
- package/dist/react/GRail/client.js +27 -47
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/GRail/render.js +3 -2
- package/dist/react/GRail/render.js.map +1 -1
- package/dist/react/MediaPicker.js +12 -16
- package/dist/react/MediaPicker.js.map +1 -1
- package/dist/react/Repeater/client.js +12 -13
- package/dist/react/Repeater/client.js.map +1 -1
- package/dist/react/Team.d.ts +1 -7
- package/dist/react/Team.js +21 -46
- package/dist/react/Team.js.map +1 -1
- package/dist/react/guardrail.css +563 -0
- package/dist/server/actions.js +52 -31
- package/dist/server/actions.js.map +1 -1
- package/dist/server/loader.js +4 -4
- package/dist/server/loader.js.map +1 -1
- package/dist/server/rateLimit.d.ts +21 -0
- package/dist/server/rateLimit.js +94 -0
- package/dist/server/rateLimit.js.map +1 -0
- package/dist/server/repeaterActions.js +9 -1
- package/dist/server/repeaterActions.js.map +1 -1
- package/dist/server/wrapper.js +18 -1
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +11 -9
- package/supabase/admin/check-grants.sql +41 -0
- package/supabase/migrations/20261028000000_require_confirmed_email.sql +53 -0
- package/supabase/migrations/20261028010000_rate_limits.sql +67 -0
- package/supabase/migrations/20261028020000_member_and_bucket_guards.sql +96 -0
- package/dist/react/styles.d.ts +0 -504
- package/dist/react/styles.js +0 -500
- package/dist/react/styles.js.map +0 -1
- package/scripts/postinstall.mjs +0 -48
package/dist/react/AuthDrawer.js
CHANGED
|
@@ -4,8 +4,6 @@ import React, { useActionState, useEffect, useState, useTransition } from "react
|
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { forgotPasswordAction, logoutAction, setPasswordAction } from "../server/actions";
|
|
6
6
|
import { CloseIcon } from "./Icons";
|
|
7
|
-
import { S, v } from "./styles";
|
|
8
|
-
import { ANIMATION_CSS } from "./Team";
|
|
9
7
|
const initialPasswordState = {};
|
|
10
8
|
function AuthDrawer({
|
|
11
9
|
mode,
|
|
@@ -14,13 +12,12 @@ function AuthDrawer({
|
|
|
14
12
|
onClose
|
|
15
13
|
}) {
|
|
16
14
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17
|
-
/* @__PURE__ */ jsx("style", { href: "guardrail-team-animation", precedence: "default", children: ANIMATION_CSS }),
|
|
18
15
|
/* @__PURE__ */ jsx(
|
|
19
16
|
"div",
|
|
20
17
|
{
|
|
21
18
|
"data-guardrail": true,
|
|
22
19
|
"data-guardrail-scrim": true,
|
|
23
|
-
|
|
20
|
+
className: "gr-scrim",
|
|
24
21
|
onClick: mode === "forgot" ? onClose : void 0
|
|
25
22
|
}
|
|
26
23
|
),
|
|
@@ -29,16 +26,16 @@ function AuthDrawer({
|
|
|
29
26
|
{
|
|
30
27
|
"data-guardrail": true,
|
|
31
28
|
"data-guardrail-team": true,
|
|
32
|
-
|
|
29
|
+
className: "gr-drawer",
|
|
33
30
|
role: "dialog",
|
|
34
31
|
"aria-label": mode === "set-password" ? "Set your password" : "Forgot password",
|
|
35
32
|
children: [
|
|
36
|
-
/* @__PURE__ */ jsxs("header", {
|
|
37
|
-
/* @__PURE__ */ jsx("h2", {
|
|
38
|
-
mode === "forgot" && onClose && /* @__PURE__ */ jsx("button", {
|
|
33
|
+
/* @__PURE__ */ jsxs("header", { className: "gr-drawer-head", children: [
|
|
34
|
+
/* @__PURE__ */ jsx("h2", { className: "gr-drawer-title", children: title ?? (mode === "set-password" ? "Set your password" : "Forgot your password?") }),
|
|
35
|
+
mode === "forgot" && onClose && /* @__PURE__ */ jsx("button", { className: "gr-icon-btn gr-text-muted", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
39
36
|
] }),
|
|
40
|
-
/* @__PURE__ */ jsxs("div", {
|
|
41
|
-
message && /* @__PURE__ */ jsx("p", {
|
|
37
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-drawer-body", children: [
|
|
38
|
+
message && /* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-lh-15 gr-m-0 gr-mb-16 gr-text-fg", children: message }),
|
|
42
39
|
mode === "set-password" ? /* @__PURE__ */ jsx(SetPasswordForm, {}) : /* @__PURE__ */ jsx(ForgotPasswordForm, {})
|
|
43
40
|
] })
|
|
44
41
|
]
|
|
@@ -54,14 +51,14 @@ function SetPasswordForm() {
|
|
|
54
51
|
if (state.success) router.refresh();
|
|
55
52
|
}, [state.success, router]);
|
|
56
53
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
57
|
-
/* @__PURE__ */ jsx("p", {
|
|
54
|
+
/* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-lh-15 gr-m-0 gr-mb-16 gr-text-muted", children: "Choose a password. It's how you'll sign in from now on." }),
|
|
58
55
|
/* @__PURE__ */ jsxs("form", { action: formAction, children: [
|
|
59
|
-
/* @__PURE__ */ jsx("label", {
|
|
56
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", htmlFor: "gr-new-password", children: "New password" }),
|
|
60
57
|
/* @__PURE__ */ jsx(
|
|
61
58
|
"input",
|
|
62
59
|
{
|
|
63
60
|
id: "gr-new-password",
|
|
64
|
-
|
|
61
|
+
className: "gr-field",
|
|
65
62
|
name: "password",
|
|
66
63
|
type: "password",
|
|
67
64
|
autoComplete: "new-password",
|
|
@@ -70,12 +67,12 @@ function SetPasswordForm() {
|
|
|
70
67
|
autoFocus: true
|
|
71
68
|
}
|
|
72
69
|
),
|
|
73
|
-
/* @__PURE__ */ jsx("label", {
|
|
70
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", htmlFor: "gr-confirm-password", children: "Confirm password" }),
|
|
74
71
|
/* @__PURE__ */ jsx(
|
|
75
72
|
"input",
|
|
76
73
|
{
|
|
77
74
|
id: "gr-confirm-password",
|
|
78
|
-
|
|
75
|
+
className: "gr-field",
|
|
79
76
|
name: "confirm",
|
|
80
77
|
type: "password",
|
|
81
78
|
autoComplete: "new-password",
|
|
@@ -83,25 +80,16 @@ function SetPasswordForm() {
|
|
|
83
80
|
required: true
|
|
84
81
|
}
|
|
85
82
|
),
|
|
86
|
-
/* @__PURE__ */ jsx("p", {
|
|
87
|
-
state.error && /* @__PURE__ */ jsx("div", {
|
|
88
|
-
/* @__PURE__ */ jsx("button", {
|
|
83
|
+
/* @__PURE__ */ jsx("p", { className: "gr-fs-11 gr-text-subtle gr-m-0 gr-mb-12", children: "At least 8 characters." }),
|
|
84
|
+
state.error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mb-12", children: state.error }),
|
|
85
|
+
/* @__PURE__ */ jsx("button", { className: "gr-primary-btn gr-w-full", type: "submit", disabled: isPending || signingOut, children: isPending ? "Saving\u2026" : "Set password" })
|
|
89
86
|
] }),
|
|
90
87
|
/* @__PURE__ */ jsx(
|
|
91
88
|
"button",
|
|
92
89
|
{
|
|
93
90
|
type: "button",
|
|
94
91
|
disabled: isPending || signingOut,
|
|
95
|
-
|
|
96
|
-
background: "none",
|
|
97
|
-
border: "none",
|
|
98
|
-
cursor: "pointer",
|
|
99
|
-
color: v("muted"),
|
|
100
|
-
fontSize: 12,
|
|
101
|
-
textDecoration: "underline",
|
|
102
|
-
padding: 0,
|
|
103
|
-
marginTop: 16
|
|
104
|
-
},
|
|
92
|
+
className: "gr-text-btn gr-p-0 gr-mt-16",
|
|
105
93
|
onClick: () => startSignOut(async () => {
|
|
106
94
|
await logoutAction();
|
|
107
95
|
router.refresh();
|
|
@@ -116,7 +104,7 @@ function ForgotPasswordForm() {
|
|
|
116
104
|
const [status, setStatus] = useState("idle");
|
|
117
105
|
const [error, setError] = useState(null);
|
|
118
106
|
if (status === "sent") {
|
|
119
|
-
return /* @__PURE__ */ jsxs("p", {
|
|
107
|
+
return /* @__PURE__ */ jsxs("p", { className: "gr-fs-13 gr-lh-15 gr-m-0 gr-text-fg", children: [
|
|
120
108
|
"If ",
|
|
121
109
|
/* @__PURE__ */ jsx("strong", { children: email.trim() }),
|
|
122
110
|
" has access to this site, a link to choose a new password is on its way. It works once, so open it on this device."
|
|
@@ -137,13 +125,13 @@ function ForgotPasswordForm() {
|
|
|
137
125
|
}
|
|
138
126
|
},
|
|
139
127
|
children: [
|
|
140
|
-
/* @__PURE__ */ jsx("p", {
|
|
141
|
-
/* @__PURE__ */ jsx("label", {
|
|
128
|
+
/* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-lh-15 gr-m-0 gr-mb-16 gr-text-muted", children: "Enter your email and we'll send a link to choose a new password." }),
|
|
129
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", htmlFor: "gr-forgot-email", children: "Email" }),
|
|
142
130
|
/* @__PURE__ */ jsx(
|
|
143
131
|
"input",
|
|
144
132
|
{
|
|
145
133
|
id: "gr-forgot-email",
|
|
146
|
-
|
|
134
|
+
className: "gr-field",
|
|
147
135
|
type: "email",
|
|
148
136
|
autoComplete: "email",
|
|
149
137
|
placeholder: "name@company.com",
|
|
@@ -153,8 +141,8 @@ function ForgotPasswordForm() {
|
|
|
153
141
|
autoFocus: true
|
|
154
142
|
}
|
|
155
143
|
),
|
|
156
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
157
|
-
/* @__PURE__ */ jsx("button", {
|
|
144
|
+
error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mb-12", children: error }),
|
|
145
|
+
/* @__PURE__ */ jsx("button", { className: "gr-primary-btn gr-w-full", type: "submit", disabled: status === "sending", children: status === "sending" ? "Sending\u2026" : "Send reset link" })
|
|
158
146
|
]
|
|
159
147
|
}
|
|
160
148
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/AuthDrawer.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { forgotPasswordAction, logoutAction, setPasswordAction } from \"../server/actions\";\nimport { CloseIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport { ANIMATION_CSS } from \"./Team\";\nimport type { SetPasswordState } from \"../core/types\";\n\nconst initialPasswordState: SetPasswordState = {};\n\n/**\n * The two password steps, in the same drawer the Team panel uses.\n *\n * Guardrail asks people for one credential, a password. Emailed links exist only\n * to let someone choose or replace it, so this is where they land:\n *\n * - \"set-password\" — after an invite or reset link. Cannot be dismissed: until a\n * password exists there is nothing to sign in with next time. The one way out\n * is to sign out.\n * - \"forgot\" — asks for an address and emails a reset link. Opened from the\n * sign-in bar, and from the \"that link didn't work\" message with `message`.\n */\nexport function AuthDrawer({\n mode,\n title,\n message,\n onClose,\n}: {\n mode: \"set-password\" | \"forgot\";\n title?: string;\n /** Plain-language context shown above the form, e.g. why a link failed. */\n message?: string;\n onClose?: () => void;\n}) {\n return (\n <>\n {/* Same sheet the Team drawer ships; React dedupes it by href. */}\n <style href=\"guardrail-team-animation\" precedence=\"default\">\n {ANIMATION_CSS}\n </style>\n <div\n data-guardrail\n data-guardrail-scrim\n style={S.scrim}\n // A set-password step is not something to click away from.\n onClick={mode === \"forgot\" ? onClose : undefined}\n />\n <aside\n data-guardrail\n data-guardrail-team\n style={S.drawer}\n role=\"dialog\"\n aria-label={mode === \"set-password\" ? \"Set your password\" : \"Forgot password\"}\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>\n {title ?? (mode === \"set-password\" ? \"Set your password\" : \"Forgot your password?\")}\n </h2>\n {mode === \"forgot\" && onClose && (\n <button style={{ ...S.iconBtn, color: v(\"muted\") }} onClick={onClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n )}\n </header>\n <div style={S.drawerBody}>\n {message && (\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: \"0 0 16px\", color: v(\"fg\") }}>{message}</p>\n )}\n {mode === \"set-password\" ? <SetPasswordForm /> : <ForgotPasswordForm />}\n </div>\n </aside>\n </>\n );\n}\n\nfunction SetPasswordForm() {\n const router = useRouter();\n const [state, formAction, isPending] = useActionState(setPasswordAction, initialPasswordState);\n const [signingOut, startSignOut] = useTransition();\n\n useEffect(() => {\n if (state.success) router.refresh();\n }, [state.success, router]);\n\n return (\n <>\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: \"0 0 16px\", color: v(\"muted\") }}>\n Choose a password. It's how you'll sign in from now on.\n </p>\n <form action={formAction}>\n <label style={S.label} htmlFor=\"gr-new-password\">\n New password\n </label>\n <input\n id=\"gr-new-password\"\n style={S.field}\n name=\"password\"\n type=\"password\"\n autoComplete=\"new-password\"\n minLength={8}\n required\n autoFocus\n />\n <label style={S.label} htmlFor=\"gr-confirm-password\">\n Confirm password\n </label>\n <input\n id=\"gr-confirm-password\"\n style={S.field}\n name=\"confirm\"\n type=\"password\"\n autoComplete=\"new-password\"\n minLength={8}\n required\n />\n <p style={{ fontSize: 11, color: v(\"subtle\"), margin: \"0 0 12px\" }}>At least 8 characters.</p>\n {state.error && <div style={{ ...S.error, marginBottom: 12 }}>{state.error}</div>}\n <button style={{ ...S.primaryBtn, width: \"100%\" }} type=\"submit\" disabled={isPending || signingOut}>\n {isPending ? \"Saving…\" : \"Set password\"}\n </button>\n </form>\n <button\n type=\"button\"\n disabled={isPending || signingOut}\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n color: v(\"muted\"),\n fontSize: 12,\n textDecoration: \"underline\",\n padding: 0,\n marginTop: 16,\n }}\n onClick={() =>\n startSignOut(async () => {\n await logoutAction();\n router.refresh();\n })\n }\n >\n Cancel and sign out\n </button>\n </>\n );\n}\n\nfunction ForgotPasswordForm() {\n const [email, setEmail] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"sending\" | \"sent\">(\"idle\");\n const [error, setError] = useState<string | null>(null);\n\n if (status === \"sent\") {\n return (\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: 0, color: v(\"fg\") }}>\n If <strong>{email.trim()}</strong> has access to this site, a link to choose a new password is on its\n way. It works once, so open it on this device.\n </p>\n );\n }\n\n return (\n <form\n onSubmit={async (e) => {\n e.preventDefault();\n setStatus(\"sending\");\n setError(null);\n const res = await forgotPasswordAction(email);\n if (res.success) setStatus(\"sent\");\n else {\n setError(res.error ?? \"Something went wrong.\");\n setStatus(\"idle\");\n }\n }}\n >\n <p style={{ fontSize: 13, lineHeight: 1.5, margin: \"0 0 16px\", color: v(\"muted\") }}>\n Enter your email and we'll send a link to choose a new password.\n </p>\n <label style={S.label} htmlFor=\"gr-forgot-email\">\n Email\n </label>\n <input\n id=\"gr-forgot-email\"\n style={S.field}\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"name@company.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n autoFocus\n />\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n <button style={{ ...S.primaryBtn, width: \"100%\" }} type=\"submit\" disabled={status === \"sending\"}>\n {status === \"sending\" ? \"Sending…\" : \"Send reset link\"}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/AuthDrawer.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useActionState, useEffect, useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { forgotPasswordAction, logoutAction, setPasswordAction } from \"../server/actions\";\nimport { CloseIcon } from \"./Icons\";\nimport type { SetPasswordState } from \"../core/types\";\n\nconst initialPasswordState: SetPasswordState = {};\n\n/**\n * The two password steps, in the same drawer the Team panel uses.\n *\n * Guardrail asks people for one credential, a password. Emailed links exist only\n * to let someone choose or replace it, so this is where they land:\n *\n * - \"set-password\" — after an invite or reset link. Cannot be dismissed: until a\n * password exists there is nothing to sign in with next time. The one way out\n * is to sign out.\n * - \"forgot\" — asks for an address and emails a reset link. Opened from the\n * sign-in bar, and from the \"that link didn't work\" message with `message`.\n */\nexport function AuthDrawer({\n mode,\n title,\n message,\n onClose,\n}: {\n mode: \"set-password\" | \"forgot\";\n title?: string;\n /** Plain-language context shown above the form, e.g. why a link failed. */\n message?: string;\n onClose?: () => void;\n}) {\n return (\n <>\n <div\n data-guardrail\n data-guardrail-scrim\n className=\"gr-scrim\"\n // A set-password step is not something to click away from.\n onClick={mode === \"forgot\" ? onClose : undefined}\n />\n <aside\n data-guardrail\n data-guardrail-team\n className=\"gr-drawer\"\n role=\"dialog\"\n aria-label={mode === \"set-password\" ? \"Set your password\" : \"Forgot password\"}\n >\n <header className=\"gr-drawer-head\">\n <h2 className=\"gr-drawer-title\">\n {title ?? (mode === \"set-password\" ? \"Set your password\" : \"Forgot your password?\")}\n </h2>\n {mode === \"forgot\" && onClose && (\n <button className=\"gr-icon-btn gr-text-muted\" onClick={onClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n )}\n </header>\n <div className=\"gr-drawer-body\">\n {message && (\n <p className=\"gr-fs-13 gr-lh-15 gr-m-0 gr-mb-16 gr-text-fg\">{message}</p>\n )}\n {mode === \"set-password\" ? <SetPasswordForm /> : <ForgotPasswordForm />}\n </div>\n </aside>\n </>\n );\n}\n\nfunction SetPasswordForm() {\n const router = useRouter();\n const [state, formAction, isPending] = useActionState(setPasswordAction, initialPasswordState);\n const [signingOut, startSignOut] = useTransition();\n\n useEffect(() => {\n if (state.success) router.refresh();\n }, [state.success, router]);\n\n return (\n <>\n <p className=\"gr-fs-13 gr-lh-15 gr-m-0 gr-mb-16 gr-text-muted\">\n Choose a password. It's how you'll sign in from now on.\n </p>\n <form action={formAction}>\n <label className=\"gr-label\" htmlFor=\"gr-new-password\">\n New password\n </label>\n <input\n id=\"gr-new-password\"\n className=\"gr-field\"\n name=\"password\"\n type=\"password\"\n autoComplete=\"new-password\"\n minLength={8}\n required\n autoFocus\n />\n <label className=\"gr-label\" htmlFor=\"gr-confirm-password\">\n Confirm password\n </label>\n <input\n id=\"gr-confirm-password\"\n className=\"gr-field\"\n name=\"confirm\"\n type=\"password\"\n autoComplete=\"new-password\"\n minLength={8}\n required\n />\n <p className=\"gr-fs-11 gr-text-subtle gr-m-0 gr-mb-12\">At least 8 characters.</p>\n {state.error && <div className=\"gr-error gr-mb-12\">{state.error}</div>}\n <button className=\"gr-primary-btn gr-w-full\" type=\"submit\" disabled={isPending || signingOut}>\n {isPending ? \"Saving…\" : \"Set password\"}\n </button>\n </form>\n <button\n type=\"button\"\n disabled={isPending || signingOut}\n className=\"gr-text-btn gr-p-0 gr-mt-16\"\n onClick={() =>\n startSignOut(async () => {\n await logoutAction();\n router.refresh();\n })\n }\n >\n Cancel and sign out\n </button>\n </>\n );\n}\n\nfunction ForgotPasswordForm() {\n const [email, setEmail] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"sending\" | \"sent\">(\"idle\");\n const [error, setError] = useState<string | null>(null);\n\n if (status === \"sent\") {\n return (\n <p className=\"gr-fs-13 gr-lh-15 gr-m-0 gr-text-fg\">\n If <strong>{email.trim()}</strong> has access to this site, a link to choose a new password is on its\n way. It works once, so open it on this device.\n </p>\n );\n }\n\n return (\n <form\n onSubmit={async (e) => {\n e.preventDefault();\n setStatus(\"sending\");\n setError(null);\n const res = await forgotPasswordAction(email);\n if (res.success) setStatus(\"sent\");\n else {\n setError(res.error ?? \"Something went wrong.\");\n setStatus(\"idle\");\n }\n }}\n >\n <p className=\"gr-fs-13 gr-lh-15 gr-m-0 gr-mb-16 gr-text-muted\">\n Enter your email and we'll send a link to choose a new password.\n </p>\n <label className=\"gr-label\" htmlFor=\"gr-forgot-email\">\n Email\n </label>\n <input\n id=\"gr-forgot-email\"\n className=\"gr-field\"\n type=\"email\"\n autoComplete=\"email\"\n placeholder=\"name@company.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n autoFocus\n />\n {error && <div className=\"gr-error gr-mb-12\">{error}</div>}\n <button className=\"gr-primary-btn gr-w-full\" type=\"submit\" disabled={status === \"sending\"}>\n {status === \"sending\" ? \"Sending…\" : \"Send reset link\"}\n </button>\n </form>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,29 +6,35 @@ function AuthRedirectSync() {
|
|
|
6
6
|
const searchParams = useSearchParams();
|
|
7
7
|
const handled = useRef(false);
|
|
8
8
|
useEffect(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
const check = () => {
|
|
10
|
+
if (handled.current) return;
|
|
11
|
+
const rawHash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash;
|
|
12
|
+
const hasHashTokens = /(?:^|&)access_token=/.test(rawHash);
|
|
13
|
+
const hashParams = new URLSearchParams(rawHash);
|
|
14
|
+
const code = searchParams.get("code") ?? void 0;
|
|
15
|
+
const tokenHash = hashParams.get("token_hash") ?? searchParams.get("token_hash") ?? void 0;
|
|
16
|
+
const type = hashParams.get("type") ?? searchParams.get("type") ?? void 0;
|
|
17
|
+
if (!hasHashTokens && !code && !tokenHash) return;
|
|
18
|
+
handled.current = true;
|
|
19
|
+
const params = {
|
|
20
|
+
...hasHashTokens ? { hash: rawHash } : {},
|
|
21
|
+
...code ? { code } : {},
|
|
22
|
+
...tokenHash ? { tokenHash } : {},
|
|
23
|
+
...type ? { type } : {}
|
|
24
|
+
};
|
|
25
|
+
completeAuthRedirectAction(params).then((res) => {
|
|
26
|
+
const url = new URL(window.location.href);
|
|
27
|
+
url.hash = "";
|
|
28
|
+
url.searchParams.delete("code");
|
|
29
|
+
url.searchParams.delete("token_hash");
|
|
30
|
+
url.searchParams.delete("type");
|
|
31
|
+
window.history.replaceState(null, "", url.toString());
|
|
32
|
+
window.location.reload();
|
|
33
|
+
});
|
|
22
34
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
url.searchParams.delete("code");
|
|
27
|
-
url.searchParams.delete("token_hash");
|
|
28
|
-
url.searchParams.delete("type");
|
|
29
|
-
window.history.replaceState(null, "", url.toString());
|
|
30
|
-
window.location.reload();
|
|
31
|
-
});
|
|
35
|
+
check();
|
|
36
|
+
window.addEventListener("hashchange", check);
|
|
37
|
+
return () => window.removeEventListener("hashchange", check);
|
|
32
38
|
}, [searchParams]);
|
|
33
39
|
return null;
|
|
34
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/AuthRedirectSync.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { completeAuthRedirectAction } from \"../server/actions\";\n\n/**\n * Notices a session that arrived via a redirect link — an invite, magic\n * link, or password-recovery email — and hands whatever it found to\n * completeAuthRedirectAction, which defers to the auth adapter's own\n * completeAuthRedirect for the actual exchange. Deliberately generic: this\n * file has no idea what a Supabase token looks like, only that a hash\n * carrying `access_token=` or a `code`/`token_hash` query param might be one.\n *\n * Has to run client-side and unconditionally (not gated behind edit mode,\n * unlike EditSyncer) — an invite link redirects to the site's plain URL, not\n * necessarily one with ?edit=true, and hash-based tokens never reach the\n * server on their own; only the browser ever sees window.location.hash.\n *\n * Mirrors the community-documented pattern for this exact problem, just\n * generalized across whichever redirect style a provider's own email\n * templates happen to produce, rather than requiring one specific route.\n */\nexport function AuthRedirectSync() {\n const searchParams = useSearchParams();\n const handled = useRef(false);\n\n useEffect(() => {\n if (handled.current) return;\n\n
|
|
1
|
+
{"version":3,"sources":["../../src/react/AuthRedirectSync.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { completeAuthRedirectAction } from \"../server/actions\";\n\n/**\n * Notices a session that arrived via a redirect link — an invite, magic\n * link, or password-recovery email — and hands whatever it found to\n * completeAuthRedirectAction, which defers to the auth adapter's own\n * completeAuthRedirect for the actual exchange. Deliberately generic: this\n * file has no idea what a Supabase token looks like, only that a hash\n * carrying `access_token=` or a `code`/`token_hash` query param might be one.\n *\n * Has to run client-side and unconditionally (not gated behind edit mode,\n * unlike EditSyncer) — an invite link redirects to the site's plain URL, not\n * necessarily one with ?edit=true, and hash-based tokens never reach the\n * server on their own; only the browser ever sees window.location.hash.\n *\n * Mirrors the community-documented pattern for this exact problem, just\n * generalized across whichever redirect style a provider's own email\n * templates happen to produce, rather than requiring one specific route.\n */\nexport function AuthRedirectSync() {\n const searchParams = useSearchParams();\n const handled = useRef(false);\n\n useEffect(() => {\n const check = () => {\n if (handled.current) return;\n\n const rawHash = window.location.hash.startsWith(\"#\")\n ? window.location.hash.slice(1)\n : window.location.hash;\n // The one thing worth checking before assuming this hash is ours: every\n // normal page load has an empty hash, and this runs on all of them.\n const hasHashTokens = /(?:^|&)access_token=/.test(rawHash);\n\n // Links Guardrail mints carry token_hash/type in the fragment; the query\n // string is still read for links emailed before that change.\n const hashParams = new URLSearchParams(rawHash);\n const code = searchParams.get(\"code\") ?? undefined;\n const tokenHash =\n hashParams.get(\"token_hash\") ?? searchParams.get(\"token_hash\") ?? undefined;\n const type = hashParams.get(\"type\") ?? searchParams.get(\"type\") ?? undefined;\n\n if (!hasHashTokens && !code && !tokenHash) return;\n handled.current = true;\n\n const params = {\n ...(hasHashTokens ? { hash: rawHash } : {}),\n ...(code ? { code } : {}),\n ...(tokenHash ? { tokenHash } : {}),\n ...(type ? { type } : {}),\n };\n\n completeAuthRedirectAction(params).then((res) => {\n // Stripped either way — a token that failed to exchange (already used,\n // expired) has no business lingering in the address bar or history\n // either.\n const url = new URL(window.location.href);\n url.hash = \"\";\n url.searchParams.delete(\"code\");\n url.searchParams.delete(\"token_hash\");\n url.searchParams.delete(\"type\");\n window.history.replaceState(null, \"\", url.toString());\n\n // A full reload, not router.refresh(): the session was just set via\n // cookies a Server Action wrote, and the current page's React tree\n // (including this component) was built from the pre-session render —\n // simplest to just start over. On failure the action set a cookie\n // explaining it, and a reload is what shows that, rather than leaving the\n // person on a page where nothing visibly happened.\n window.location.reload();\n });\n };\n\n check();\n // A link pasted into a tab that already has the site open changes only the\n // fragment, which is not a page load.\n window.addEventListener(\"hashchange\", check);\n return () => window.removeEventListener(\"hashchange\", check);\n }, [searchParams]);\n\n return null;\n}\n"],"mappings":";AAEA,SAAS,WAAW,cAAc;AAClC,SAAS,uBAAuB;AAChC,SAAS,kCAAkC;AAmBpC,SAAS,mBAAmB;AACjC,QAAM,eAAe,gBAAgB;AACrC,QAAM,UAAU,OAAO,KAAK;AAE5B,YAAU,MAAM;AACd,UAAM,QAAQ,MAAM;AAClB,UAAI,QAAQ,QAAS;AAErB,YAAM,UAAU,OAAO,SAAS,KAAK,WAAW,GAAG,IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,IAC5B,OAAO,SAAS;AAGpB,YAAM,gBAAgB,uBAAuB,KAAK,OAAO;AAIzD,YAAM,aAAa,IAAI,gBAAgB,OAAO;AAC9C,YAAM,OAAO,aAAa,IAAI,MAAM,KAAK;AACzC,YAAM,YACJ,WAAW,IAAI,YAAY,KAAK,aAAa,IAAI,YAAY,KAAK;AACpE,YAAM,OAAO,WAAW,IAAI,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK;AAEnE,UAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,UAAW;AAC3C,cAAQ,UAAU;AAElB,YAAM,SAAS;AAAA,QACb,GAAI,gBAAgB,EAAE,MAAM,QAAQ,IAAI,CAAC;AAAA,QACzC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,QACvB,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AAAA,QACjC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,MACzB;AAEA,iCAA2B,MAAM,EAAE,KAAK,CAAC,QAAQ;AAI/C,cAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,YAAI,OAAO;AACX,YAAI,aAAa,OAAO,MAAM;AAC9B,YAAI,aAAa,OAAO,YAAY;AACpC,YAAI,aAAa,OAAO,MAAM;AAC9B,eAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,SAAS,CAAC;AAQpD,eAAO,SAAS,OAAO;AAAA,MACzB,CAAC;AAAA,IACH;AAEA,UAAM;AAGN,WAAO,iBAAiB,cAAc,KAAK;AAC3C,WAAO,MAAM,OAAO,oBAAoB,cAAc,KAAK;AAAA,EAC7D,GAAG,CAAC,YAAY,CAAC;AAEjB,SAAO;AACT;","names":[]}
|
|
@@ -9,7 +9,6 @@ import { useGRail } from "../Provider";
|
|
|
9
9
|
import { useAnchorRect } from "../useAnchorRect";
|
|
10
10
|
import { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from "../Icons";
|
|
11
11
|
import { openHref } from "../openHref";
|
|
12
|
-
import { S } from "../styles";
|
|
13
12
|
import { MediaPicker } from "../MediaPicker";
|
|
14
13
|
function GRailClient({
|
|
15
14
|
as,
|
|
@@ -84,17 +83,8 @@ function GRailClient({
|
|
|
84
83
|
...htmlProps,
|
|
85
84
|
ref: elementRef,
|
|
86
85
|
override: renderedValue,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Images are left alone — no outline, position, or z-index — and
|
|
90
|
-
// get a separate ImageMarker instead. Restyling the <img> itself
|
|
91
|
-
// can't work for a background image with content layered over it.
|
|
92
|
-
...isImage ? { cursor: "pointer" } : {
|
|
93
|
-
outline: editing ? "2px solid #2563eb" : "1px dashed #60a5fa",
|
|
94
|
-
outlineOffset: 2,
|
|
95
|
-
cursor: editing ? "text" : "pointer"
|
|
96
|
-
}
|
|
97
|
-
},
|
|
86
|
+
"data-gr-field": isImage ? "image" : "text",
|
|
87
|
+
...editing && !isImage ? { "data-gr-editing": "" } : {},
|
|
98
88
|
onClick: (e) => {
|
|
99
89
|
if (editing) return;
|
|
100
90
|
e.preventDefault();
|
|
@@ -168,13 +158,13 @@ function ImageMarker({
|
|
|
168
158
|
"div",
|
|
169
159
|
{
|
|
170
160
|
"data-guardrail": true,
|
|
161
|
+
className: "gr-marker-outline",
|
|
162
|
+
...editing ? { "data-gr-editing": "" } : {},
|
|
171
163
|
style: {
|
|
172
|
-
...S.markerOutline,
|
|
173
164
|
top: rect.top - 2,
|
|
174
165
|
left: rect.left - 2,
|
|
175
166
|
width: rect.width + 4,
|
|
176
|
-
height: height + 4
|
|
177
|
-
border: editing ? "2px solid #2563eb" : "1px dashed #60a5fa"
|
|
167
|
+
height: height + 4
|
|
178
168
|
}
|
|
179
169
|
}
|
|
180
170
|
),
|
|
@@ -183,7 +173,8 @@ function ImageMarker({
|
|
|
183
173
|
{
|
|
184
174
|
type: "button",
|
|
185
175
|
"data-guardrail": true,
|
|
186
|
-
|
|
176
|
+
className: "gr-marker-handle",
|
|
177
|
+
style: { top: handleTop, left: handleLeft },
|
|
187
178
|
"aria-label": "Edit image",
|
|
188
179
|
title: "Edit image",
|
|
189
180
|
onClick: (e) => {
|
|
@@ -226,7 +217,7 @@ function Chrome({
|
|
|
226
217
|
/* @__PURE__ */ jsx(
|
|
227
218
|
"button",
|
|
228
219
|
{
|
|
229
|
-
|
|
220
|
+
className: "gr-icon-btn-primary",
|
|
230
221
|
onClick: onSave,
|
|
231
222
|
disabled: saving,
|
|
232
223
|
title: "Save (Enter)",
|
|
@@ -237,7 +228,7 @@ function Chrome({
|
|
|
237
228
|
/* @__PURE__ */ jsx(
|
|
238
229
|
"button",
|
|
239
230
|
{
|
|
240
|
-
|
|
231
|
+
className: "gr-icon-btn",
|
|
241
232
|
onClick: onCancel,
|
|
242
233
|
disabled: saving,
|
|
243
234
|
title: "Cancel (Esc)",
|
|
@@ -247,46 +238,47 @@ function Chrome({
|
|
|
247
238
|
)
|
|
248
239
|
] });
|
|
249
240
|
if (isInline) {
|
|
250
|
-
return /* @__PURE__ */ jsxs("div", { "data-guardrail": true,
|
|
241
|
+
return /* @__PURE__ */ jsxs("div", { "data-guardrail": true, className: "gr-anchored-bar", style: position, children: [
|
|
251
242
|
actions,
|
|
252
|
-
error && /* @__PURE__ */ jsx("span", {
|
|
243
|
+
error && /* @__PURE__ */ jsx("span", { className: "gr-error gr-mx-6", children: error })
|
|
253
244
|
] });
|
|
254
245
|
}
|
|
255
246
|
return /* @__PURE__ */ jsxs(
|
|
256
247
|
"div",
|
|
257
248
|
{
|
|
258
249
|
"data-guardrail": true,
|
|
259
|
-
|
|
250
|
+
className: "gr-anchored-panel",
|
|
251
|
+
style: position,
|
|
260
252
|
onClick: (e) => e.stopPropagation(),
|
|
261
253
|
children: [
|
|
262
254
|
row.value.type === "image" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
263
255
|
row.value.src && // Plain img: previews must not depend on remotePatterns.
|
|
264
|
-
/* @__PURE__ */ jsx("img", { src: row.value.src, alt: "",
|
|
256
|
+
/* @__PURE__ */ jsx("img", { src: row.value.src, alt: "", className: "gr-preview-thumb" }),
|
|
265
257
|
storage && /* @__PURE__ */ jsx(
|
|
266
258
|
"button",
|
|
267
259
|
{
|
|
268
260
|
type: "button",
|
|
269
|
-
|
|
261
|
+
className: "gr-secondary-btn",
|
|
270
262
|
onClick: () => setPickerOpen(true),
|
|
271
263
|
children: row.value.src ? "Replace image" : "Choose image"
|
|
272
264
|
}
|
|
273
265
|
),
|
|
274
|
-
/* @__PURE__ */ jsx("label", {
|
|
266
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", children: "Alt text" }),
|
|
275
267
|
/* @__PURE__ */ jsx(
|
|
276
268
|
"input",
|
|
277
269
|
{
|
|
278
|
-
|
|
270
|
+
className: "gr-field",
|
|
279
271
|
value: row.value.alt || "",
|
|
280
272
|
placeholder: "Describe the image for screen readers",
|
|
281
273
|
onChange: (e) => onChange("alt", e.target.value)
|
|
282
274
|
}
|
|
283
275
|
),
|
|
284
276
|
!storage && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
285
|
-
/* @__PURE__ */ jsx("label", {
|
|
277
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", children: "Image URL" }),
|
|
286
278
|
/* @__PURE__ */ jsx(
|
|
287
279
|
"input",
|
|
288
280
|
{
|
|
289
|
-
|
|
281
|
+
className: "gr-field",
|
|
290
282
|
value: row.value.src || "",
|
|
291
283
|
onChange: (e) => onPatch({ src: e.target.value, width: void 0, height: void 0 })
|
|
292
284
|
}
|
|
@@ -302,48 +294,36 @@ function Chrome({
|
|
|
302
294
|
)
|
|
303
295
|
] }),
|
|
304
296
|
row.value.type === "link" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
305
|
-
/* @__PURE__ */ jsx("label", {
|
|
297
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", children: "Link text" }),
|
|
306
298
|
/* @__PURE__ */ jsx(
|
|
307
299
|
"input",
|
|
308
300
|
{
|
|
309
|
-
|
|
301
|
+
className: "gr-field",
|
|
310
302
|
value: row.value.text || "",
|
|
311
303
|
onChange: (e) => onChange("text", e.target.value)
|
|
312
304
|
}
|
|
313
305
|
),
|
|
314
|
-
/* @__PURE__ */ jsx("label", {
|
|
306
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", children: "URL" }),
|
|
315
307
|
/* @__PURE__ */ jsx(
|
|
316
308
|
"input",
|
|
317
309
|
{
|
|
318
|
-
|
|
310
|
+
className: "gr-field",
|
|
319
311
|
value: row.value.href || "",
|
|
320
312
|
onChange: (e) => onChange("href", e.target.value)
|
|
321
313
|
}
|
|
322
314
|
)
|
|
323
315
|
] }),
|
|
324
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
316
|
+
error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mb-8", children: error }),
|
|
325
317
|
/* @__PURE__ */ jsxs(
|
|
326
318
|
"div",
|
|
327
319
|
{
|
|
328
|
-
|
|
329
|
-
display: "flex",
|
|
330
|
-
gap: 4,
|
|
331
|
-
alignItems: "center",
|
|
332
|
-
justifyContent: row.value.type === "link" ? "space-between" : "flex-end"
|
|
333
|
-
},
|
|
320
|
+
className: row.value.type === "link" ? "gr-flex gr-gap-4 gr-items-center gr-justify-between" : "gr-flex gr-gap-4 gr-items-center gr-justify-end",
|
|
334
321
|
children: [
|
|
335
322
|
row.value.type === "link" && /* @__PURE__ */ jsxs(
|
|
336
323
|
"button",
|
|
337
324
|
{
|
|
338
325
|
type: "button",
|
|
339
|
-
|
|
340
|
-
...S.secondaryBtn,
|
|
341
|
-
width: "auto",
|
|
342
|
-
marginBottom: 0,
|
|
343
|
-
display: "inline-flex",
|
|
344
|
-
alignItems: "center",
|
|
345
|
-
gap: 6
|
|
346
|
-
},
|
|
326
|
+
className: "gr-secondary-btn gr-w-auto gr-mb-0 gr-inline-flex gr-items-center gr-gap-6",
|
|
347
327
|
disabled: !row.value.href?.trim(),
|
|
348
328
|
onClick: () => openHref(row.value.href ?? "", (path) => router.push(path)),
|
|
349
329
|
title: "Open this link",
|
|
@@ -354,7 +334,7 @@ function Chrome({
|
|
|
354
334
|
]
|
|
355
335
|
}
|
|
356
336
|
),
|
|
357
|
-
/* @__PURE__ */ jsx("div", {
|
|
337
|
+
/* @__PURE__ */ jsx("div", { className: "gr-flex gr-gap-4", children: actions })
|
|
358
338
|
]
|
|
359
339
|
}
|
|
360
340
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useRouter } from \"next/navigation\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from \"../Icons\";\nimport { openHref } from \"../openHref\";\nimport { S } from \"../styles\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { HTMLKeys, OverrideRow, OverrideValue } from \"../../core/types\";\n\nexport function GRailClient({\n as,\n initialOverride,\n ...htmlProps\n}: {\n as?: HTMLKeys;\n initialOverride: OverrideRow;\n [key: string]: unknown;\n}) {\n const { isEditMode } = useGRail();\n const [editing, setEditing] = useState(false);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [row, setRow] = useState<OverrideRow>(initialOverride);\n\n const elementRef = useRef<HTMLElement | null>(null);\n const isImage = row.value.type === \"image\";\n // Image fields keep a rect for their whole time in edit mode (their marker\n // needs it); everything else only while actively editing.\n const rect = useAnchorRect(elementRef, editing || (isEditMode && isImage), {\n // Only the idle marker case (isImage, not actively editing) is gated —\n // an image mid-edit tracks unconditionally, same as text/link chrome.\n gateByVisibility: isImage && !editing,\n });\n\n /**\n * The text the element was rendered with when this edit session began.\n *\n * While editing, the DOM owns the text and React must not touch it. Feeding\n * state back into a contentEditable's children makes React replace the text\n * node on every keystroke, which resets the caret to offset 0 — typing then\n * appears reversed and backspace jumps to the start. Holding this value\n * constant means the children prop never changes, so React leaves the node\n * alone and the caret stays put.\n */\n const sessionTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n /** Live DOM text, updated without re-rendering. Read on save. */\n const draftTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n\n const isInline = row.value.type === \"text\" || row.value.type === \"button\";\n\n const beginEdit = () => {\n sessionTextRef.current = row.value.text ?? \"\";\n draftTextRef.current = row.value.text ?? \"\";\n setError(null);\n setEditing(true);\n };\n\n const updateAttribute = (key: string, value: string) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));\n\n const patchValue = (patch: Partial<OverrideValue>) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));\n\n const handleSave = async () => {\n // Inline text lives in the DOM, not in state, so read it back here.\n const next: OverrideRow = isInline\n ? { ...row, value: { ...row.value, text: draftTextRef.current } }\n : row;\n\n setSaving(true);\n setError(null);\n const res = await saveOverride(next);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Save failed.\");\n return;\n }\n setRow(next);\n setEditing(false);\n };\n\n const handleCancel = () => {\n setRow(initialOverride);\n draftTextRef.current = initialOverride.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = initialOverride.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n };\n\n // Clicking away from an active edit cancels it, the same as Escape. Anything\n // inside the field itself (repositioning the caret) or inside the anchored\n // toolbar/panel (both carry data-guardrail) is not \"away\" — checked via\n // closest() rather than React's onClick so a stopPropagation() inside the\n // chrome (image/link panel inputs) reliably suppresses this too, since that\n // call stops the native event before it ever reaches this document listener.\n useEffect(() => {\n if (!editing) return;\n const handlePointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (elementRef.current?.contains(target)) return;\n if (target instanceof Element && target.closest(\"[data-guardrail]\")) return;\n handleCancel();\n };\n document.addEventListener(\"mousedown\", handlePointerDown);\n return () => document.removeEventListener(\"mousedown\", handlePointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editing]);\n\n if (!isEditMode) {\n return <GRailRender as={as} {...htmlProps} override={row.value} />;\n }\n\n // Frozen during the session so React never rewrites the live text node.\n const renderedValue =\n editing && isInline\n ? { ...row.value, text: sessionTextRef.current }\n : row.value;\n\n return (\n <>\n <GRailRender\n as={as}\n {...htmlProps}\n ref={elementRef}\n override={renderedValue}\n style={{\n ...(htmlProps.style as React.CSSProperties),\n // Images are left alone — no outline, position, or z-index — and\n // get a separate ImageMarker instead. Restyling the <img> itself\n // can't work for a background image with content layered over it.\n ...(isImage\n ? { cursor: \"pointer\" }\n : {\n outline: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n outlineOffset: 2,\n cursor: editing ? \"text\" : \"pointer\",\n }),\n }}\n onClick={(e: React.MouseEvent) => {\n if (editing) return;\n e.preventDefault();\n // preventDefault only blocks the browser's native action (following\n // an href); it does not stop the click from bubbling. A field\n // rendered inside a Link would still trigger the Link's own onClick\n // and navigate anyway — stopPropagation is what actually claims the\n // click for editing instead. (Use `editable={false}` on fields\n // meant to stay pure navigation, e.g. inside a card that links to\n // that content's own page.)\n e.stopPropagation();\n beginEdit();\n }}\n // Spread only for inline text: images and links would receive invalid\n // DOM props (contentEditable on next/image, etc).\n {...(isInline\n ? {\n contentEditable: editing,\n suppressContentEditableWarning: true,\n onInput: (e: React.FormEvent<HTMLElement>) => {\n // Ref only — setState here re-renders and moves the caret.\n draftTextRef.current = e.currentTarget.textContent ?? \"\";\n },\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleSave();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n handleCancel();\n }\n },\n }\n : {})}\n />\n\n {/*\n Portaled to <body>, not rendered next to the field. The chrome is\n position:fixed with a high z-index, but z-index only ranks within the\n nearest stacking context — rendered inline, a field inside any ancestor\n that forms one (a z-indexed or transformed hero, say) traps its own\n popup there, where unrelated content stacked higher elsewhere on the\n page can paint straight over it. Also sidesteps a transformed ancestor\n silently re-anchoring position:fixed to itself instead of the viewport.\n */}\n {rect &&\n (isImage || editing) &&\n createPortal(\n <>\n {isImage && (\n <ImageMarker rect={rect} editing={editing} onEdit={beginEdit} />\n )}\n {editing && (\n <Chrome\n rect={rect}\n isInline={isInline}\n row={row}\n error={error}\n saving={saving}\n onChange={updateAttribute}\n onPatch={patchValue}\n onSave={handleSave}\n onCancel={handleCancel}\n />\n )}\n </>,\n document.body,\n )}\n </>\n );\n}\n\n/**\n * Dashed outline plus an \"Edit\" handle for an image field, positioned from the\n * image's measured rect rather than attached to the image. Everything here is\n * portaled to <body> by the caller, so it ranks above page content whatever the\n * image's own ancestors do, and never shifts the developer's layout.\n */\nfunction ImageMarker({\n rect,\n editing,\n onEdit,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n editing: boolean;\n onEdit: () => void;\n}) {\n const height = rect.bottom - rect.top;\n // Collapsed (display:none, zero-size) or scrolled fully out of view.\n if (rect.width <= 0 || height <= 0) return null;\n if (typeof window !== \"undefined\" && (rect.bottom < 0 || rect.top > window.innerHeight)) {\n return null;\n }\n\n const HANDLE_H = 26;\n // A logo-sized image would be mostly covered by its own handle, so small\n // ones get it just outside (above, or below when there's no room above).\n const small = height < HANDLE_H * 2 || rect.width < 120;\n let handleTop: number;\n if (small) {\n handleTop = rect.top - HANDLE_H - 4 >= 4 ? rect.top - HANDLE_H - 4 : rect.bottom + 4;\n } else {\n // Stays on screen for a tall background that's partly scrolled past, but\n // never leaves the image's own bounds.\n handleTop = Math.min(\n Math.max(rect.top + 6, 6),\n Math.max(rect.top + 6, rect.bottom - HANDLE_H - 6),\n );\n }\n const handleLeft = small ? rect.left : rect.left + 6;\n\n return (\n <>\n <div\n data-guardrail\n style={{\n ...S.markerOutline,\n top: rect.top - 2,\n left: rect.left - 2,\n width: rect.width + 4,\n height: height + 4,\n border: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n }}\n />\n {!editing && (\n <button\n type=\"button\"\n data-guardrail\n style={{ ...S.markerHandle, top: handleTop, left: handleLeft }}\n aria-label=\"Edit image\"\n title=\"Edit image\"\n onClick={(e) => {\n // React bubbles synthetic events through portals to the field's\n // *React* ancestors — a field inside a <Link> would otherwise\n // navigate on this click, same reason the field's own onClick\n // claims its click.\n e.preventDefault();\n e.stopPropagation();\n onEdit();\n }}\n >\n <PencilIcon /> Edit\n </button>\n )}\n </>\n );\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n isInline: boolean;\n row: OverrideRow;\n error: string | null;\n saving: boolean;\n onChange: (key: string, value: string) => void;\n onPatch: (patch: Partial<OverrideValue>) => void;\n onSave: () => void;\n onCancel: () => void;\n}) {\n const { storage } = useGRail();\n const router = useRouter();\n const [pickerOpen, setPickerOpen] = useState(false);\n const GAP = 8;\n // Flip above the element when there is no room below it.\n const below = rect.bottom + GAP;\n const flip = typeof window !== \"undefined\" && below > window.innerHeight - 120;\n\n // The panel is anchored at the field's left edge, so a field near the right\n // of the viewport (a nav link, say) would push it — and its Save button —\n // off-screen. Pull it back so its full width stays visible. 260 mirrors\n // anchoredPanel's minWidth; the inline bar is small enough not to need it.\n const PANEL_MIN_WIDTH = 260;\n const maxLeft =\n typeof window !== \"undefined\"\n ? window.innerWidth - (isInline ? 0 : PANEL_MIN_WIDTH) - 8\n : Infinity;\n\n const position: React.CSSProperties = {\n left: Math.max(8, Math.min(rect.left, maxLeft)),\n ...(flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }),\n };\n\n const actions = (\n <>\n <button\n style={S.iconBtnPrimary}\n onClick={onSave}\n disabled={saving}\n title=\"Save (Enter)\"\n aria-label=\"Save\"\n >\n {saving ? <SpinnerIcon /> : <CheckIcon />}\n </button>\n <button\n style={S.iconBtn}\n onClick={onCancel}\n disabled={saving}\n title=\"Cancel (Esc)\"\n aria-label=\"Cancel\"\n >\n <CloseIcon />\n </button>\n </>\n );\n\n // Text and buttons edit in place, so they only need the two controls.\n if (isInline) {\n return (\n <div data-guardrail style={{ ...S.anchoredBar, ...position }}>\n {actions}\n {error && <span style={{ ...S.error, margin: \"0 6px\" }}>{error}</span>}\n </div>\n );\n }\n\n // Images and links carry attributes with no on-page representation.\n return (\n <div\n data-guardrail\n style={{ ...S.anchoredPanel, ...position }}\n onClick={(e) => e.stopPropagation()}\n >\n {row.value.type === \"image\" && (\n <>\n {row.value.src && (\n // Plain img: previews must not depend on remotePatterns.\n <img src={row.value.src} alt=\"\" style={S.previewThumb} />\n )}\n\n {storage && (\n <button\n type=\"button\"\n style={S.secondaryBtn}\n onClick={() => setPickerOpen(true)}\n >\n {row.value.src ? \"Replace image\" : \"Choose image\"}\n </button>\n )}\n\n <label style={S.label}>Alt text</label>\n <input\n style={S.field}\n value={row.value.alt || \"\"}\n placeholder=\"Describe the image for screen readers\"\n onChange={(e) => onChange(\"alt\", e.target.value)}\n />\n\n {/* Only without storage: there is no picker then, so a URL is the one\n way to set an image. A typed URL has unknown proportions, so any\n stored dimensions are cleared. */}\n {!storage && (\n <>\n <label style={S.label}>Image URL</label>\n <input\n style={S.field}\n value={row.value.src || \"\"}\n onChange={(e) =>\n onPatch({ src: e.target.value, width: undefined, height: undefined })\n }\n />\n </>\n )}\n\n {pickerOpen && (\n <MediaPicker\n current={row.value.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n onPatch({ src: image.src, width: image.width, height: image.height })\n }\n />\n )}\n </>\n )}\n\n {row.value.type === \"link\" && (\n <>\n <label style={S.label}>Link text</label>\n <input\n style={S.field}\n value={row.value.text || \"\"}\n onChange={(e) => onChange(\"text\", e.target.value)}\n />\n <label style={S.label}>URL</label>\n <input\n style={S.field}\n value={row.value.href || \"\"}\n onChange={(e) => onChange(\"href\", e.target.value)}\n />\n </>\n )}\n\n {error && <div style={{ ...S.error, marginBottom: 8 }}>{error}</div>}\n <div\n style={{\n display: \"flex\",\n gap: 4,\n alignItems: \"center\",\n justifyContent: row.value.type === \"link\" ? \"space-between\" : \"flex-end\",\n }}\n >\n {/* The field's own click opens this editor, so an editable link can't\n be followed by clicking it — this is how an editor gets to the page. */}\n {row.value.type === \"link\" && (\n <button\n type=\"button\"\n style={{\n ...S.secondaryBtn,\n width: \"auto\",\n marginBottom: 0,\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 6,\n }}\n disabled={!row.value.href?.trim()}\n onClick={() => openHref(row.value.href ?? \"\", (path) => router.push(path))}\n title=\"Open this link\"\n aria-label=\"Open link\"\n >\n <OpenIcon /> Open\n </button>\n )}\n <div style={{ display: \"flex\", gap: 4 }}>{actions}</div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useRouter } from \"next/navigation\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from \"../Icons\";\nimport { openHref } from \"../openHref\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { HTMLKeys, OverrideRow, OverrideValue } from \"../../core/types\";\n\nexport function GRailClient({\n as,\n initialOverride,\n ...htmlProps\n}: {\n as?: HTMLKeys;\n initialOverride: OverrideRow;\n [key: string]: unknown;\n}) {\n const { isEditMode } = useGRail();\n const [editing, setEditing] = useState(false);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [row, setRow] = useState<OverrideRow>(initialOverride);\n\n const elementRef = useRef<HTMLElement | null>(null);\n const isImage = row.value.type === \"image\";\n // Image fields keep a rect for their whole time in edit mode (their marker\n // needs it); everything else only while actively editing.\n const rect = useAnchorRect(elementRef, editing || (isEditMode && isImage), {\n // Only the idle marker case (isImage, not actively editing) is gated —\n // an image mid-edit tracks unconditionally, same as text/link chrome.\n gateByVisibility: isImage && !editing,\n });\n\n /**\n * The text the element was rendered with when this edit session began.\n *\n * While editing, the DOM owns the text and React must not touch it. Feeding\n * state back into a contentEditable's children makes React replace the text\n * node on every keystroke, which resets the caret to offset 0 — typing then\n * appears reversed and backspace jumps to the start. Holding this value\n * constant means the children prop never changes, so React leaves the node\n * alone and the caret stays put.\n */\n const sessionTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n /** Live DOM text, updated without re-rendering. Read on save. */\n const draftTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n\n const isInline = row.value.type === \"text\" || row.value.type === \"button\";\n\n const beginEdit = () => {\n sessionTextRef.current = row.value.text ?? \"\";\n draftTextRef.current = row.value.text ?? \"\";\n setError(null);\n setEditing(true);\n };\n\n const updateAttribute = (key: string, value: string) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));\n\n const patchValue = (patch: Partial<OverrideValue>) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));\n\n const handleSave = async () => {\n // Inline text lives in the DOM, not in state, so read it back here.\n const next: OverrideRow = isInline\n ? { ...row, value: { ...row.value, text: draftTextRef.current } }\n : row;\n\n setSaving(true);\n setError(null);\n const res = await saveOverride(next);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Save failed.\");\n return;\n }\n setRow(next);\n setEditing(false);\n };\n\n const handleCancel = () => {\n setRow(initialOverride);\n draftTextRef.current = initialOverride.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = initialOverride.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n };\n\n // Clicking away from an active edit cancels it, the same as Escape. Anything\n // inside the field itself (repositioning the caret) or inside the anchored\n // toolbar/panel (both carry data-guardrail) is not \"away\" — checked via\n // closest() rather than React's onClick so a stopPropagation() inside the\n // chrome (image/link panel inputs) reliably suppresses this too, since that\n // call stops the native event before it ever reaches this document listener.\n useEffect(() => {\n if (!editing) return;\n const handlePointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (elementRef.current?.contains(target)) return;\n if (target instanceof Element && target.closest(\"[data-guardrail]\")) return;\n handleCancel();\n };\n document.addEventListener(\"mousedown\", handlePointerDown);\n return () => document.removeEventListener(\"mousedown\", handlePointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editing]);\n\n if (!isEditMode) {\n return <GRailRender as={as} {...htmlProps} override={row.value} />;\n }\n\n // Frozen during the session so React never rewrites the live text node.\n const renderedValue =\n editing && isInline\n ? { ...row.value, text: sessionTextRef.current }\n : row.value;\n\n return (\n <>\n <GRailRender\n as={as}\n {...htmlProps}\n ref={elementRef}\n override={renderedValue}\n // The outline and cursor come from guardrail.css, keyed off these\n // attributes — not an inline style, which a strict CSP refuses in\n // server-rendered markup (and which would be repeated on every field).\n // Images are left alone — no outline, position, or z-index — and get a\n // separate ImageMarker instead. Restyling the <img> itself can't work\n // for a background image with content layered over it.\n data-gr-field={isImage ? \"image\" : \"text\"}\n {...(editing && !isImage ? { \"data-gr-editing\": \"\" } : {})}\n onClick={(e: React.MouseEvent) => {\n if (editing) return;\n e.preventDefault();\n // preventDefault only blocks the browser's native action (following\n // an href); it does not stop the click from bubbling. A field\n // rendered inside a Link would still trigger the Link's own onClick\n // and navigate anyway — stopPropagation is what actually claims the\n // click for editing instead. (Use `editable={false}` on fields\n // meant to stay pure navigation, e.g. inside a card that links to\n // that content's own page.)\n e.stopPropagation();\n beginEdit();\n }}\n // Spread only for inline text: images and links would receive invalid\n // DOM props (contentEditable on next/image, etc).\n {...(isInline\n ? {\n contentEditable: editing,\n suppressContentEditableWarning: true,\n onInput: (e: React.FormEvent<HTMLElement>) => {\n // Ref only — setState here re-renders and moves the caret.\n draftTextRef.current = e.currentTarget.textContent ?? \"\";\n },\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleSave();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n handleCancel();\n }\n },\n }\n : {})}\n />\n\n {/*\n Portaled to <body>, not rendered next to the field. The chrome is\n position:fixed with a high z-index, but z-index only ranks within the\n nearest stacking context — rendered inline, a field inside any ancestor\n that forms one (a z-indexed or transformed hero, say) traps its own\n popup there, where unrelated content stacked higher elsewhere on the\n page can paint straight over it. Also sidesteps a transformed ancestor\n silently re-anchoring position:fixed to itself instead of the viewport.\n */}\n {rect &&\n (isImage || editing) &&\n createPortal(\n <>\n {isImage && (\n <ImageMarker rect={rect} editing={editing} onEdit={beginEdit} />\n )}\n {editing && (\n <Chrome\n rect={rect}\n isInline={isInline}\n row={row}\n error={error}\n saving={saving}\n onChange={updateAttribute}\n onPatch={patchValue}\n onSave={handleSave}\n onCancel={handleCancel}\n />\n )}\n </>,\n document.body,\n )}\n </>\n );\n}\n\n/**\n * Dashed outline plus an \"Edit\" handle for an image field, positioned from the\n * image's measured rect rather than attached to the image. Everything here is\n * portaled to <body> by the caller, so it ranks above page content whatever the\n * image's own ancestors do, and never shifts the developer's layout.\n */\nfunction ImageMarker({\n rect,\n editing,\n onEdit,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n editing: boolean;\n onEdit: () => void;\n}) {\n const height = rect.bottom - rect.top;\n // Collapsed (display:none, zero-size) or scrolled fully out of view.\n if (rect.width <= 0 || height <= 0) return null;\n if (typeof window !== \"undefined\" && (rect.bottom < 0 || rect.top > window.innerHeight)) {\n return null;\n }\n\n const HANDLE_H = 26;\n // A logo-sized image would be mostly covered by its own handle, so small\n // ones get it just outside (above, or below when there's no room above).\n const small = height < HANDLE_H * 2 || rect.width < 120;\n let handleTop: number;\n if (small) {\n handleTop = rect.top - HANDLE_H - 4 >= 4 ? rect.top - HANDLE_H - 4 : rect.bottom + 4;\n } else {\n // Stays on screen for a tall background that's partly scrolled past, but\n // never leaves the image's own bounds.\n handleTop = Math.min(\n Math.max(rect.top + 6, 6),\n Math.max(rect.top + 6, rect.bottom - HANDLE_H - 6),\n );\n }\n const handleLeft = small ? rect.left : rect.left + 6;\n\n return (\n <>\n <div\n data-guardrail\n className=\"gr-marker-outline\"\n {...(editing ? { \"data-gr-editing\": \"\" } : {})}\n style={{\n top: rect.top - 2,\n left: rect.left - 2,\n width: rect.width + 4,\n height: height + 4,\n }}\n />\n {!editing && (\n <button\n type=\"button\"\n data-guardrail\n className=\"gr-marker-handle\"\n style={{ top: handleTop, left: handleLeft }}\n aria-label=\"Edit image\"\n title=\"Edit image\"\n onClick={(e) => {\n // React bubbles synthetic events through portals to the field's\n // *React* ancestors — a field inside a <Link> would otherwise\n // navigate on this click, same reason the field's own onClick\n // claims its click.\n e.preventDefault();\n e.stopPropagation();\n onEdit();\n }}\n >\n <PencilIcon /> Edit\n </button>\n )}\n </>\n );\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n isInline: boolean;\n row: OverrideRow;\n error: string | null;\n saving: boolean;\n onChange: (key: string, value: string) => void;\n onPatch: (patch: Partial<OverrideValue>) => void;\n onSave: () => void;\n onCancel: () => void;\n}) {\n const { storage } = useGRail();\n const router = useRouter();\n const [pickerOpen, setPickerOpen] = useState(false);\n const GAP = 8;\n // Flip above the element when there is no room below it.\n const below = rect.bottom + GAP;\n const flip = typeof window !== \"undefined\" && below > window.innerHeight - 120;\n\n // The panel is anchored at the field's left edge, so a field near the right\n // of the viewport (a nav link, say) would push it — and its Save button —\n // off-screen. Pull it back so its full width stays visible. 260 mirrors\n // anchoredPanel's minWidth; the inline bar is small enough not to need it.\n const PANEL_MIN_WIDTH = 260;\n const maxLeft =\n typeof window !== \"undefined\"\n ? window.innerWidth - (isInline ? 0 : PANEL_MIN_WIDTH) - 8\n : Infinity;\n\n const position: React.CSSProperties = {\n left: Math.max(8, Math.min(rect.left, maxLeft)),\n ...(flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }),\n };\n\n const actions = (\n <>\n <button\n className=\"gr-icon-btn-primary\"\n onClick={onSave}\n disabled={saving}\n title=\"Save (Enter)\"\n aria-label=\"Save\"\n >\n {saving ? <SpinnerIcon /> : <CheckIcon />}\n </button>\n <button\n className=\"gr-icon-btn\"\n onClick={onCancel}\n disabled={saving}\n title=\"Cancel (Esc)\"\n aria-label=\"Cancel\"\n >\n <CloseIcon />\n </button>\n </>\n );\n\n // Text and buttons edit in place, so they only need the two controls.\n if (isInline) {\n return (\n <div data-guardrail className=\"gr-anchored-bar\" style={position}>\n {actions}\n {error && <span className=\"gr-error gr-mx-6\">{error}</span>}\n </div>\n );\n }\n\n // Images and links carry attributes with no on-page representation.\n return (\n <div\n data-guardrail\n className=\"gr-anchored-panel\"\n style={position}\n onClick={(e) => e.stopPropagation()}\n >\n {row.value.type === \"image\" && (\n <>\n {row.value.src && (\n // Plain img: previews must not depend on remotePatterns.\n <img src={row.value.src} alt=\"\" className=\"gr-preview-thumb\" />\n )}\n\n {storage && (\n <button\n type=\"button\"\n className=\"gr-secondary-btn\"\n onClick={() => setPickerOpen(true)}\n >\n {row.value.src ? \"Replace image\" : \"Choose image\"}\n </button>\n )}\n\n <label className=\"gr-label\">Alt text</label>\n <input\n className=\"gr-field\"\n value={row.value.alt || \"\"}\n placeholder=\"Describe the image for screen readers\"\n onChange={(e) => onChange(\"alt\", e.target.value)}\n />\n\n {/* Only without storage: there is no picker then, so a URL is the one\n way to set an image. A typed URL has unknown proportions, so any\n stored dimensions are cleared. */}\n {!storage && (\n <>\n <label className=\"gr-label\">Image URL</label>\n <input\n className=\"gr-field\"\n value={row.value.src || \"\"}\n onChange={(e) =>\n onPatch({ src: e.target.value, width: undefined, height: undefined })\n }\n />\n </>\n )}\n\n {pickerOpen && (\n <MediaPicker\n current={row.value.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n onPatch({ src: image.src, width: image.width, height: image.height })\n }\n />\n )}\n </>\n )}\n\n {row.value.type === \"link\" && (\n <>\n <label className=\"gr-label\">Link text</label>\n <input\n className=\"gr-field\"\n value={row.value.text || \"\"}\n onChange={(e) => onChange(\"text\", e.target.value)}\n />\n <label className=\"gr-label\">URL</label>\n <input\n className=\"gr-field\"\n value={row.value.href || \"\"}\n onChange={(e) => onChange(\"href\", e.target.value)}\n />\n </>\n )}\n\n {error && <div className=\"gr-error gr-mb-8\">{error}</div>}\n <div\n className={\n row.value.type === \"link\"\n ? \"gr-flex gr-gap-4 gr-items-center gr-justify-between\"\n : \"gr-flex gr-gap-4 gr-items-center gr-justify-end\"\n }\n >\n {/* The field's own click opens this editor, so an editable link can't\n be followed by clicking it — this is how an editor gets to the page. */}\n {row.value.type === \"link\" && (\n <button\n type=\"button\"\n className=\"gr-secondary-btn gr-w-auto gr-mb-0 gr-inline-flex gr-items-center gr-gap-6\"\n disabled={!row.value.href?.trim()}\n onClick={() => openHref(row.value.href ?? \"\", (path) => router.push(path))}\n title=\"Open this link\"\n aria-label=\"Open link\"\n >\n <OpenIcon /> Open\n </button>\n )}\n <div className=\"gr-flex gr-gap-4\">{actions}</div>\n </div>\n </div>\n );\n}\n"],"mappings":";AAqHW,SAwED,UAxEC,KAwED,YAxEC;AAnHX,OAAO,SAAS,WAAW,QAAQ,gBAAgB;AACnD,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,WAAW,WAAW,UAAU,YAAY,mBAAmB;AACxE,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAGrB,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAIG;AACD,QAAM,EAAE,WAAW,IAAI,SAAS;AAChC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAsB,eAAe;AAE3D,QAAM,aAAa,OAA2B,IAAI;AAClD,QAAM,UAAU,IAAI,MAAM,SAAS;AAGnC,QAAM,OAAO,cAAc,YAAY,WAAY,cAAc,SAAU;AAAA;AAAA;AAAA,IAGzE,kBAAkB,WAAW,CAAC;AAAA,EAChC,CAAC;AAYD,QAAM,iBAAiB,OAAe,gBAAgB,MAAM,QAAQ,EAAE;AAEtE,QAAM,eAAe,OAAe,gBAAgB,MAAM,QAAQ,EAAE;AAEpE,QAAM,WAAW,IAAI,MAAM,SAAS,UAAU,IAAI,MAAM,SAAS;AAEjE,QAAM,YAAY,MAAM;AACtB,mBAAe,UAAU,IAAI,MAAM,QAAQ;AAC3C,iBAAa,UAAU,IAAI,MAAM,QAAQ;AACzC,aAAS,IAAI;AACb,eAAW,IAAI;AAAA,EACjB;AAEA,QAAM,kBAAkB,CAAC,KAAa,UACpC,OAAO,CAAC,UAAU,EAAE,GAAG,MAAM,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,GAAG,MAAM,EAAE,EAAE;AAExE,QAAM,aAAa,CAAC,UAClB,OAAO,CAAC,UAAU,EAAE,GAAG,MAAM,OAAO,EAAE,GAAG,KAAK,OAAO,GAAG,MAAM,EAAE,EAAE;AAEpE,QAAM,aAAa,YAAY;AAE7B,UAAM,OAAoB,WACtB,EAAE,GAAG,KAAK,OAAO,EAAE,GAAG,IAAI,OAAO,MAAM,aAAa,QAAQ,EAAE,IAC9D;AAEJ,cAAU,IAAI;AACd,aAAS,IAAI;AACb,UAAM,MAAM,MAAM,aAAa,IAAI;AACnC,cAAU,KAAK;AAEf,QAAI,CAAC,IAAI,SAAS;AAChB,eAAS,IAAI,SAAS,cAAc;AACpC;AAAA,IACF;AACA,WAAO,IAAI;AACX,eAAW,KAAK;AAAA,EAClB;AAEA,QAAM,eAAe,MAAM;AACzB,WAAO,eAAe;AACtB,iBAAa,UAAU,gBAAgB,MAAM,QAAQ;AACrD,QAAI,YAAY,WAAW,SAAS;AAClC,iBAAW,QAAQ,cAAc,gBAAgB,MAAM,QAAQ;AAAA,IACjE;AACA,aAAS,IAAI;AACb,eAAW,KAAK;AAAA,EAClB;AAQA,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,oBAAoB,CAAC,MAAkB;AAC3C,YAAM,SAAS,EAAE;AACjB,UAAI,WAAW,SAAS,SAAS,MAAM,EAAG;AAC1C,UAAI,kBAAkB,WAAW,OAAO,QAAQ,kBAAkB,EAAG;AACrE,mBAAa;AAAA,IACf;AACA,aAAS,iBAAiB,aAAa,iBAAiB;AACxD,WAAO,MAAM,SAAS,oBAAoB,aAAa,iBAAiB;AAAA,EAE1E,GAAG,CAAC,OAAO,CAAC;AAEZ,MAAI,CAAC,YAAY;AACf,WAAO,oBAAC,eAAY,IAAS,GAAG,WAAW,UAAU,IAAI,OAAO;AAAA,EAClE;AAGA,QAAM,gBACJ,WAAW,WACP,EAAE,GAAG,IAAI,OAAO,MAAM,eAAe,QAAQ,IAC7C,IAAI;AAEV,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACC,GAAG;AAAA,QACJ,KAAK;AAAA,QACL,UAAU;AAAA,QAOV,iBAAe,UAAU,UAAU;AAAA,QAClC,GAAI,WAAW,CAAC,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;AAAA,QACxD,SAAS,CAAC,MAAwB;AAChC,cAAI,QAAS;AACb,YAAE,eAAe;AAQjB,YAAE,gBAAgB;AAClB,oBAAU;AAAA,QACZ;AAAA,QAGC,GAAI,WACD;AAAA,UACE,iBAAiB;AAAA,UACjB,gCAAgC;AAAA,UAChC,SAAS,CAAC,MAAoC;AAE5C,yBAAa,UAAU,EAAE,cAAc,eAAe;AAAA,UACxD;AAAA,UACA,WAAW,CAAC,MAA2B;AACrC,gBAAI,EAAE,QAAQ,WAAW,CAAC,EAAE,UAAU;AACpC,gBAAE,eAAe;AACjB,mBAAK,WAAW;AAAA,YAClB,WAAW,EAAE,QAAQ,UAAU;AAC7B,gBAAE,eAAe;AACjB,2BAAa;AAAA,YACf;AAAA,UACF;AAAA,QACF,IACA,CAAC;AAAA;AAAA,IACP;AAAA,IAWC,SACE,WAAW,YACZ;AAAA,MACE,iCACG;AAAA,mBACC,oBAAC,eAAY,MAAY,SAAkB,QAAQ,WAAW;AAAA,QAE/D,WACC;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,UAAU;AAAA,YACV,SAAS;AAAA,YACT,QAAQ;AAAA,YACR,UAAU;AAAA;AAAA,QACZ;AAAA,SAEJ;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;AAQA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,SAAS,KAAK,SAAS,KAAK;AAElC,MAAI,KAAK,SAAS,KAAK,UAAU,EAAG,QAAO;AAC3C,MAAI,OAAO,WAAW,gBAAgB,KAAK,SAAS,KAAK,KAAK,MAAM,OAAO,cAAc;AACvF,WAAO;AAAA,EACT;AAEA,QAAM,WAAW;AAGjB,QAAM,QAAQ,SAAS,WAAW,KAAK,KAAK,QAAQ;AACpD,MAAI;AACJ,MAAI,OAAO;AACT,gBAAY,KAAK,MAAM,WAAW,KAAK,IAAI,KAAK,MAAM,WAAW,IAAI,KAAK,SAAS;AAAA,EACrF,OAAO;AAGL,gBAAY,KAAK;AAAA,MACf,KAAK,IAAI,KAAK,MAAM,GAAG,CAAC;AAAA,MACxB,KAAK,IAAI,KAAK,MAAM,GAAG,KAAK,SAAS,WAAW,CAAC;AAAA,IACnD;AAAA,EACF;AACA,QAAM,aAAa,QAAQ,KAAK,OAAO,KAAK,OAAO;AAEnD,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,kBAAc;AAAA,QACd,WAAU;AAAA,QACT,GAAI,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;AAAA,QAC5C,OAAO;AAAA,UACL,KAAK,KAAK,MAAM;AAAA,UAChB,MAAM,KAAK,OAAO;AAAA,UAClB,OAAO,KAAK,QAAQ;AAAA,UACpB,QAAQ,SAAS;AAAA,QACnB;AAAA;AAAA,IACF;AAAA,IACC,CAAC,WACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,kBAAc;AAAA,QACd,WAAU;AAAA,QACV,OAAO,EAAE,KAAK,WAAW,MAAM,WAAW;AAAA,QAC1C,cAAW;AAAA,QACX,OAAM;AAAA,QACN,SAAS,CAAC,MAAM;AAKd,YAAE,eAAe;AACjB,YAAE,gBAAgB;AAClB,iBAAO;AAAA,QACT;AAAA,QAEA;AAAA,8BAAC,cAAW;AAAA,UAAE;AAAA;AAAA;AAAA,IAChB;AAAA,KAEJ;AAEJ;AAEA,SAAS,OAAO;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,QAAM,EAAE,QAAQ,IAAI,SAAS;AAC7B,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,MAAM;AAEZ,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,OAAO,OAAO,WAAW,eAAe,QAAQ,OAAO,cAAc;AAM3E,QAAM,kBAAkB;AACxB,QAAM,UACJ,OAAO,WAAW,cACd,OAAO,cAAc,WAAW,IAAI,mBAAmB,IACvD;AAEN,QAAM,WAAgC;AAAA,IACpC,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,OAAO,CAAC;AAAA,IAC9C,GAAI,OAAO,EAAE,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,MAAM;AAAA,EACtE;AAEA,QAAM,UACJ,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QAEV,mBAAS,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,IACzC;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU;AAAA,QACV,OAAM;AAAA,QACN,cAAW;AAAA,QAEX,8BAAC,aAAU;AAAA;AAAA,IACb;AAAA,KACF;AAIF,MAAI,UAAU;AACZ,WACE,qBAAC,SAAI,kBAAc,MAAC,WAAU,mBAAkB,OAAO,UACpD;AAAA;AAAA,MACA,SAAS,oBAAC,UAAK,WAAU,oBAAoB,iBAAM;AAAA,OACtD;AAAA,EAEJ;AAGA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,kBAAc;AAAA,MACd,WAAU;AAAA,MACV,OAAO;AAAA,MACP,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,MAEjC;AAAA,YAAI,MAAM,SAAS,WAClB,iCACG;AAAA,cAAI,MAAM;AAAA,UAET,oBAAC,SAAI,KAAK,IAAI,MAAM,KAAK,KAAI,IAAG,WAAU,oBAAmB;AAAA,UAG9D,WACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,SAAS,MAAM,cAAc,IAAI;AAAA,cAEhC,cAAI,MAAM,MAAM,kBAAkB;AAAA;AAAA,UACrC;AAAA,UAGF,oBAAC,WAAM,WAAU,YAAW,sBAAQ;AAAA,UACpC;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO,IAAI,MAAM,OAAO;AAAA,cACxB,aAAY;AAAA,cACZ,UAAU,CAAC,MAAM,SAAS,OAAO,EAAE,OAAO,KAAK;AAAA;AAAA,UACjD;AAAA,UAKC,CAAC,WACA,iCACE;AAAA,gCAAC,WAAM,WAAU,YAAW,uBAAS;AAAA,YACrC;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,OAAO,IAAI,MAAM,OAAO;AAAA,gBACxB,UAAU,CAAC,MACT,QAAQ,EAAE,KAAK,EAAE,OAAO,OAAO,OAAO,QAAW,QAAQ,OAAU,CAAC;AAAA;AAAA,YAExE;AAAA,aACF;AAAA,UAGD,cACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,IAAI,MAAM;AAAA,cACnB,SAAS,MAAM,cAAc,KAAK;AAAA,cAClC,UAAU,CAAC,UACT,QAAQ,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA;AAAA,UAExE;AAAA,WAEJ;AAAA,QAGD,IAAI,MAAM,SAAS,UAClB,iCACE;AAAA,8BAAC,WAAM,WAAU,YAAW,uBAAS;AAAA,UACrC;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO,IAAI,MAAM,QAAQ;AAAA,cACzB,UAAU,CAAC,MAAM,SAAS,QAAQ,EAAE,OAAO,KAAK;AAAA;AAAA,UAClD;AAAA,UACA,oBAAC,WAAM,WAAU,YAAW,iBAAG;AAAA,UAC/B;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO,IAAI,MAAM,QAAQ;AAAA,cACzB,UAAU,CAAC,MAAM,SAAS,QAAQ,EAAE,OAAO,KAAK;AAAA;AAAA,UAClD;AAAA,WACF;AAAA,QAGD,SAAS,oBAAC,SAAI,WAAU,oBAAoB,iBAAM;AAAA,QACnD;AAAA,UAAC;AAAA;AAAA,YACC,WACE,IAAI,MAAM,SAAS,SACf,wDACA;AAAA,YAKL;AAAA,kBAAI,MAAM,SAAS,UAClB;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,UAAU,CAAC,IAAI,MAAM,MAAM,KAAK;AAAA,kBAChC,SAAS,MAAM,SAAS,IAAI,MAAM,QAAQ,IAAI,CAAC,SAAS,OAAO,KAAK,IAAI,CAAC;AAAA,kBACzE,OAAM;AAAA,kBACN,cAAW;AAAA,kBAEX;AAAA,wCAAC,YAAS;AAAA,oBAAE;AAAA;AAAA;AAAA,cACd;AAAA,cAEF,oBAAC,SAAI,WAAU,oBAAoB,mBAAQ;AAAA;AAAA;AAAA,QAC7C;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|