/* Sprig — main app. */
const { useState, useRef, useMemo, useEffect, useCallback } = React;

function makeTheme(dark, accent) {
  return dark ? {
    dark: true, accent,
    bg: '#1f242e', panel: '#2a313d', barBg: 'rgba(31,36,46,.82)',
    nodeBg: '#2d3543', nodeBorder: '#3a4250', text: '#e8ebf0', muted: '#98a0ad',
    line: '#353d4a', hover: '#333b48', chipBg: '#333b48', dot: '#39414e', edge: '#3a4250',
    good: '#3FB680', bad: '#F0688A', shadow: '0 16px 50px rgba(0,0,0,.45)',
  } : {
    dark: false, accent,
    bg: '#f6f7f9', panel: '#ffffff', barBg: 'rgba(255,255,255,.82)',
    nodeBg: '#ffffff', nodeBorder: '#e3e6ec', text: '#1b1f27', muted: '#727885',
    line: '#e7e9ef', hover: '#eef0f3', chipBg: '#eef0f4', dot: '#d6dae2', edge: '#dde0e6',
    good: '#1F8A5B', bad: '#E5557A', shadow: '0 16px 50px rgba(20,25,40,.13)',
  };
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#2A6FDB",
  "layout": "right",
  "connector": "bracket",
  "connectorColor": "neutral",
  "connectorWidth": 2,
  "background": "plain",
  "fontSize": 15,
  "density": "regular",
  "fontFamily": "'Hanken Grotesk', sans-serif",
  "monoFamily": "'JetBrains Mono', monospace",
  "dark": true,
  "titleAutocomplete": true
}/*EDITMODE-END*/;

const DENSITY = { compact: { gapMain: 44, gapCross: 9 }, regular: { gapMain: 64, gapCross: 16 }, comfy: { gapMain: 92, gapCross: 26 } };
const TODAY = '2026-05-31';
// Inspector panel width (268) + its right/left margins — kept clear when centering
// a node that's about to be edited, so it doesn't land behind the panel.
const INSPECTOR_PAD = 300;

