// 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 && (
setOpen(false)}>
e.stopPropagation()}>
EDIT LINK
)} ); }; // ============ 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) => ( ))}
)}
); };