@guard-rail/core 0.12.0 → 0.13.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 +53 -4
- package/dist/adapters/supabase/db.js +4 -1
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +4 -1
- package/dist/core/seo.d.ts +69 -0
- package/dist/core/seo.js +23 -0
- package/dist/core/seo.js.map +1 -0
- package/dist/index.client.d.ts +3 -1
- package/dist/index.client.js +5 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.js +14 -1
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/GRail/client.js +1 -10
- 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/react/PageSettings.d.ts +12 -0
- package/dist/react/PageSettings.js +275 -0
- package/dist/react/PageSettings.js.map +1 -0
- package/dist/react/guardrail.css +8 -0
- package/dist/react/revert.d.ts +8 -0
- package/dist/react/revert.js +14 -0
- package/dist/react/revert.js.map +1 -0
- package/dist/server/actions.d.ts +19 -1
- package/dist/server/actions.js +77 -0
- package/dist/server/actions.js.map +1 -1
- package/dist/server/loader.js +3 -1
- package/dist/server/loader.js.map +1 -1
- package/dist/server/seo.d.ts +33 -0
- package/dist/server/seo.js +51 -0
- package/dist/server/seo.js.map +1 -0
- package/package.json +1 -1
|
@@ -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 { 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":[]}
|
|
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 { revertLabel } from \"../revert\";\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\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":";AA4KW,SAwED,UAxEC,KAwED,YAxEC;AA1KX,OAAO,SAAS,WAAW,QAAQ,gBAAgB;AACnD,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,eAAe,aAAa,oBAAoB;AACzD,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,WAAW,WAAW,UAAU,YAAY,YAAY,mBAAmB;AACpF,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAIrB,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,CAAC,QAAQ,SAAS,IAAI,SAA8B,IAAI;AAO9D,QAAM,cAAc,OAAoB,eAAe;AAEvD,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;AACf,cAAU,IAAI;AACd,kBAAc,IAAI,OAAO,EAAE,KAAK,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,EAClE;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,gBAAY,UAAU;AACtB,WAAO,IAAI;AACX,eAAW,KAAK;AAChB,iBAAa;AAAA,EACf;AAKA,QAAM,eAAe,MAAM;AACzB,QAAI,WAAW,WAAW,SAAS,kBAAkB,WAAW,SAAS;AACvE,iBAAW,QAAQ,KAAK;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,eAAe,MAAM;AACzB,UAAM,QAAQ,YAAY;AAC1B,WAAO,KAAK;AACZ,iBAAa,UAAU,MAAM,MAAM,QAAQ;AAC3C,QAAI,YAAY,WAAW,SAAS;AAClC,iBAAW,QAAQ,cAAc,MAAM,MAAM,QAAQ;AAAA,IACvD;AACA,aAAS,IAAI;AACb,eAAW,KAAK;AAChB,iBAAa;AAAA,EACf;AAKA,QAAM,eAAe,YAAY;AAC/B,cAAU,IAAI;AACd,aAAS,IAAI;AACb,UAAM,MAAM,MAAM,YAAY,IAAI,OAAO;AACzC,cAAU,KAAK;AAEf,QAAI,CAAC,IAAI,SAAS;AAChB,eAAS,IAAI,SAAS,uBAAuB;AAC7C;AAAA,IACF;AACA,UAAM,QACJ,IAAI,WAAW,cAAc,IAAI,QAC7B,IAAI,QACJ,iBAAiB,IAAI,SAAS,SAAS,EAAE;AAC/C,UAAM,OAAoB,EAAE,GAAG,KAAK,MAAM;AAC1C,gBAAY,UAAU;AACtB,WAAO,IAAI;AACX,iBAAa,UAAU,MAAM,QAAQ;AACrC,mBAAe,UAAU,MAAM,QAAQ;AACvC,QAAI,YAAY,WAAW,SAAS;AAClC,iBAAW,QAAQ,cAAc,MAAM,QAAQ;AAAA,IACjD;AACA,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,YACV;AAAA,YACA,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;AAAA,EACA;AAAA,EACA;AACF,GAYG;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,IAEC,UAAU,OAAO,SAAS,UACzB;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,SAAS;AAAA,QACT,UAAU;AAAA,QACV,OAAO,YAAY,MAAM;AAAA,QACzB,cAAY,YAAY,MAAM;AAAA,QAE9B,8BAAC,cAAW;AAAA;AAAA,IACd;AAAA,KAEJ;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":[]}
|
package/dist/react/Icons.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
3
3
|
declare const CheckIcon: () => react_jsx_runtime.JSX.Element;
|
|
4
4
|
/** A circular arrow, for reverting a field to its previous value. */
|
|
5
5
|
declare const RevertIcon: () => react_jsx_runtime.JSX.Element;
|
|
6
|
+
/** A gear, for page settings. */
|
|
7
|
+
declare const SettingsIcon: () => react_jsx_runtime.JSX.Element;
|
|
6
8
|
declare const CloseIcon: () => react_jsx_runtime.JSX.Element;
|
|
7
9
|
declare const PencilIcon: () => react_jsx_runtime.JSX.Element;
|
|
8
10
|
declare const PeopleIcon: () => react_jsx_runtime.JSX.Element;
|
|
@@ -15,4 +17,4 @@ declare const SpinnerIcon: () => react_jsx_runtime.JSX.Element;
|
|
|
15
17
|
declare const OpenIcon: () => react_jsx_runtime.JSX.Element;
|
|
16
18
|
declare const KeyIcon: () => react_jsx_runtime.JSX.Element;
|
|
17
19
|
|
|
18
|
-
export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, KeyIcon, LogOutIcon, OpenIcon, PencilIcon, PeopleIcon, PlusIcon, RevertIcon, SpinnerIcon, TrashIcon };
|
|
20
|
+
export { ArrowDownIcon, ArrowUpIcon, CheckIcon, CloseIcon, KeyIcon, LogOutIcon, OpenIcon, PencilIcon, PeopleIcon, PlusIcon, RevertIcon, SettingsIcon, SpinnerIcon, TrashIcon };
|
package/dist/react/Icons.js
CHANGED
|
@@ -15,6 +15,10 @@ const RevertIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, "aria-hidden": "
|
|
|
15
15
|
/* @__PURE__ */ jsx("polyline", { points: "1 4 1 10 7 10" }),
|
|
16
16
|
/* @__PURE__ */ jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })
|
|
17
17
|
] });
|
|
18
|
+
const SettingsIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, strokeWidth: 2.4, "aria-hidden": "true", children: [
|
|
19
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
|
|
20
|
+
/* @__PURE__ */ jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
|
|
21
|
+
] });
|
|
18
22
|
const CloseIcon = () => /* @__PURE__ */ jsxs("svg", { ...base, "aria-hidden": "true", children: [
|
|
19
23
|
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
20
24
|
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
@@ -80,6 +84,7 @@ export {
|
|
|
80
84
|
PeopleIcon,
|
|
81
85
|
PlusIcon,
|
|
82
86
|
RevertIcon,
|
|
87
|
+
SettingsIcon,
|
|
83
88
|
SpinnerIcon,
|
|
84
89
|
TrashIcon
|
|
85
90
|
};
|
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\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":[]}
|
|
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\n/** A gear, for page settings. */\nexport const SettingsIcon = () => (\n <svg {...base} strokeWidth={2.4} aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n <path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z\" />\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":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-page title, description, social image and "hide from search engines",
|
|
5
|
+
* saved as ordinary content rows (so they get history and revert like
|
|
6
|
+
* everything else) and applied by guardrailMetadata() in the page.
|
|
7
|
+
*/
|
|
8
|
+
declare function PageSettingsDrawer({ onClose }: {
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}): react_jsx_runtime.JSX.Element;
|
|
11
|
+
|
|
12
|
+
export { PageSettingsDrawer };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useCallback, useEffect, useState, useTransition } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { getPageSettings, revertField, savePageSettings } from "../server/actions";
|
|
6
|
+
import { CloseIcon, RevertIcon } from "./Icons";
|
|
7
|
+
import { MediaPicker } from "./MediaPicker";
|
|
8
|
+
import { revertLabel } from "./revert";
|
|
9
|
+
import {
|
|
10
|
+
SEO_LIMITS,
|
|
11
|
+
normalizePagePath,
|
|
12
|
+
seoKey
|
|
13
|
+
} from "../core/seo";
|
|
14
|
+
const toForm = (s) => ({
|
|
15
|
+
title: s.title,
|
|
16
|
+
description: s.description,
|
|
17
|
+
image: s.image,
|
|
18
|
+
noindex: s.saved.noindex ? s.noindex : null
|
|
19
|
+
});
|
|
20
|
+
function readHead() {
|
|
21
|
+
const meta = (selector) => document.querySelector(selector)?.content ?? "";
|
|
22
|
+
return {
|
|
23
|
+
title: document.title,
|
|
24
|
+
description: meta('meta[name="description"]'),
|
|
25
|
+
image: meta('meta[property="og:image"]'),
|
|
26
|
+
noindex: /noindex/i.test(meta('meta[name="robots"]'))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const TITLE_HINT = 60;
|
|
30
|
+
const DESCRIPTION_HINT = 160;
|
|
31
|
+
function PageSettingsDrawer({ onClose }) {
|
|
32
|
+
const pathname = usePathname();
|
|
33
|
+
const path = normalizePagePath(pathname) ?? "/";
|
|
34
|
+
const [settings, setSettings] = useState(null);
|
|
35
|
+
const [form, setForm] = useState(null);
|
|
36
|
+
const [head, setHead] = useState(readHead);
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
const [notice, setNotice] = useState(null);
|
|
39
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
40
|
+
const [pending, startTransition] = useTransition();
|
|
41
|
+
const load = useCallback(async () => {
|
|
42
|
+
const res = await getPageSettings(path);
|
|
43
|
+
if (!res.success) {
|
|
44
|
+
setError(res.error);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setSettings(res.settings);
|
|
48
|
+
setForm(toForm(res.settings));
|
|
49
|
+
}, [path]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
void load();
|
|
52
|
+
}, [load]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const observer = new MutationObserver(() => setHead(readHead()));
|
|
55
|
+
observer.observe(document.head, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
56
|
+
return () => observer.disconnect();
|
|
57
|
+
}, []);
|
|
58
|
+
const [closing, setClosing] = useState(false);
|
|
59
|
+
const requestClose = useCallback(() => {
|
|
60
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return onClose();
|
|
61
|
+
setClosing(true);
|
|
62
|
+
}, [onClose]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!closing) return;
|
|
65
|
+
const t = window.setTimeout(onClose, 400);
|
|
66
|
+
return () => window.clearTimeout(t);
|
|
67
|
+
}, [closing, onClose]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const onKey = (e) => e.key === "Escape" && !pickerOpen && requestClose();
|
|
70
|
+
window.addEventListener("keydown", onKey);
|
|
71
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
72
|
+
}, [requestClose, pickerOpen]);
|
|
73
|
+
const initial = settings ? toForm(settings) : null;
|
|
74
|
+
const dirty = {
|
|
75
|
+
title: !!form && !!initial && form.title !== initial.title,
|
|
76
|
+
description: !!form && !!initial && form.description !== initial.description,
|
|
77
|
+
image: !!form && !!initial && (form.image.src !== initial.image.src || form.image.alt !== initial.image.alt),
|
|
78
|
+
noindex: !!form && !!initial && form.noindex !== initial.noindex
|
|
79
|
+
};
|
|
80
|
+
const anyDirty = Object.values(dirty).some(Boolean);
|
|
81
|
+
const save = () => startTransition(async () => {
|
|
82
|
+
if (!form) return;
|
|
83
|
+
setError(null);
|
|
84
|
+
setNotice(null);
|
|
85
|
+
const patch = {};
|
|
86
|
+
if (dirty.title) patch.title = form.title;
|
|
87
|
+
if (dirty.description) patch.description = form.description;
|
|
88
|
+
if (dirty.image) patch.image = form.image.src ? form.image : null;
|
|
89
|
+
if (dirty.noindex && form.noindex !== null) patch.noindex = form.noindex;
|
|
90
|
+
const res = await savePageSettings(path, patch);
|
|
91
|
+
if (!res.success) setError(res.error ?? "Couldn't save that.");
|
|
92
|
+
else setNotice("Saved.");
|
|
93
|
+
await load();
|
|
94
|
+
});
|
|
95
|
+
const revert = (field) => startTransition(async () => {
|
|
96
|
+
setError(null);
|
|
97
|
+
setNotice(null);
|
|
98
|
+
const res = await revertField(seoKey(path, field));
|
|
99
|
+
if (!res.success) setError(res.error ?? "Couldn't revert that.");
|
|
100
|
+
else setNotice(res.action === "restored" ? "Reverted to the previous version." : "Back to the page's default.");
|
|
101
|
+
await load();
|
|
102
|
+
});
|
|
103
|
+
const revertButton = (field) => {
|
|
104
|
+
const info = settings?.revert[field];
|
|
105
|
+
if (!info || info.kind === "none") return null;
|
|
106
|
+
return /* @__PURE__ */ jsx(
|
|
107
|
+
"button",
|
|
108
|
+
{
|
|
109
|
+
type: "button",
|
|
110
|
+
className: "gr-icon-btn gr-text-muted gr-size-22",
|
|
111
|
+
onClick: () => revert(field),
|
|
112
|
+
disabled: pending,
|
|
113
|
+
title: revertLabel(info),
|
|
114
|
+
"aria-label": revertLabel(info),
|
|
115
|
+
children: /* @__PURE__ */ jsx(RevertIcon, {})
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
const counter = (length, hint) => /* @__PURE__ */ jsxs("p", { className: `gr-counter${length > hint ? " gr-text-danger" : ""}`, children: [
|
|
120
|
+
length,
|
|
121
|
+
" characters \xB7 search results show about ",
|
|
122
|
+
hint
|
|
123
|
+
] });
|
|
124
|
+
const noindexChecked = form ? form.noindex ?? head.noindex : false;
|
|
125
|
+
const savedImage = !!settings?.saved.image && !!settings.image.src;
|
|
126
|
+
const placeholder = (field, fromHead) => settings?.saved[field] && settings[field] ? `Cleared \u2014 the page's default ${field} is used` : fromHead;
|
|
127
|
+
const shownImage = form?.image.src || (savedImage ? "" : head.image);
|
|
128
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
129
|
+
/* @__PURE__ */ jsx(
|
|
130
|
+
"div",
|
|
131
|
+
{
|
|
132
|
+
"data-guardrail": true,
|
|
133
|
+
"data-guardrail-scrim": true,
|
|
134
|
+
...closing ? { "data-closing": "" } : {},
|
|
135
|
+
className: "gr-scrim",
|
|
136
|
+
onClick: requestClose
|
|
137
|
+
}
|
|
138
|
+
),
|
|
139
|
+
/* @__PURE__ */ jsxs(
|
|
140
|
+
"aside",
|
|
141
|
+
{
|
|
142
|
+
"data-guardrail": true,
|
|
143
|
+
"data-guardrail-team": true,
|
|
144
|
+
...closing ? { "data-closing": "" } : {},
|
|
145
|
+
onAnimationEnd: (e) => {
|
|
146
|
+
if (closing && e.target === e.currentTarget) onClose();
|
|
147
|
+
},
|
|
148
|
+
className: "gr-drawer",
|
|
149
|
+
role: "dialog",
|
|
150
|
+
"aria-label": "Page settings",
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsxs("header", { className: "gr-drawer-head", children: [
|
|
153
|
+
/* @__PURE__ */ jsx("h2", { className: "gr-drawer-title", children: "Page settings" }),
|
|
154
|
+
/* @__PURE__ */ jsx("button", { className: "gr-icon-btn gr-text-muted", onClick: requestClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
155
|
+
] }),
|
|
156
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-drawer-body", children: [
|
|
157
|
+
/* @__PURE__ */ jsxs("p", { className: "gr-fs-12 gr-text-muted gr-mb-12", children: [
|
|
158
|
+
"How ",
|
|
159
|
+
/* @__PURE__ */ jsx("strong", { children: path }),
|
|
160
|
+
" appears in search results and when it's shared."
|
|
161
|
+
] }),
|
|
162
|
+
error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mb-12", children: error }),
|
|
163
|
+
notice && /* @__PURE__ */ jsx("div", { className: "gr-fs-12 gr-text-muted gr-mb-12", role: "status", children: notice }),
|
|
164
|
+
!form ? !error && /* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-text-muted", children: "Loading\u2026" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
165
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-meta-row", children: [
|
|
166
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", htmlFor: "gr-meta-title", children: "Title" }),
|
|
167
|
+
revertButton("title")
|
|
168
|
+
] }),
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
"input",
|
|
171
|
+
{
|
|
172
|
+
id: "gr-meta-title",
|
|
173
|
+
className: "gr-field",
|
|
174
|
+
value: form.title,
|
|
175
|
+
placeholder: placeholder("title", head.title),
|
|
176
|
+
maxLength: SEO_LIMITS.title,
|
|
177
|
+
onChange: (e) => setForm({ ...form, title: e.target.value })
|
|
178
|
+
}
|
|
179
|
+
),
|
|
180
|
+
counter(form.title.length || head.title.length, TITLE_HINT),
|
|
181
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-meta-row", children: [
|
|
182
|
+
/* @__PURE__ */ jsx("label", { className: "gr-label", htmlFor: "gr-meta-description", children: "Description" }),
|
|
183
|
+
revertButton("description")
|
|
184
|
+
] }),
|
|
185
|
+
/* @__PURE__ */ jsx(
|
|
186
|
+
"textarea",
|
|
187
|
+
{
|
|
188
|
+
id: "gr-meta-description",
|
|
189
|
+
className: "gr-field gr-field-area",
|
|
190
|
+
value: form.description,
|
|
191
|
+
placeholder: placeholder("description", head.description),
|
|
192
|
+
maxLength: SEO_LIMITS.description,
|
|
193
|
+
onChange: (e) => setForm({ ...form, description: e.target.value })
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
counter(form.description.length || head.description.length, DESCRIPTION_HINT),
|
|
197
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-meta-row", children: [
|
|
198
|
+
/* @__PURE__ */ jsx("span", { className: "gr-label", children: "Social image" }),
|
|
199
|
+
revertButton("image")
|
|
200
|
+
] }),
|
|
201
|
+
shownImage ? (
|
|
202
|
+
// Plain img: the admin UI mustn't depend on next/image's remotePatterns.
|
|
203
|
+
/* @__PURE__ */ jsx("img", { className: "gr-meta-thumb", src: shownImage, alt: "" })
|
|
204
|
+
) : !savedImage && /* @__PURE__ */ jsx("p", { className: "gr-fs-12 gr-text-muted gr-mb-8", children: "No image set." }),
|
|
205
|
+
!form.image.src && savedImage && /* @__PURE__ */ jsx("p", { className: "gr-counter", children: "The saved image will be removed; the page's default is used." }),
|
|
206
|
+
!form.image.src && !savedImage && head.image && /* @__PURE__ */ jsx("p", { className: "gr-counter", children: "The page's default image." }),
|
|
207
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-flex gr-gap-8 gr-mb-8", children: [
|
|
208
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "gr-secondary-btn gr-w-auto gr-mb-0", onClick: () => setPickerOpen(true), children: "Choose image" }),
|
|
209
|
+
form.image.src && /* @__PURE__ */ jsx(
|
|
210
|
+
"button",
|
|
211
|
+
{
|
|
212
|
+
type: "button",
|
|
213
|
+
className: "gr-text-btn",
|
|
214
|
+
onClick: () => setForm({ ...form, image: { src: "", alt: "" } }),
|
|
215
|
+
children: "Remove"
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
] }),
|
|
219
|
+
form.image.src && /* @__PURE__ */ jsx(
|
|
220
|
+
"input",
|
|
221
|
+
{
|
|
222
|
+
className: "gr-field",
|
|
223
|
+
value: form.image.alt,
|
|
224
|
+
placeholder: "Describe the image",
|
|
225
|
+
"aria-label": "Image description",
|
|
226
|
+
onChange: (e) => setForm({ ...form, image: { ...form.image, alt: e.target.value } })
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-meta-row gr-mt-8", children: [
|
|
230
|
+
/* @__PURE__ */ jsxs("label", { className: "gr-check", children: [
|
|
231
|
+
/* @__PURE__ */ jsx(
|
|
232
|
+
"input",
|
|
233
|
+
{
|
|
234
|
+
type: "checkbox",
|
|
235
|
+
checked: noindexChecked,
|
|
236
|
+
onChange: (e) => setForm({ ...form, noindex: e.target.checked })
|
|
237
|
+
}
|
|
238
|
+
),
|
|
239
|
+
/* @__PURE__ */ jsx("span", { children: "Hide this page from search engines" })
|
|
240
|
+
] }),
|
|
241
|
+
revertButton("noindex")
|
|
242
|
+
] }),
|
|
243
|
+
/* @__PURE__ */ jsx(
|
|
244
|
+
"button",
|
|
245
|
+
{
|
|
246
|
+
type: "button",
|
|
247
|
+
className: "gr-primary-btn gr-w-full gr-mt-16",
|
|
248
|
+
onClick: save,
|
|
249
|
+
disabled: pending || !anyDirty,
|
|
250
|
+
children: pending ? "Saving\u2026" : "Save"
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ jsx("p", { className: "gr-fs-11 gr-text-subtle gr-mt-10", children: "Applies to pages whose metadata comes from guardrailMetadata()." })
|
|
254
|
+
] })
|
|
255
|
+
] })
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
),
|
|
259
|
+
pickerOpen && form && /* @__PURE__ */ jsx(
|
|
260
|
+
MediaPicker,
|
|
261
|
+
{
|
|
262
|
+
current: form.image.src,
|
|
263
|
+
onClose: () => setPickerOpen(false),
|
|
264
|
+
onSelect: (image) => setForm({
|
|
265
|
+
...form,
|
|
266
|
+
image: { src: image.src, alt: form.image.alt, width: image.width, height: image.height }
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
] });
|
|
271
|
+
}
|
|
272
|
+
export {
|
|
273
|
+
PageSettingsDrawer
|
|
274
|
+
};
|
|
275
|
+
//# sourceMappingURL=PageSettings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/PageSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useState, useTransition } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { getPageSettings, revertField, savePageSettings } from \"../server/actions\";\nimport { CloseIcon, RevertIcon } from \"./Icons\";\nimport { MediaPicker } from \"./MediaPicker\";\nimport { revertLabel } from \"./revert\";\nimport {\n SEO_LIMITS,\n normalizePagePath,\n seoKey,\n type GRPageImage,\n type GRPageSettings,\n type GRPageSettingsPatch,\n type SeoField,\n} from \"../core/seo\";\n\n/** The drawer's working copy. `noindex: null` means \"nothing saved — use the page's own default\". */\ninterface Form {\n title: string;\n description: string;\n image: GRPageImage;\n noindex: boolean | null;\n}\n\nconst toForm = (s: GRPageSettings): Form => ({\n title: s.title,\n description: s.description,\n image: s.image,\n noindex: s.saved.noindex ? s.noindex : null,\n});\n\n/**\n * What the page renders right now, read from the document itself. It is the\n * placeholder for anything not yet saved — the page's own defaults — and needs\n * no wiring between this drawer and the page's generateMetadata().\n */\nfunction readHead() {\n const meta = (selector: string) => document.querySelector<HTMLMetaElement>(selector)?.content ?? \"\";\n return {\n title: document.title,\n description: meta('meta[name=\"description\"]'),\n image: meta('meta[property=\"og:image\"]'),\n noindex: /noindex/i.test(meta('meta[name=\"robots\"]')),\n };\n}\n\n// Search results show roughly this much before truncating. Guidance only.\nconst TITLE_HINT = 60;\nconst DESCRIPTION_HINT = 160;\n\n/**\n * Per-page title, description, social image and \"hide from search engines\",\n * saved as ordinary content rows (so they get history and revert like\n * everything else) and applied by guardrailMetadata() in the page.\n */\nexport function PageSettingsDrawer({ onClose }: { onClose: () => void }) {\n const pathname = usePathname();\n const path = normalizePagePath(pathname) ?? \"/\";\n\n const [settings, setSettings] = useState<GRPageSettings | null>(null);\n const [form, setForm] = useState<Form | null>(null);\n const [head, setHead] = useState(readHead);\n const [error, setError] = useState<string | null>(null);\n const [notice, setNotice] = useState<string | null>(null);\n const [pickerOpen, setPickerOpen] = useState(false);\n const [pending, startTransition] = useTransition();\n\n const load = useCallback(async () => {\n const res = await getPageSettings(path);\n if (!res.success) {\n setError(res.error);\n return;\n }\n setSettings(res.settings);\n setForm(toForm(res.settings));\n }, [path]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n // Saving or reverting re-renders the page's head; keep the placeholders current.\n useEffect(() => {\n const observer = new MutationObserver(() => setHead(readHead()));\n observer.observe(document.head, { childList: true, subtree: true, characterData: true, attributes: true });\n return () => observer.disconnect();\n }, []);\n\n // The same close behavior as the Team drawer: stay mounted to play the exit.\n const [closing, setClosing] = useState(false);\n const requestClose = useCallback(() => {\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return onClose();\n setClosing(true);\n }, [onClose]);\n\n useEffect(() => {\n if (!closing) return;\n const t = window.setTimeout(onClose, 400);\n return () => window.clearTimeout(t);\n }, [closing, onClose]);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && !pickerOpen && requestClose();\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [requestClose, pickerOpen]);\n\n const initial = settings ? toForm(settings) : null;\n const dirty = {\n title: !!form && !!initial && form.title !== initial.title,\n description: !!form && !!initial && form.description !== initial.description,\n image: !!form && !!initial && (form.image.src !== initial.image.src || form.image.alt !== initial.image.alt),\n noindex: !!form && !!initial && form.noindex !== initial.noindex,\n };\n const anyDirty = Object.values(dirty).some(Boolean);\n\n const save = () =>\n startTransition(async () => {\n if (!form) return;\n setError(null);\n setNotice(null);\n const patch: GRPageSettingsPatch = {};\n if (dirty.title) patch.title = form.title;\n if (dirty.description) patch.description = form.description;\n if (dirty.image) patch.image = form.image.src ? form.image : null;\n if (dirty.noindex && form.noindex !== null) patch.noindex = form.noindex;\n\n const res = await savePageSettings(path, patch);\n if (!res.success) setError(res.error ?? \"Couldn't save that.\");\n else setNotice(\"Saved.\");\n await load();\n });\n\n const revert = (field: SeoField) =>\n startTransition(async () => {\n setError(null);\n setNotice(null);\n const res = await revertField(seoKey(path, field));\n if (!res.success) setError(res.error ?? \"Couldn't revert that.\");\n else setNotice(res.action === \"restored\" ? \"Reverted to the previous version.\" : \"Back to the page's default.\");\n await load();\n });\n\n const revertButton = (field: SeoField) => {\n const info = settings?.revert[field];\n if (!info || info.kind === \"none\") return null;\n return (\n <button\n type=\"button\"\n className=\"gr-icon-btn gr-text-muted gr-size-22\"\n onClick={() => revert(field)}\n disabled={pending}\n title={revertLabel(info)}\n aria-label={revertLabel(info)}\n >\n <RevertIcon />\n </button>\n );\n };\n\n const counter = (length: number, hint: number) => (\n <p className={`gr-counter${length > hint ? \" gr-text-danger\" : \"\"}`}>\n {length} characters · search results show about {hint}\n </p>\n );\n\n const noindexChecked = form ? (form.noindex ?? head.noindex) : false;\n\n // The page's tags show the *saved* value once there is one, so they only stand\n // in for \"the default\" when nothing is saved. With a saved value, clearing the\n // field falls back to a default we can't see from here.\n const savedImage = !!settings?.saved.image && !!settings.image.src;\n const placeholder = (field: \"title\" | \"description\", fromHead: string) =>\n settings?.saved[field] && settings[field] ? `Cleared — the page's default ${field} is used` : fromHead;\n const shownImage = form?.image.src || (savedImage ? \"\" : head.image);\n\n return (\n <>\n <div\n data-guardrail\n data-guardrail-scrim\n {...(closing ? { \"data-closing\": \"\" } : {})}\n className=\"gr-scrim\"\n onClick={requestClose}\n />\n <aside\n data-guardrail\n data-guardrail-team\n {...(closing ? { \"data-closing\": \"\" } : {})}\n onAnimationEnd={(e) => {\n if (closing && e.target === e.currentTarget) onClose();\n }}\n className=\"gr-drawer\"\n role=\"dialog\"\n aria-label=\"Page settings\"\n >\n <header className=\"gr-drawer-head\">\n <h2 className=\"gr-drawer-title\">Page settings</h2>\n <button className=\"gr-icon-btn gr-text-muted\" onClick={requestClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n </header>\n\n <div className=\"gr-drawer-body\">\n <p className=\"gr-fs-12 gr-text-muted gr-mb-12\">\n How <strong>{path}</strong> appears in search results and when it's shared.\n </p>\n\n {error && <div className=\"gr-error gr-mb-12\">{error}</div>}\n {notice && (\n <div className=\"gr-fs-12 gr-text-muted gr-mb-12\" role=\"status\">\n {notice}\n </div>\n )}\n\n {!form ? (\n !error && <p className=\"gr-fs-13 gr-text-muted\">Loading…</p>\n ) : (\n <>\n <div className=\"gr-meta-row\">\n <label className=\"gr-label\" htmlFor=\"gr-meta-title\">Title</label>\n {revertButton(\"title\")}\n </div>\n <input\n id=\"gr-meta-title\"\n className=\"gr-field\"\n value={form.title}\n placeholder={placeholder(\"title\", head.title)}\n maxLength={SEO_LIMITS.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n />\n {counter(form.title.length || head.title.length, TITLE_HINT)}\n\n <div className=\"gr-meta-row\">\n <label className=\"gr-label\" htmlFor=\"gr-meta-description\">Description</label>\n {revertButton(\"description\")}\n </div>\n <textarea\n id=\"gr-meta-description\"\n className=\"gr-field gr-field-area\"\n value={form.description}\n placeholder={placeholder(\"description\", head.description)}\n maxLength={SEO_LIMITS.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n />\n {counter(form.description.length || head.description.length, DESCRIPTION_HINT)}\n\n <div className=\"gr-meta-row\">\n <span className=\"gr-label\">Social image</span>\n {revertButton(\"image\")}\n </div>\n {shownImage ? (\n // Plain img: the admin UI mustn't depend on next/image's remotePatterns.\n <img className=\"gr-meta-thumb\" src={shownImage} alt=\"\" />\n ) : (\n !savedImage && <p className=\"gr-fs-12 gr-text-muted gr-mb-8\">No image set.</p>\n )}\n {!form.image.src && savedImage && (\n <p className=\"gr-counter\">The saved image will be removed; the page's default is used.</p>\n )}\n {!form.image.src && !savedImage && head.image && (\n <p className=\"gr-counter\">The page's default image.</p>\n )}\n <div className=\"gr-flex gr-gap-8 gr-mb-8\">\n <button type=\"button\" className=\"gr-secondary-btn gr-w-auto gr-mb-0\" onClick={() => setPickerOpen(true)}>\n Choose image\n </button>\n {form.image.src && (\n <button\n type=\"button\"\n className=\"gr-text-btn\"\n onClick={() => setForm({ ...form, image: { src: \"\", alt: \"\" } })}\n >\n Remove\n </button>\n )}\n </div>\n {form.image.src && (\n <input\n className=\"gr-field\"\n value={form.image.alt}\n placeholder=\"Describe the image\"\n aria-label=\"Image description\"\n onChange={(e) => setForm({ ...form, image: { ...form.image, alt: e.target.value } })}\n />\n )}\n\n <div className=\"gr-meta-row gr-mt-8\">\n <label className=\"gr-check\">\n <input\n type=\"checkbox\"\n checked={noindexChecked}\n onChange={(e) => setForm({ ...form, noindex: e.target.checked })}\n />\n <span>Hide this page from search engines</span>\n </label>\n {revertButton(\"noindex\")}\n </div>\n\n <button\n type=\"button\"\n className=\"gr-primary-btn gr-w-full gr-mt-16\"\n onClick={save}\n disabled={pending || !anyDirty}\n >\n {pending ? \"Saving…\" : \"Save\"}\n </button>\n <p className=\"gr-fs-11 gr-text-subtle gr-mt-10\">\n Applies to pages whose metadata comes from guardrailMetadata().\n </p>\n </>\n )}\n </div>\n </aside>\n\n {pickerOpen && form && (\n <MediaPicker\n current={form.image.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n setForm({\n ...form,\n image: { src: image.src, alt: form.image.alt, width: image.width, height: image.height },\n })\n }\n />\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/react/guardrail.css
CHANGED
|
@@ -529,6 +529,14 @@
|
|
|
529
529
|
white-space: nowrap;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
/* Page settings drawer */
|
|
533
|
+
.gr-meta-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 22px; }
|
|
534
|
+
.gr-field-area { min-height: 76px; resize: vertical; font-family: inherit; line-height: 1.4; }
|
|
535
|
+
.gr-counter { font-size: 11px; color: var(--gr-subtle); margin: -2px 0 14px; }
|
|
536
|
+
.gr-meta-thumb { display: block; max-width: 100%; max-height: 120px; border-radius: 6px; border: 1px solid var(--gr-overlay-border); margin-bottom: 8px; }
|
|
537
|
+
.gr-check { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; color: var(--gr-overlay-fg); cursor: pointer; }
|
|
538
|
+
.gr-check input { margin-top: 2px; }
|
|
539
|
+
|
|
532
540
|
/* ---- editable fields ------------------------------------------------------
|
|
533
541
|
* Drawn on the developer's own elements by <GRail>, which marks them with a
|
|
534
542
|
* data attribute instead of a style attribute. Images get a separate marker. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { GRRevertInfo } from '../core/adapter-types.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
import '../core/types.js';
|
|
4
|
+
|
|
5
|
+
/** "Revert to the version from Oct 28" / "Reset to default" — what a revert button will actually do. */
|
|
6
|
+
declare function revertLabel(info: GRRevertInfo): string;
|
|
7
|
+
|
|
8
|
+
export { revertLabel };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function revertLabel(info) {
|
|
2
|
+
if (info.kind === "previous" && info.at) {
|
|
3
|
+
const when = new Date(info.at);
|
|
4
|
+
if (!Number.isNaN(when.getTime())) {
|
|
5
|
+
return `Revert to the version from ${when.toLocaleDateString(void 0, { month: "short", day: "numeric" })}`;
|
|
6
|
+
}
|
|
7
|
+
return "Revert to the previous version";
|
|
8
|
+
}
|
|
9
|
+
return "Reset to default";
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
revertLabel
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=revert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/revert.ts"],"sourcesContent":["import type { GRRevertInfo } from \"../core/adapter-types\";\n\n/** \"Revert to the version from Oct 28\" / \"Reset to default\" — what a revert button will actually do. */\nexport function 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"],"mappings":"AAGO,SAAS,YAAY,MAA4B;AACtD,MAAI,KAAK,SAAS,cAAc,KAAK,IAAI;AACvC,UAAM,OAAO,IAAI,KAAK,KAAK,EAAE;AAC7B,QAAI,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG;AACjC,aAAO,8BAA8B,KAAK,mBAAmB,QAAW,EAAE,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC;AAAA,IAC7G;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
|