// Find the "Day Goals"-typed node nearest to `node` by tree distance.
// Excludes the node itself and its descendants (so we never create a cycle).
function findNearestDayGoals(node, ns, byId) {
  const kids = {}; ns.forEach(n => { (kids[n.parentId] = kids[n.parentId] || []).push(n.id); });
  const desc = new Set([node.id]); const stack = [node.id];
  while (stack.length) { const x = stack.pop(); (kids[x] || []).forEach(k => { desc.add(k); stack.push(k); }); }

  const goals = ns.filter(n => n.nodeType === 'daygoals' && !desc.has(n.id));
  if (!goals.length) return null;

  // ancestors of `node` (including itself), mapped to their distance from node
  const anc = new Map([[node.id, 0]]);
  let c = byId[node.parentId], d = 1;
  while (c) { anc.set(c.id, d++); c = byId[c.parentId]; }

  let best = null, bestScore = Infinity;
  for (const g of goals) {
    let cc = g, gd = 0, lca = null;
    while (cc) { if (anc.has(cc.id)) { lca = cc.id; break; } cc = byId[cc.parentId]; gd++; }
    if (lca == null) continue;
    const score = anc.get(lca) + gd; // tree-path length between node and g
    if (score < bestScore) { bestScore = score; best = g; }
  }
  return best;
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [nodes, setNodes, nodesHistory] = window.SPRIG_HISTORY.useHistoryState(() => window.SPRIG.seed.nodes.map(n => ({ ...n })));
  const rootId = window.SPRIG.seed.rootId;
  const [title, setTitle] = useState('My Week · Planning Map');
  const [selectedId, setSelectedId] = useState(null);
  const [editingId, setEditingId] = useState(null);
  const [view, setView] = useState({ scale: 1, tx: 0, ty: 0 });
  const [agendaOpen, setAgendaOpen] = useState(false);
  const [searchOpen, setSearchOpen] = useState(false);
  const [present, setPresent] = useState(null); // null or index into walk
  const [previewImage, setPreviewImage] = useState(null); // full-size image URL or null
  const [clipboard, setClipboard] = useState(null); // { rootId, snapshot } or null
  const [fitToken, setFitToken] = useState(0);
  const [saveStatus, setSaveStatus] = useState('idle'); // 'idle' | 'saving' | 'saved' | 'error'
  const [authState, setAuthState] = useState('checking'); // 'checking' | 'anon' | 'authed'
  const apiRef = useRef({});
  const isLoadedRef = useRef(false);
  const saveTimerRef = useRef(null);

  // Check for an existing login session on mount, before touching any mindmap data.
  useEffect(() => {
    window.SprigAuth.checkAuth().then(ok => setAuthState(ok ? 'authed' : 'anon'));
  }, []);

  function handleLogout() {
    fetch('/api/auth/logout', { method: 'POST' }).finally(() => {
      isLoadedRef.current = false;
      setAuthState('anon');
    });
  }

  // Load from MongoDB once logged in
  useEffect(() => {
    if (authState !== 'authed') return;
    fetch('/api/mindmap')
      .then(r => { if (r.status === 401) { setAuthState('anon'); return null; } return r.json(); })
      .then(data => {
        if (!data) return;
        if (data.nodes && data.nodes.length) {
          nodesHistory.resetTo(data.nodes);
          const maxId = data.nodes.reduce((max, n) => {
            const num = parseInt((n.id || '').replace('n', ''), 10);
            return isNaN(num) ? max : Math.max(max, num);
          }, 0);
          window.SPRIG.syncId(maxId);
        }
        if (data.title) setTitle(data.title);
        if (data.tweaks) setTweak(data.tweaks);
        isLoadedRef.current = true;
      })
      .catch(() => { isLoadedRef.current = true; });
  }, [authState]);

  // Auto-save to MongoDB (debounced 1s after last change)
  useEffect(() => {
    if (authState !== 'authed' || !isLoadedRef.current) return;
    setSaveStatus('saving');
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(() => {
      fetch('/api/mindmap', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title, tweaks: t, nodes }),
      })
        .then(r => { if (r.status === 401) { setAuthState('anon'); throw new Error('unauthorized'); } return r.json(); })
        .then(() => { setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 2000); })
        .catch(() => setSaveStatus('error'));
    }, 1000);
  }, [nodes, t, title, authState]);

  function exportJSON() {
    const data = { version: 1, title, tweaks: t, nodes };
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (title || 'mindmap').replace(/[^a-z0-9]/gi, '_') + '.json';
    a.click();
    URL.revokeObjectURL(url);
  }

  function importJSON() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json,application/json';
    input.onchange = (e) => {
      const file = e.target.files[0]; if (!file) return;
      const reader = new FileReader();
      reader.onload = (ev) => {
        try {
          const data = JSON.parse(ev.target.result);
          if (data.nodes) nodesHistory.resetTo(data.nodes);
          if (data.title) setTitle(data.title);
          if (data.tweaks) setTweak(data.tweaks);
        } catch { alert('Invalid JSON file'); }
      };
      reader.readAsText(file);
    };
    input.click();
  }

  const dark = t.dark;
  const theme = useMemo(() => makeTheme(dark, t.accent), [dark, t.accent]);

  // layout (shared with canvas + present + agenda focus)
  const dens = DENSITY[t.density] || DENSITY.regular;
  const tw = { ...t, gapMain: dens.gapMain, gapCross: dens.gapCross, _today: TODAY };
  const L = useMemo(() => window.SPRIG_LAYOUT.layout(nodes, rootId, {
    type: t.layout, fontSize: t.fontSize, fontFamily: t.fontFamily,
    gapMain: dens.gapMain, gapCross: dens.gapCross,
  }), [nodes, rootId, t.layout, t.fontSize, t.fontFamily, dens.gapMain, dens.gapCross]);

  const byId = useMemo(() => { const m = {}; nodes.forEach(n => m[n.id] = n); return m; }, [nodes]);
  const kidsOf = useCallback((id) => nodes.filter(n => n.parentId === id), [nodes]);
  const selected = byId[selectedId];

  // Undo/redo (or any other node removal) can leave selectedId/editingId
  // pointing at a node that no longer exists.
  useEffect(() => {
    if (selectedId && !byId[selectedId]) setSelectedId(null);
    if (editingId && !byId[editingId]) setEditingId(null);
  }, [byId, selectedId, editingId]);

  // Focus mode: whenever a node is selected, keep its full branch prominent —
  // the path back to the root, the node itself, and everything below it — and
  // dim the rest of the map. Null (no dimming) when nothing is selected.
  const focusSet = useMemo(() => {
    const node = byId[selectedId]; if (!node) return null;
    const kids = {}; nodes.forEach(n => { (kids[n.parentId] = kids[n.parentId] || []).push(n.id); });
    const set = new Set([node.id]);
    let cur = node;
    while (cur && cur.parentId) { set.add(cur.parentId); cur = byId[cur.parentId]; }
    const stack = [node.id];
    while (stack.length) { const x = stack.pop(); (kids[x] || []).forEach(k => { set.add(k); stack.push(k); }); }
    return set;
  }, [selectedId, nodes, byId]);

  // ---- operations ----
  const updateNode = useCallback((id, patch) => setNodes(ns => ns.map(n => n.id === id ? { ...n, ...patch } : n)), []);
  const commit = useCallback((id, text) => { setNodes(ns => ns.map(n => n.id === id ? { ...n, text: text.trim() || 'Untitled' } : n)); setEditingId(null); }, []);
  const toggleCollapse = useCallback((id) => setNodes(ns => ns.map(n => n.id === id ? { ...n, collapsed: !n.collapsed } : n)), []);
  const toggleDone = useCallback((id) => setNodes(ns => {
    const node = ns.find(n => n.id === id); if (!node) return ns;
    const done = !node.done;
    // keep a day-goal source and its linked copy in sync on the tickbox
    const partnerId = node.isDayGoalClone ? node.dayGoalSourceId : node.dayGoalCloneId;
    return ns.map(n => (n.id === id || (partnerId && n.id === partnerId)) ? { ...n, done, task: true } : n);
  }), []);

  const addChild = useCallback((parentId) => {
    const id = window.SPRIG.uid();
    setNodes(ns => {
      const p = ns.find(n => n.id === parentId);
      const node = { id, parentId, text: 'New idea', color: 'neutral', shape: 'underline', icon: null,
        collapsed: false, side: 'auto', note: '', link: '', task: false, done: false, due: null, badge: null,
        nodeType: null, image: null, isDayGoal: false, isDayGoalClone: false, dayGoalSourceId: null, dayGoalCloneId: null };
      // If the parent's type forces a child type, apply that type's presets.
      const types = window.SPRIG.NODE_TYPES || [];
      const ptype = p && types.find(tp => tp.id === p.nodeType);
      if (ptype && ptype.childType) {
        const ct = types.find(tp => tp.id === ptype.childType);
        if (ct) { node.nodeType = ct.id; if (ct.icon) node.icon = ct.icon; node.color = ct.color; node.shape = ct.shape; if (ct.task) node.task = true; }
      }
      const np = p && p.collapsed ? ns.map(n => n.id === parentId ? { ...n, collapsed: false } : n) : ns;
      return [...np, node];
    });
    setSelectedId(id); setEditingId(id);
    // Wait a tick for the layout to include the new node before centering on it.
    setTimeout(() => apiRef.current.center && apiRef.current.center(id, Math.max(view.scale, 0.9), INSPECTOR_PAD), 40);
    return id;
  }, [view.scale]);
  const addSibling = useCallback((id) => { const n = byId[id]; if (!n || !n.parentId) return addChild(id); return addChild(n.parentId); }, [byId, addChild]);
  const deleteNode = useCallback((id) => {
    if (id === rootId) return;
    const set = new Set(); const stack = [id];
    while (stack.length) { const x = stack.pop(); set.add(x); nodes.forEach(n => { if (n.parentId === x) stack.push(n.id); }); }
    const parent = byId[id] && byId[id].parentId;
    setNodes(ns => ns.filter(n => !set.has(n.id)));
    setSelectedId(parent || rootId); setEditingId(null);
  }, [nodes, byId, rootId]);
  // Nesting into a collapsed parent would hide the node the user just dropped, so
  // the new parent is expanded as part of the move.
  const reparent = useCallback((id, newParent) => setNodes(ns => ns.map(n =>
    n.id === id ? { ...n, parentId: newParent } :
    n.id === newParent && n.collapsed ? { ...n, collapsed: false } : n)), []);

  // Copy a node + its whole subtree (children and their data) onto an in-memory
  // clipboard. Paste re-instantiates it with fresh ids under the target node, so
  // the same clipboard can be pasted repeatedly into different nodes.
  const copyNode = useCallback((id) => {
    if (!byId[id]) return;
    setClipboard({ rootId: id, snapshot: window.SPRIG.snapshotSubtree(nodes, id) });
  }, [nodes, byId]);
  const pasteNode = useCallback((targetId) => {
    if (!clipboard || !byId[targetId]) return;
    const { nodes: fresh, rootId: newId } = window.SPRIG.instantiateSubtree(clipboard.snapshot, clipboard.rootId, targetId);
    setNodes(ns => {
      const np = byId[targetId].collapsed ? ns.map(n => n.id === targetId ? { ...n, collapsed: false } : n) : ns;
      return [...np, ...fresh];
    });
    setSelectedId(newId); setEditingId(null);
  }, [clipboard, byId]);

  // Mark/unmark a node as a "day goal". The node stays in place; marking adds a
  // linked duplicate under the nearest Day Goals node. Both the original and the
  // duplicate show as marked, and unmarking either removes the duplicate.
  const toggleDayGoal = useCallback((id) => {
    setNodes(ns => {
      const byId = {}; ns.forEach(n => byId[n.id] = n);
      const node = byId[id]; if (!node) return ns;

      // Toggling the duplicate off -> remove it and unmark its source.
      if (node.isDayGoalClone) {
        const srcId = node.dayGoalSourceId;
        return ns.filter(n => n.id !== id)
          .map(n => n.id === srcId ? { ...n, isDayGoal: false, dayGoalCloneId: null } : n);
      }
      // Toggling a marked source off -> remove its duplicate and unmark.
      if (node.isDayGoal) {
        const cloneId = node.dayGoalCloneId;
        return ns.filter(n => n.id !== cloneId)
          .map(n => n.id === id ? { ...n, isDayGoal: false, dayGoalCloneId: null } : n);
      }
      // Marking a source -> create a linked duplicate under the nearest Day Goals node.
      const target = findNearestDayGoals(node, ns, byId);
      if (!target) return ns; // no Day Goals node to attach to
      const cloneId = window.SPRIG.uid();
      const clone = { ...node, id: cloneId, parentId: target.id, collapsed: false, task: true,
        isDayGoal: false, isDayGoalClone: true, dayGoalSourceId: node.id, dayGoalCloneId: null };
      const updated = ns.map(n => n.id === id ? { ...n, isDayGoal: true, task: true, dayGoalCloneId: cloneId } : n);
      return [...updated, clone];
    });
  }, []);
  const reorderSibling = useCallback((id, targetId, insertPos) => {
    setNodes(ns => {
      const dragged = ns.find(n => n.id === id); if (!dragged) return ns;
      const without = ns.filter(n => n.id !== id);
      const targetIdx = without.findIndex(n => n.id === targetId); if (targetIdx === -1) return ns;
      const at = insertPos === 'before' ? targetIdx : targetIdx + 1;
      return [...without.slice(0, at), dragged, ...without.slice(at)];
    });
  }, []);

  const collapseAll = useCallback(() => { const parents = new Set(nodes.filter(n => n.parentId).map(n => n.parentId));
    setNodes(ns => ns.map(n => parents.has(n.id) && n.id !== rootId ? { ...n, collapsed: true } : n)); }, [nodes, rootId]);
  const expandAll = useCallback(() => setNodes(ns => ns.map(n => n.collapsed ? { ...n, collapsed: false } : n)), []);

  // Fold every branch except the path down to today's date node — the node
  // whose title mentions today (e.g. "22nd July 2026") — then select + center
  // on it. Today's node and everything under it are left exactly as they
  // were; only its ancestors are forced open (so the path to it is visible)
  // and unrelated branches elsewhere fold shut.
  const focusToday = useCallback(() => {
    const target = window.SPRIG.findTodayNode(nodes);
    if (!target) { alert("No node found mentioning today's date"); return; }
    const anc = new Set(); let cur = byId[target.parentId];
    while (cur) { anc.add(cur.id); cur = byId[cur.parentId]; }
    const desc = new Set(); const stack = [target.id];
    while (stack.length) { const x = stack.pop(); nodes.forEach(n => { if (n.parentId === x) { desc.add(n.id); stack.push(n.id); } }); }
    const parents = new Set(nodes.filter(n => n.parentId).map(n => n.parentId));
    setNodes(ns => ns.map(n => {
      if (n.id === rootId || !parents.has(n.id)) return n;
      if (n.id === target.id || desc.has(n.id)) return n; // leave today's subtree untouched
      const collapsed = !anc.has(n.id);
      return n.collapsed === collapsed ? n : { ...n, collapsed };
    }));
    setSelectedId(target.id);
    setTimeout(() => apiRef.current.center && apiRef.current.center(target.id, Math.max(view.scale, 0.9), INSPECTOR_PAD), 40);
  }, [nodes, byId, rootId, view.scale]);

  const fit = useCallback(() => setFitToken(x => x + 1), []);

  // Generate a type's child template under a node, but only if it has no children yet.
  const applyTemplate = useCallback((parentId, template) => {
    if (template === 'day') {
      setNodes(ns => {
        if (ns.some(n => n.parentId === parentId)) return ns; // only when empty
        return [...ns, ...window.SPRIG.buildDayTemplate(parentId)];
      });
    }
  }, []);

  // reveal a node: expand all collapsed ancestors, select + center on it
  const revealNode = useCallback((id) => {
    const anc = new Set(); let cur = byId[id];
    while (cur && cur.parentId) { anc.add(cur.parentId); cur = byId[cur.parentId]; }
    setNodes(ns => ns.map(n => anc.has(n.id) && n.collapsed ? { ...n, collapsed: false } : n));
    setSelectedId(id);
    setTimeout(() => apiRef.current.center && apiRef.current.center(id, Math.max(view.scale, 0.9)), 40);
  }, [byId, view.scale]);

  // ---- keyboard ----
  useEffect(() => {
    const onKey = (e) => {
      // global: open search palette (works even while typing in a field)
      if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
        e.preventDefault(); setSearchOpen(o => !o); return;
      }
      if ((e.metaKey || e.ctrlKey) && (e.key === 't' || e.key === 'T')) {
        e.preventDefault(); focusToday(); return;
      }
      if (editingId) return;
      const tag = (e.target.tagName || '').toLowerCase();
      if (tag === 'input' || tag === 'textarea') return;
      if (e.key === '/' && !searchOpen) { e.preventDefault(); setSearchOpen(true); return; }
      if (present !== null) {
        if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); stepPresent(1); }
        if (e.key === 'ArrowLeft') { e.preventDefault(); stepPresent(-1); }
        if (e.key === 'Escape') setPresent(null);
        return;
      }
      const sel = selectedId || rootId;
      if (!selectedId) setSelectedId(sel);
      if (e.key === 'Tab') { e.preventDefault(); addChild(sel); }
      else if (e.key === 'Enter') { e.preventDefault(); if (sel === rootId) setEditingId(sel); else addSibling(sel); }
      else if (e.key === 'F2') { e.preventDefault(); setEditingId(sel); }
      else if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); deleteNode(sel); }
      else if (e.key === ' ') { e.preventDefault(); const k = kidsOf(sel); if (k.length) toggleCollapse(sel); }
      else if ((e.metaKey || e.ctrlKey) && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); copyNode(sel); }
      else if ((e.metaKey || e.ctrlKey) && (e.key === 'v' || e.key === 'V')) { e.preventDefault(); pasteNode(sel); }
      else if ((e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'Z')) {
        e.preventDefault();
        if (e.shiftKey) nodesHistory.redo(); else nodesHistory.undo();
      }
      else if ((e.metaKey || e.ctrlKey) && (e.key === 'y' || e.key === 'Y')) { e.preventDefault(); nodesHistory.redo(); }
      else if (e.key.startsWith('Arrow')) { e.preventDefault(); navigate(e.key); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  });

  function navigate(key) {
    const n = byId[selectedId] || byId[rootId]; if (!n) return;
    const sibs = n.parentId ? kidsOf(n.parentId) : [n];
    const idx = sibs.findIndex(s => s.id === n.id);
    const down = key === 'ArrowDown', up = key === 'ArrowUp';
    const intoChild = (t.layout === 'down') ? down : key === 'ArrowRight';
    const toParent = (t.layout === 'down') ? up : key === 'ArrowLeft';
    const nextSib = (t.layout === 'down') ? key === 'ArrowRight' : down;
    const prevSib = (t.layout === 'down') ? key === 'ArrowLeft' : up;
    let target = null;
    if (intoChild) { const k = kidsOf(n.id); if (n.collapsed && k.length) { toggleCollapse(n.id); return; } target = k[0]; }
    else if (toParent) target = byId[n.parentId];
    else if (nextSib) target = sibs[idx + 1];
    else if (prevSib) target = sibs[idx - 1];
    if (target) setSelectedId(target.id);
  }

  // ---- present mode ----
  const walk = useMemo(() => {
    const order = []; (function dfs(id) { const n = byId[id]; if (!n) return; order.push(id);
      if (!n.collapsed) kidsOf(id).forEach(c => dfs(c.id)); })(rootId); return order;
  }, [nodes, byId, rootId]);

  function startPresent() { setAgendaOpen(false); setPresent(0); setSelectedId(rootId);
    setTimeout(() => apiRef.current.center && apiRef.current.center(rootId, 1.15), 30); }
  function stepPresent(dir) {
    setPresent(p => { let np = Math.min(walk.length - 1, Math.max(0, p + dir)); const id = walk[np];
      setSelectedId(id); setTimeout(() => apiRef.current.center && apiRef.current.center(id, 1.15), 10); return np; });
  }

  const tasks = nodes.filter(n => n.task && n.due && !n.done).length;
  const presentNode = present !== null ? byId[walk[present]] : null;

  if (authState === 'checking') {
    return React.createElement('div', { style: { position: 'absolute', inset: 0, background: theme.bg } });
  }
  if (authState === 'anon') {
    return React.createElement(window.SprigAuth.LoginScreen, { onLoggedIn: () => setAuthState('authed') });
  }

  return React.createElement('div', { style: { position: 'absolute', inset: 0, background: theme.bg, color: theme.text,
    fontFamily: t.fontFamily, overflow: 'hidden' } },
    React.createElement(SprigCanvas, { nodes, rootId, L, theme, tw, selectedId, editingId, view, setView, focusSet,
      onSelect: setSelectedId, onCommit: commit, onReparent: reparent, onReorder: reorderSibling,
      onResize: (id, w) => updateNode(id, { width: w }),
      onToggleCollapse: toggleCollapse, onToggleDone: toggleDone, onStartEdit: setEditingId,
      onPreviewImage: setPreviewImage,
      fitToken, registerApi: (api) => { apiRef.current = api; } }),

    present === null && React.createElement(SprigToolbar, { theme, tw, setTweak, view, setView, onFit: fit,
      title, setTitle, dark, onToggleTheme: () => setTweak('dark', !dark),
      agendaOpen, setAgendaOpen, onPresent: startPresent, taskCount: tasks,
      onFoldAll: collapseAll, onUnfoldAll: expandAll, onFocusToday: focusToday, onOpenSearch: () => setSearchOpen(true),
      onExport: exportJSON, onImport: importJSON, saveStatus,
      onUndo: nodesHistory.undo, onRedo: nodesHistory.redo,
      canUndo: nodesHistory.canUndo, canRedo: nodesHistory.canRedo, onLogout: handleLogout }),

    present === null && searchOpen && React.createElement(SprigSearch, { nodes, byId, theme, tw,
      onClose: () => setSearchOpen(false),
      onPick: (id) => { setSearchOpen(false); revealNode(id); } }),

    present === null && agendaOpen && React.createElement(SprigAgenda, { nodes, theme, tw, today: TODAY,
      onClose: () => setAgendaOpen(false), onToggleDone: toggleDone,
      onPick: (id) => { setSelectedId(id); setTimeout(() => apiRef.current.center && apiRef.current.center(id, Math.max(view.scale, 0.9)), 10); } }),

    present === null && selected && React.createElement(SprigInspector, { node: selected, theme, tw,
      onChange: updateNode, onAddChild: addChild, onAddSibling: addSibling, onDelete: deleteNode,
      onToggleDayGoal: toggleDayGoal, onApplyTemplate: applyTemplate, onPreviewImage: setPreviewImage,
      onCopy: copyNode, onPaste: pasteNode, hasClipboard: !!clipboard }),

    previewImage && React.createElement(SprigLightbox, { url: previewImage, onClose: () => setPreviewImage(null) }),

    present === null && React.createElement(HelpHint, { theme, tw }),

    present !== null && React.createElement(PresentBar, { theme, tw, node: presentNode, index: present, total: walk.length,
      onPrev: () => stepPresent(-1), onNext: () => stepPresent(1), onExit: () => { setPresent(null); fit(); } }),

    React.createElement(TweaksUI, { t, setTweak })
  );
}

