// Admin / edit-mode primitives
// Exposes:
// window.useCMS() — subscribes to CMS, returns [state, setKey, update]
// window.useEditMode() — [editing, setEditing]
// window.EditableText — contentEditable inline text bound to CMS
// window.EditableUrl — text + url pair editor (modal popover)
// window.AdminBar — floating top toolbar (when admin)
// window.AdminAddButton — "+ add" tile
// window.AdminItemControls — hover overlay (delete / move)
const { useState, useEffect, useRef, useCallback } = React;
// ============ HOOKS ============
window.useCMS = function () {
const [state, setState] = useState(window.CMS.get());
useEffect(() => {
const unsub = window.CMS.subscribe((s) => setState({ ...s }));
return unsub;
}, []);
const setKey = useCallback((k, v) => window.CMS.set(k, v), []);
const update = useCallback((fn) => window.CMS.update(fn), []);
return [state, setKey, update];
};
window.useEditMode = function () {
const [editing, setEditing] = useState(!!window.__EXEC_EDIT_MODE);
useEffect(() => {
document.body.classList.toggle('edit-mode', editing);
window.__EXEC_EDIT_MODE = editing;
}, [editing]);
return [editing, setEditing];
};
// ============ INLINE EDITABLE TEXT ============
// Live contentEditable. Renders read-only when `editing` is false.
// Saves on blur. Render the current value as children of the contentEditable
// element so initial paint is correct. We then *avoid* re-rendering on every
// keystroke by remembering the last value we committed: if the DOM already
// matches the value prop, we don't touch it. This keeps the caret stable
// while the user types but still picks up external updates (resets etc).
window.EditableText = React.memo(function EditableText({
value, onChange, editing, as = 'span', placeholder = '…',
multiline = false, className = '', style = {}, maxLength,
}) {
const ref = useRef(null);
const Tag = as;
// After mount / value change, only set DOM text if it differs from prop.
// This catches "reset" and "edit elsewhere" updates while leaving caret
// alone during normal typing.
useEffect(() => {
if (!ref.current) return;
const next = value == null ? '' : String(value);
if (ref.current.innerText !== next && document.activeElement !== ref.current) {
ref.current.innerText = next;
}
});
if (!editing) {
return {value || placeholder};
}
const commit = () => {
let next = ref.current.innerText || '';
if (maxLength && next.length > maxLength) next = next.slice(0, maxLength);
if (next !== value) onChange(next);
};
const onKey = (e) => {
if (!multiline && e.key === 'Enter') { e.preventDefault(); ref.current.blur(); }
if (e.key === 'Escape') { ref.current.blur(); }
};
return (
e.stopPropagation()}
data-placeholder={placeholder}
>
{value}
);
}, (prev, next) => {
// Re-render when editing flips or value changes (the useEffect above
// decides whether to actually touch the DOM).
return prev.value === next.value && prev.editing === next.editing && prev.placeholder === next.placeholder;
});
// ============ EDITABLE LINK (label + url) ============
window.EditableUrl = function EditableUrl({
label, url, onChange, editing, className = '',
}) {
const [open, setOpen] = useState(false);
const [draftLabel, setDraftLabel] = useState(label);
const [draftUrl, setDraftUrl] = useState(url);
useEffect(() => { setDraftLabel(label); setDraftUrl(url); }, [label, url, open]);
if (!editing) {
return {label};
}
const save = () => {
onChange({ label: draftLabel, url: draftUrl });
setOpen(false);
};
return (
<>
{open && (
)}
>
);
};
// ============ ADD BUTTON / ADD TILE ============
window.AdminAddButton = function AdminAddButton({ onClick, label = 'ADD', className = '', style = {} }) {
return (
);
};
// ============ ITEM CONTROLS (delete, up/down) ============
window.AdminItemControls = function AdminItemControls({ onDelete, onUp, onDown, className = '' }) {
return (
e.stopPropagation()}>
{onUp &&
}
{onDown &&
}
{onDelete && (
)}
);
};
// ============ ADMIN BAR (floating, top) ============
window.AdminBar = function AdminBar({ editing, setEditing, extraActions }) {
const isAdmin = window.CMS.isAdmin();
if (!isAdmin) return null;
const logout = () => {
if (!confirm('Sign out of admin and exit edit mode?')) return;
window.CMS.logout();
setEditing(false);
window.location.reload();
};
const reset = () => {
if (!confirm('Reset ALL content to defaults? This cannot be undone.')) return;
window.CMS.reset();
};
return (
ADMIN //
{editing ? 'EDITING' : 'PREVIEW'}
{extraActions}
);
};
// ============ STATUS PICKER (small popover for edit-status) ============
window.StatusPicker = function StatusPicker({ value, onChange, options = ['online','offline','updating'] }) {
const [open, setOpen] = useState(false);
return (
e.stopPropagation()}>
{open && (
{options.map((opt) => (
))}
)}
);
};