@guard-rail/core 0.11.0 → 0.12.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 +49 -3
- package/dist/adapters/supabase/db.js +33 -0
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/cli/doctor.js +37 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/adapter-types.d.ts +35 -2
- package/dist/index.client.d.ts +2 -2
- package/dist/index.client.js +5 -1
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react/GRail/client.js +68 -9
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/Icons.d.ts +3 -1
- package/dist/react/Icons.js +5 -0
- package/dist/react/Icons.js.map +1 -1
- package/dist/server/actions.d.ts +19 -2
- package/dist/server/actions.js +35 -0
- package/dist/server/actions.js.map +1 -1
- package/package.json +1 -1
- package/supabase/admin/check-grants.sql +12 -3
- package/supabase/migrations/20261103000000_content_history.sql +211 -0
package/dist/index.js
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
skipSetPasswordAction,
|
|
11
11
|
forgotPasswordAction,
|
|
12
12
|
sendPasswordResetAction,
|
|
13
|
-
setFieldValue
|
|
13
|
+
setFieldValue,
|
|
14
|
+
getRevertInfo,
|
|
15
|
+
revertField
|
|
14
16
|
} from "./server/actions";
|
|
15
17
|
import { GRail } from "./react/GRail";
|
|
16
18
|
import { GRepeater } from "./react/Repeater";
|
|
@@ -42,6 +44,7 @@ export {
|
|
|
42
44
|
forgotPasswordAction,
|
|
43
45
|
getFieldValue,
|
|
44
46
|
getRepeaterItemBySlug,
|
|
47
|
+
getRevertInfo,
|
|
45
48
|
guardrailCspDirectives,
|
|
46
49
|
inviteMember,
|
|
47
50
|
isAdmin,
|
|
@@ -54,6 +57,7 @@ export {
|
|
|
54
57
|
reorderRepeaterItems,
|
|
55
58
|
resendEmails,
|
|
56
59
|
resendInvite,
|
|
60
|
+
revertField,
|
|
57
61
|
sendPasswordResetAction,
|
|
58
62
|
setFieldValue,
|
|
59
63
|
setPasswordAction,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport { consoleEmails } from \"./email/console\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,cAAc,oBAAoB;AAC3C,SAAS,qBAAqB;AAU9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Server-safe entry. Client components are reached through the files that\n// carry their own \"use client\" directive, so importing this from a Server\n// Component does not pull the editor into the client bundle.\nexport { withGuardrail } from \"./server/wrapper\";\n\n// Team management and the set-password step, exported so an app can build\n// its own UI instead of using the bundled drawer / sign-in bar.\nexport {\n listTeam,\n inviteMember,\n updateMemberRole,\n removeMember,\n createAssetUpload,\n listAssets,\n setPasswordAction,\n skipSetPasswordAction,\n forgotPasswordAction,\n sendPasswordResetAction,\n setFieldValue,\n getRevertInfo,\n revertField,\n} from \"./server/actions\";\nexport { GRail } from \"./react/GRail\";\nexport { GRepeater } from \"./react/Repeater\";\nexport type { GRepeaterProps, RepeaterItemProps } from \"./react/Repeater\";\nexport { configureGuardrail } from \"./core/config\";\nexport {\n listRepeaterItems,\n getRepeaterItemBySlug,\n} from \"./server/repeaterData\";\nexport { getFieldValue } from \"./server/fieldData\";\n// Role / edit-mode checks for a custom admin UI and the Server Actions behind\n// it. Plain server functions, not actions — see access.ts for why.\nexport { isAdmin, isEditor, canEdit } from \"./server/access\";\n// Invite delivery through Resend, as an onInvite handler — see email/resend.ts.\nexport { resendInvite, resendEmails } from \"./email/resend\";\nexport { consoleEmails } from \"./email/console\";\nexport type {\n ResendInviteOptions,\n ResendEmailOptions,\n ResendInviteContent,\n ResendInviteTemplateEvent,\n ResendResetTemplateEvent,\n} from \"./email/resend\";\n// Mutations, exported so an app can build its own add/remove/reorder UI\n// instead of the bundled chrome — same rationale as the team actions above.\nexport {\n addRepeaterItem,\n removeRepeaterItem,\n reorderRepeaterItems,\n} from \"./server/repeaterActions\";\nexport { useGRail } from \"./react/Provider\";\n// What a strict Content-Security-Policy has to allow for the editing chrome.\nexport { guardrailCspDirectives, cspString } from \"./core/csp\";\nexport type { GRCspDirectives, GRCspOptions } from \"./core/csp\";\n\nexport type {\n GRState,\n GRRole,\n GRMember,\n GRViewer,\n GRAsset,\n GRRepeaterItem,\n ActionState,\n SetPasswordState,\n OverrideRow,\n OverrideValue,\n OverrideType,\n PolymorphicProps,\n HTMLKeys,\n} from \"./core/types\";\nexport type {\n GRConfig,\n GRPrefetch,\n GRMembershipAdapter,\n GRInviteEvent,\n GRStorageAdapter,\n GRUploadTarget,\n GRRepeaterAdapter,\n GRDBAdapter,\n GRAuthAdapter,\n GRSession,\n GRStorageRow,\n AuthResult,\n AuthRedirectParams,\n GRPasswordResetEvent,\n GRRevertInfo,\n GRRevertResult,\n GRRateLimiter,\n GRRateLimitHit,\n GRRateLimitName,\n} from \"./core/adapter-types\";\n"],"mappings":"AAGA,SAAS,qBAAqB;AAI9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAE1B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;AAG9B,SAAS,SAAS,UAAU,eAAe;AAE3C,SAAS,cAAc,oBAAoB;AAC3C,SAAS,qBAAqB;AAU9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB,iBAAiB;","names":[]}
|
|
@@ -3,11 +3,12 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React, { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
|
-
import { saveOverride } from "../../server/actions";
|
|
6
|
+
import { getRevertInfo, revertField, saveOverride } from "../../server/actions";
|
|
7
|
+
import { generateScaffold } from "../../core/scaffold";
|
|
7
8
|
import { GRailRender } from "./render";
|
|
8
9
|
import { useGRail } from "../Provider";
|
|
9
10
|
import { useAnchorRect } from "../useAnchorRect";
|
|
10
|
-
import { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from "../Icons";
|
|
11
|
+
import { CheckIcon, CloseIcon, OpenIcon, PencilIcon, RevertIcon, SpinnerIcon } from "../Icons";
|
|
11
12
|
import { openHref } from "../openHref";
|
|
12
13
|
import { MediaPicker } from "../MediaPicker";
|
|
13
14
|
function GRailClient({
|
|
@@ -20,6 +21,8 @@ function GRailClient({
|
|
|
20
21
|
const [saving, setSaving] = useState(false);
|
|
21
22
|
const [error, setError] = useState(null);
|
|
22
23
|
const [row, setRow] = useState(initialOverride);
|
|
24
|
+
const [revert, setRevert] = useState(null);
|
|
25
|
+
const baselineRef = useRef(initialOverride);
|
|
23
26
|
const elementRef = useRef(null);
|
|
24
27
|
const isImage = row.value.type === "image";
|
|
25
28
|
const rect = useAnchorRect(elementRef, editing || isEditMode && isImage, {
|
|
@@ -35,6 +38,8 @@ function GRailClient({
|
|
|
35
38
|
draftTextRef.current = row.value.text ?? "";
|
|
36
39
|
setError(null);
|
|
37
40
|
setEditing(true);
|
|
41
|
+
setRevert(null);
|
|
42
|
+
getRevertInfo(row.pathKey).then(setRevert, () => setRevert(null));
|
|
38
43
|
};
|
|
39
44
|
const updateAttribute = (key, value) => setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));
|
|
40
45
|
const patchValue = (patch) => setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));
|
|
@@ -48,17 +53,46 @@ function GRailClient({
|
|
|
48
53
|
setError(res.error ?? "Save failed.");
|
|
49
54
|
return;
|
|
50
55
|
}
|
|
56
|
+
baselineRef.current = next;
|
|
51
57
|
setRow(next);
|
|
52
58
|
setEditing(false);
|
|
59
|
+
releaseFocus();
|
|
60
|
+
};
|
|
61
|
+
const releaseFocus = () => {
|
|
62
|
+
if (elementRef.current && document.activeElement === elementRef.current) {
|
|
63
|
+
elementRef.current.blur();
|
|
64
|
+
}
|
|
53
65
|
};
|
|
54
66
|
const handleCancel = () => {
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
const saved = baselineRef.current;
|
|
68
|
+
setRow(saved);
|
|
69
|
+
draftTextRef.current = saved.value.text ?? "";
|
|
57
70
|
if (isInline && elementRef.current) {
|
|
58
|
-
elementRef.current.textContent =
|
|
71
|
+
elementRef.current.textContent = saved.value.text ?? "";
|
|
59
72
|
}
|
|
60
73
|
setError(null);
|
|
61
74
|
setEditing(false);
|
|
75
|
+
releaseFocus();
|
|
76
|
+
};
|
|
77
|
+
const handleRevert = async () => {
|
|
78
|
+
setSaving(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
const res = await revertField(row.pathKey);
|
|
81
|
+
setSaving(false);
|
|
82
|
+
if (!res.success) {
|
|
83
|
+
setError(res.error ?? "Couldn't revert that.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const value = res.action === "restored" && res.value ? res.value : generateScaffold(row.pathKey, htmlProps).value;
|
|
87
|
+
const next = { ...row, value };
|
|
88
|
+
baselineRef.current = next;
|
|
89
|
+
setRow(next);
|
|
90
|
+
draftTextRef.current = value.text ?? "";
|
|
91
|
+
sessionTextRef.current = value.text ?? "";
|
|
92
|
+
if (isInline && elementRef.current) {
|
|
93
|
+
elementRef.current.textContent = value.text ?? "";
|
|
94
|
+
}
|
|
95
|
+
setEditing(false);
|
|
62
96
|
};
|
|
63
97
|
useEffect(() => {
|
|
64
98
|
if (!editing) return;
|
|
@@ -123,7 +157,9 @@ function GRailClient({
|
|
|
123
157
|
onChange: updateAttribute,
|
|
124
158
|
onPatch: patchValue,
|
|
125
159
|
onSave: handleSave,
|
|
126
|
-
onCancel: handleCancel
|
|
160
|
+
onCancel: handleCancel,
|
|
161
|
+
revert,
|
|
162
|
+
onRevert: handleRevert
|
|
127
163
|
}
|
|
128
164
|
)
|
|
129
165
|
] }),
|
|
@@ -190,6 +226,16 @@ function ImageMarker({
|
|
|
190
226
|
)
|
|
191
227
|
] });
|
|
192
228
|
}
|
|
229
|
+
function revertLabel(info) {
|
|
230
|
+
if (info.kind === "previous" && info.at) {
|
|
231
|
+
const when = new Date(info.at);
|
|
232
|
+
if (!Number.isNaN(when.getTime())) {
|
|
233
|
+
return `Revert to the version from ${when.toLocaleDateString(void 0, { month: "short", day: "numeric" })}`;
|
|
234
|
+
}
|
|
235
|
+
return "Revert to the previous version";
|
|
236
|
+
}
|
|
237
|
+
return "Reset to default";
|
|
238
|
+
}
|
|
193
239
|
function Chrome({
|
|
194
240
|
rect,
|
|
195
241
|
isInline,
|
|
@@ -199,7 +245,9 @@ function Chrome({
|
|
|
199
245
|
onChange,
|
|
200
246
|
onPatch,
|
|
201
247
|
onSave,
|
|
202
|
-
onCancel
|
|
248
|
+
onCancel,
|
|
249
|
+
revert,
|
|
250
|
+
onRevert
|
|
203
251
|
}) {
|
|
204
252
|
const { storage } = useGRail();
|
|
205
253
|
const router = useRouter();
|
|
@@ -231,10 +279,21 @@ function Chrome({
|
|
|
231
279
|
className: "gr-icon-btn",
|
|
232
280
|
onClick: onCancel,
|
|
233
281
|
disabled: saving,
|
|
234
|
-
title: "
|
|
235
|
-
"aria-label": "
|
|
282
|
+
title: "Close (Esc)",
|
|
283
|
+
"aria-label": "Close",
|
|
236
284
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
237
285
|
}
|
|
286
|
+
),
|
|
287
|
+
revert && revert.kind !== "none" && /* @__PURE__ */ jsx(
|
|
288
|
+
"button",
|
|
289
|
+
{
|
|
290
|
+
className: "gr-icon-btn",
|
|
291
|
+
onClick: onRevert,
|
|
292
|
+
disabled: saving,
|
|
293
|
+
title: revertLabel(revert),
|
|
294
|
+
"aria-label": revertLabel(revert),
|
|
295
|
+
children: /* @__PURE__ */ jsx(RevertIcon, {})
|
|
296
|
+
}
|
|
238
297
|
)
|
|
239
298
|
] });
|
|
240
299
|
if (isInline) {
|
|
@@ -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 { 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":"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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 { getRevertInfo, revertField, saveOverride } from \"../../server/actions\";\nimport { generateScaffold } from \"../../core/scaffold\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, OpenIcon, PencilIcon, RevertIcon, SpinnerIcon } from \"../Icons\";\nimport { openHref } from \"../openHref\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { GRRevertInfo } from \"../../core/adapter-types\";\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 /** What a revert would do for this field; fetched when the editor opens. */\n const [revert, setRevert] = useState<GRRevertInfo | null>(null);\n /**\n * The last value known to be saved: what the page loaded with, then whatever\n * a save or a revert since has made current. Closing the editor returns to\n * this, not to the page-load value.\n * the page-load value.\n */\n const baselineRef = useRef<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 setRevert(null);\n getRevertInfo(row.pathKey).then(setRevert, () => setRevert(null));\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 baselineRef.current = next;\n setRow(next);\n setEditing(false);\n releaseFocus();\n };\n\n // Closing the editor — the ✕, Esc, or clicking away all land here. It leaves\n // the field as it was last saved (unsaved typing is dropped) and releases\n // focus, so the caret doesn't linger on text that's no longer being edited.\n const releaseFocus = () => {\n if (elementRef.current && document.activeElement === elementRef.current) {\n elementRef.current.blur();\n }\n };\n\n const handleCancel = () => {\n const saved = baselineRef.current;\n setRow(saved);\n draftTextRef.current = saved.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = saved.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n releaseFocus();\n };\n\n // One step back: the previous saved value, or — with none left — the default\n // written in the page, which is rebuilt here from the same props the server\n // used, so the field updates in place without waiting for a refresh.\n const handleRevert = async () => {\n setSaving(true);\n setError(null);\n const res = await revertField(row.pathKey);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Couldn't revert that.\");\n return;\n }\n const value =\n res.action === \"restored\" && res.value\n ? res.value\n : generateScaffold(row.pathKey, htmlProps).value;\n const next: OverrideRow = { ...row, value };\n baselineRef.current = next;\n setRow(next);\n draftTextRef.current = value.text ?? \"\";\n sessionTextRef.current = value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = value.text ?? \"\";\n }\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 revert={revert}\n onRevert={handleRevert}\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\n/** \"Revert to the version from Oct 28\" / \"Reset to default\" — what the button will actually do. */\nfunction revertLabel(info: GRRevertInfo): string {\n if (info.kind === \"previous\" && info.at) {\n const when = new Date(info.at);\n if (!Number.isNaN(when.getTime())) {\n return `Revert to the version from ${when.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" })}`;\n }\n return \"Revert to the previous version\";\n }\n return \"Reset to default\";\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n revert,\n onRevert,\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 revert: GRRevertInfo | null;\n onRevert: () => 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=\"Close (Esc)\"\n aria-label=\"Close\"\n >\n <CloseIcon />\n </button>\n {/* Only when there is something to go back to, and labeled with what it will do. */}\n {revert && revert.kind !== \"none\" && (\n <button\n className=\"gr-icon-btn\"\n onClick={onRevert}\n disabled={saving}\n title={revertLabel(revert)}\n aria-label={revertLabel(revert)}\n >\n <RevertIcon />\n </button>\n )}\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":"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","names":[]}
|
package/dist/react/Icons.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
3
|
declare const CheckIcon: () => react_jsx_runtime.JSX.Element;
|
|
4
|
+
/** A circular arrow, for reverting a field to its previous value. */
|
|
5
|
+
declare const RevertIcon: () => react_jsx_runtime.JSX.Element;
|
|
4
6
|
declare const CloseIcon: () => react_jsx_runtime.JSX.Element;
|
|
5
7
|
declare const PencilIcon: () => react_jsx_runtime.JSX.Element;
|
|
6
8
|
declare const PeopleIcon: () => react_jsx_runtime.JSX.Element;
|
|
@@ -13,4 +15,4 @@ declare const SpinnerIcon: () => react_jsx_runtime.JSX.Element;
|
|
|
13
15
|
declare const OpenIcon: () => react_jsx_runtime.JSX.Element;
|
|
14
16
|
declare const KeyIcon: () => react_jsx_runtime.JSX.Element;
|
|
15
17
|
|
|
16
|
-
export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, KeyIcon, LogOutIcon, OpenIcon, PencilIcon, PeopleIcon, PlusIcon, SpinnerIcon, TrashIcon };
|
|
18
|
+
export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, KeyIcon, LogOutIcon, OpenIcon, PencilIcon, PeopleIcon, PlusIcon, RevertIcon, SpinnerIcon, TrashIcon };
|
package/dist/react/Icons.js
CHANGED
|
@@ -11,6 +11,10 @@ const base = {
|
|
|
11
11
|
strokeLinejoin: "round"
|
|
12
12
|
};
|
|
13
13
|
const CheckIcon = () => /* @__PURE__ */ jsx("svg", { ...base, "aria-hidden": "true", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) });
|
|
14
|
+
const RevertIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, "aria-hidden": "true", children: [
|
|
15
|
+
/* @__PURE__ */ jsx("polyline", { points: "1 4 1 10 7 10" }),
|
|
16
|
+
/* @__PURE__ */ jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
|
|
17
|
+
] });
|
|
14
18
|
const CloseIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, "aria-hidden": "true", children: [
|
|
15
19
|
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
16
20
|
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
@@ -75,6 +79,7 @@ export {
|
|
|
75
79
|
PencilIcon,
|
|
76
80
|
PeopleIcon,
|
|
77
81
|
PlusIcon,
|
|
82
|
+
RevertIcon,
|
|
78
83
|
SpinnerIcon,
|
|
79
84
|
TrashIcon
|
|
80
85
|
};
|
package/dist/react/Icons.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n\nexport const OpenIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n);\n\nexport const KeyIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\" />\n </svg>\n);\n"],"mappings":"AAmBI,
|
|
1
|
+
{"version":3,"sources":["../../src/react/Icons.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Inline SVG rather than an icon package: Guardrail's chrome should not add a\n * runtime dependency to every consuming app.\n */\nconst base = {\n width: 14,\n height: 14,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 3,\n strokeLinecap: \"round\" as const,\n strokeLinejoin: \"round\" as const,\n};\n\nexport const CheckIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n);\n\n/** A circular arrow, for reverting a field to its previous value. */\nexport const RevertIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <polyline points=\"1 4 1 10 7 10\" />\n <path d=\"M3.51 15a9 9 0 1 0 2.13-9.36L1 10\" />\n </svg>\n);\n\nexport const CloseIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n);\n\nexport const PencilIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M12 20h9\" />\n <path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" />\n </svg>\n);\n\nexport const PeopleIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\" />\n <circle cx=\"9\" cy=\"7\" r=\"4\" />\n <path d=\"M23 21v-2a4 4 0 0 0-3-3.87\" />\n <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />\n </svg>\n);\n\nexport const PlusIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\" />\n </svg>\n);\n\nexport const TrashIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6\" />\n <path d=\"M10 11v6M14 11v6M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2\" />\n </svg>\n);\n\nexport const ArrowUpIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n <polyline points=\"6 11 12 5 18 11\" />\n </svg>\n);\n\nexport const ArrowDownIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\" />\n <polyline points=\"18 13 12 19 6 13\" />\n </svg>\n);\n\nexport const LogOutIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" y1=\"12\" x2=\"9\" y2=\"12\" />\n </svg>\n);\n\nexport const SpinnerIcon = () => (\n <svg {...base} aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-6.2-8.6\">\n <animateTransform\n attributeName=\"transform\"\n type=\"rotate\"\n from=\"0 12 12\"\n to=\"360 12 12\"\n dur=\"0.7s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </svg>\n);\n\nexport const OpenIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n <polyline points=\"15 3 21 3 21 9\" />\n <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n </svg>\n);\n\nexport const KeyIcon = () => (\n <svg {...base} strokeWidth={2} aria-hidden=\"true\">\n <path d=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\" />\n </svg>\n);\n"],"mappings":"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","names":[]}
|
package/dist/server/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GRRole, GRAsset, GRMember, OverrideValue, SetPasswordState, ActionState, OverrideRow } from '../core/types.js';
|
|
2
|
-
import { GRUploadTarget, AuthRedirectParams } from '../core/adapter-types.js';
|
|
2
|
+
import { GRUploadTarget, GRRevertInfo, AuthRedirectParams } from '../core/adapter-types.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
|
|
5
5
|
/** `notice` is for a success the admin should still read, e.g. "no email was sent because...". */
|
|
@@ -28,6 +28,23 @@ declare function saveOverride(row: OverrideRow): Promise<Result>;
|
|
|
28
28
|
* <GRail>, and vice versa.
|
|
29
29
|
*/
|
|
30
30
|
declare function setFieldValue(id: string, value: string | OverrideValue): Promise<Result>;
|
|
31
|
+
/**
|
|
32
|
+
* What reverting this field would do — "previous" (with the date of the version
|
|
33
|
+
* it goes back to), "default" (back to what the page itself says), or "none".
|
|
34
|
+
* The editor asks when it opens, so visitors never pay for it.
|
|
35
|
+
*/
|
|
36
|
+
declare function getRevertInfo(id: string): Promise<GRRevertInfo>;
|
|
37
|
+
/**
|
|
38
|
+
* One step back for a field. Restores its previous saved value and returns it,
|
|
39
|
+
* or, with none left, removes the saved value (`action: "reset"`) so the field
|
|
40
|
+
* shows the default written in the page. Same permission as saving.
|
|
41
|
+
*/
|
|
42
|
+
declare function revertField(id: string): Promise<{
|
|
43
|
+
success: boolean;
|
|
44
|
+
error?: string;
|
|
45
|
+
action?: "restored" | "reset" | "none";
|
|
46
|
+
value?: OverrideValue;
|
|
47
|
+
}>;
|
|
31
48
|
/**
|
|
32
49
|
* Authorizes an upload and returns where the browser should send the bytes.
|
|
33
50
|
* Only metadata comes through here — the file itself goes straight to storage.
|
|
@@ -100,4 +117,4 @@ declare function skipSetPasswordAction(): Promise<{
|
|
|
100
117
|
success: boolean;
|
|
101
118
|
}>;
|
|
102
119
|
|
|
103
|
-
export { completeAuthRedirectAction, createAssetUpload, forgotPasswordAction, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, saveOverride, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
|
|
120
|
+
export { completeAuthRedirectAction, createAssetUpload, forgotPasswordAction, getRevertInfo, inviteMember, listAssets, listTeam, loginAction, logoutAction, removeMember, revertField, saveOverride, sendPasswordResetAction, setFieldValue, setPasswordAction, skipSetPasswordAction, updateMemberRole };
|
package/dist/server/actions.js
CHANGED
|
@@ -57,6 +57,39 @@ async function writeOverride(rawPathKey, rawValue) {
|
|
|
57
57
|
return { success: false, error: "Database write failed." };
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
async function getRevertInfo(id) {
|
|
61
|
+
const viewer = await requireRole(["admin", "editor"]);
|
|
62
|
+
const pathKey = validPathKey(id);
|
|
63
|
+
if (!viewer || !pathKey) return { kind: "none" };
|
|
64
|
+
const db = getDB();
|
|
65
|
+
if (!db.getRevertInfo) return { kind: "none" };
|
|
66
|
+
try {
|
|
67
|
+
return await db.getRevertInfo(pathKey, await resolveSiteId());
|
|
68
|
+
} catch (e) {
|
|
69
|
+
rethrowIfBailout(e);
|
|
70
|
+
return { kind: "none" };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function revertField(id) {
|
|
74
|
+
const viewer = await requireRole(["admin", "editor"]);
|
|
75
|
+
if (!viewer) return DENIED;
|
|
76
|
+
const pathKey = validPathKey(id);
|
|
77
|
+
if (!pathKey) return { success: false, error: "A valid field id is required." };
|
|
78
|
+
const db = getDB();
|
|
79
|
+
if (!db.revertOverride) {
|
|
80
|
+
return { success: false, error: "This site's database setup doesn't keep history, so there's nothing to revert to." };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const res = await db.revertOverride(pathKey, await resolveSiteId());
|
|
84
|
+
if (!res.success) return { success: false, error: res.error };
|
|
85
|
+
revalidatePath("/", "layout");
|
|
86
|
+
return res.action === "restored" ? { success: true, action: "restored", value: res.value } : { success: true, action: res.action };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
rethrowIfBailout(e);
|
|
89
|
+
console.error("Guardrail: revert failed", e);
|
|
90
|
+
return { success: false, error: "Couldn't revert that." };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
60
93
|
const IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif"];
|
|
61
94
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
62
95
|
async function createAssetUpload(file) {
|
|
@@ -381,12 +414,14 @@ export {
|
|
|
381
414
|
completeAuthRedirectAction,
|
|
382
415
|
createAssetUpload,
|
|
383
416
|
forgotPasswordAction,
|
|
417
|
+
getRevertInfo,
|
|
384
418
|
inviteMember,
|
|
385
419
|
listAssets,
|
|
386
420
|
listTeam,
|
|
387
421
|
loginAction,
|
|
388
422
|
logoutAction,
|
|
389
423
|
removeMember,
|
|
424
|
+
revertField,
|
|
390
425
|
saveOverride,
|
|
391
426
|
sendPasswordResetAction,
|
|
392
427
|
setFieldValue,
|