function TweaksUI({ t, setTweak }) {
  return React.createElement(TweaksPanel, null,
    React.createElement(TweakSection, { label: 'Theme' }),
    React.createElement(TweakColor, { label: 'Accent', value: t.accent,
      options: ['#2A6FDB', '#7C5CE6', '#0E9F8E', '#E8943A', '#E5557A', '#1F8A5B'],
      onChange: v => setTweak('accent', v) }),
    React.createElement(TweakToggle, { label: 'Dark mode', value: t.dark, onChange: v => setTweak('dark', v) }),
    React.createElement(TweakSection, { label: 'Layout' }),
    React.createElement(TweakSelect, { label: 'Layout', value: t.layout,
      options: [{ value: 'right', label: 'Tree — right' }, { value: 'both', label: 'Both sides' },
        { value: 'down', label: 'Org chart' }, { value: 'radial', label: 'Radial' }],
      onChange: v => setTweak('layout', v) }),
    React.createElement(TweakRadio, { label: 'Density', value: t.density, options: ['compact', 'regular', 'comfy'],
      onChange: v => setTweak('density', v) }),
    React.createElement(TweakSection, { label: 'Connectors' }),
    React.createElement(TweakRadio, { label: 'Style', value: t.connector, options: ['bracket', 'angled', 'organic', 'elbow', 'straight'],
      onChange: v => setTweak('connector', v) }),
    React.createElement(TweakRadio, { label: 'Color', value: t.connectorColor, options: [{ value: 'branch', label: 'Branch' }, { value: 'neutral', label: 'Neutral' }],
      onChange: v => setTweak('connectorColor', v) }),
    React.createElement(TweakSlider, { label: 'Thickness', value: t.connectorWidth, min: 1, max: 4, step: 0.5, unit: 'px',
      onChange: v => setTweak('connectorWidth', v) }),
    React.createElement(TweakSection, { label: 'Canvas' }),
    React.createElement(TweakRadio, { label: 'Background', value: t.background, options: ['dots', 'grid', 'plain'],
      onChange: v => setTweak('background', v) }),
    React.createElement(TweakSlider, { label: 'Text size', value: t.fontSize, min: 12, max: 20, unit: 'px',
      onChange: v => setTweak('fontSize', v) }),
    React.createElement(TweakSelect, { label: 'Font', value: t.fontFamily,
      options: [{ value: "'Hanken Grotesk', sans-serif", label: 'Hanken Grotesk' },
        { value: "'Schibsted Grotesk', sans-serif", label: 'Schibsted Grotesk' },
        { value: "'Spline Sans', sans-serif", label: 'Spline Sans' },
        { value: "Georgia, serif", label: 'Georgia' }],
      onChange: v => setTweak('fontFamily', v) }),
    React.createElement(TweakSection, { label: 'Editing' }),
    React.createElement(TweakToggle, { label: 'Title autocomplete', value: t.titleAutocomplete,
      onChange: v => setTweak('titleAutocomplete', v) })
  );
}

function HelpHint({ theme, tw }) {
  const [open, setOpen] = useState(false);
  const keys = [['Tab', 'Add child'], ['Enter', 'Add sibling'], ['F2', 'Edit'], ['Space', 'Collapse'], ['⌫', 'Delete'], ['⌘/Ctrl + C', 'Copy node'], ['⌘/Ctrl + V', 'Paste into node'], ['⌘/Ctrl + Z', 'Undo'], ['⌘/Ctrl + ⇧ + Z', 'Redo'], ['⌘/Ctrl + T', 'Focus today'], ['Arrows', 'Navigate'], ['Drag node', 'Re-parent'], ['⌘/Ctrl + scroll', 'Zoom']];
  return React.createElement('div', { style: { position: 'absolute', left: 12, bottom: 12, zIndex: 16, fontFamily: tw.fontFamily } },
    open && React.createElement('div', { style: { marginBottom: 8, padding: 14, borderRadius: 12, background: theme.panel,
      border: `1px solid ${theme.line}`, boxShadow: theme.shadow, width: 230 } },
      React.createElement('div', { style: { fontWeight: 700, fontSize: 13, marginBottom: 9 } }, 'Keyboard'),
      keys.map(([k, d]) => React.createElement('div', { key: k, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6, fontSize: 12.5 } },
        React.createElement('span', { style: { color: theme.muted } }, d),
        React.createElement('kbd', { style: { fontFamily: tw.monoFamily, fontSize: 11, padding: '2px 7px', borderRadius: 6,
          background: theme.chipBg, border: `1px solid ${theme.line}` } }, k)))),
    React.createElement('button', { onClick: () => setOpen(o => !o), style: { width: 34, height: 34, borderRadius: 999,
      border: `1px solid ${theme.line}`, background: theme.panel, color: theme.muted, cursor: 'pointer', fontSize: 15,
      boxShadow: theme.shadow } }, open ? '✕' : '?'));
}

function PresentBar({ theme, tw, node, index, total, onPrev, onNext, onExit }) {
  return React.createElement('div', { style: { position: 'absolute', left: '50%', bottom: 26, transform: 'translateX(-50%)',
    zIndex: 30, display: 'flex', alignItems: 'center', gap: 14, padding: '10px 14px', borderRadius: 999,
    background: theme.panel, border: `1px solid ${theme.line}`, boxShadow: theme.shadow, fontFamily: tw.fontFamily } },
    React.createElement('button', { onClick: onPrev, style: presentBtn(theme) }, '‹'),
    React.createElement('div', { style: { textAlign: 'center', minWidth: 200, maxWidth: 360 } },
      React.createElement('div', { style: { fontSize: 14, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, node ? (node.icon ? node.icon + ' ' : '') + node.text : ''),
      React.createElement('div', { style: { fontSize: 11, color: theme.muted, fontFamily: tw.monoFamily, marginTop: 2 } }, `${index + 1} / ${total}`)),
    React.createElement('button', { onClick: onNext, style: presentBtn(theme) }, '›'),
    React.createElement('div', { style: { width: 1, height: 22, background: theme.line } }),
    React.createElement('button', { onClick: onExit, style: { ...presentBtn(theme), width: 'auto', padding: '0 14px', fontSize: 13, fontWeight: 600 } }, 'Exit'));
}
function presentBtn(theme) { return { width: 36, height: 36, borderRadius: 999, border: 'none', cursor: 'pointer',
  background: theme.chipBg, color: theme.text, fontSize: 20, display: 'flex', alignItems: 'center', justifyContent: 'center' }; }

ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
