/* ============================================================
   RICH TEXT EDITOR — TipTap, loaded lazily from a CDN as ES modules.
   Fuse has no build step and no npm install, so this loads TipTap's
   packages at runtime via dynamic import() (valid even from this classic,
   Babel-transformed script — no <script type="module"> needed) and shares
   the app's existing global React instance, plus @tiptap/core/@tiptap/pm,
   through the import map in public/index.html (see the vendor-shims note
   there). Selection handling, undo/redo and schema-safe paste/drop are
   TipTap's job, not ours. On top of the base editing engine, this file
   also builds the modern block-editor interaction layer the toolbar alone
   didn't give us: a bubble menu on text selection, a contextual table
   toolbar, a "/" slash command menu, a hover grid-picker for tables, a
   drag handle for reordering blocks, and image drag-and-drop/paste. Most
   of this is editing-UI only and never changes what HTML TipTap's schema
   produces — the one exception is image resize/align (width/data-align
   attributes), which the sanitizer (lib/core/richtext.js, 00-core.jsx's
   rteWalk) explicitly allowlists.
   ============================================================ */

const TIPTAP_VERSION = '2.27.2';
const LOWLIGHT_VERSION = '3.1.0';
const RTE_EXTERNAL = 'react,react-dom,@tiptap/core,@tiptap/pm';
const rteExtUrl = (pkg) => `https://esm.sh/${pkg}@${TIPTAP_VERSION}?external=${RTE_EXTERNAL}`;

const RTE_BASE_FOR_KIND = {
  component: '/api/components', change: '/api/changes',
  request: '/api/requests', document: '/api/documents'
};
// One entry per highlight.js "common" bundle language Fuse offers; the value
// is passed straight through to both lowlight (live) and highlight.js
// (read-only) — both recognize these as standard aliases, no translation needed.
const RTE_LANGUAGE_LABELS = [
  ['plain', 'Plain text'], ['js', 'JavaScript'], ['ts', 'TypeScript'], ['python', 'Python'],
  ['bash', 'Bash'], ['sql', 'SQL'], ['json', 'JSON'], ['html', 'HTML'], ['css', 'CSS']
];

let tiptapModulesPromise = null;
/** Fetch every TipTap piece Fuse uses, once per page load, cached. */
function loadTiptap() {
  if (!tiptapModulesPromise) {
    tiptapModulesPromise = Promise.all([
      import(rteExtUrl('@tiptap/react')),
      import(rteExtUrl('@tiptap/starter-kit')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-underline')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-link')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-image')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-task-list')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-task-item')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-table')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-table-row')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-table-cell')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-table-header')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-code-block-lowlight')).then(m => m.default),
      import(rteExtUrl('@tiptap/extension-placeholder')).then(m => m.default),
      import(rteExtUrl('@tiptap/suggestion')).then(m => m.Suggestion),
      import(rteExtUrl('@tiptap/extension-drag-handle-react')).then(m => m.default),
      import('@tiptap/core').then(m => m.Extension),
      import(`https://esm.sh/lowlight@${LOWLIGHT_VERSION}`)
    ]).then(([
      { useEditor, EditorContent, BubbleMenu, ReactNodeViewRenderer, NodeViewWrapper }, StarterKit, Underline, Link, Image,
      TaskList, TaskItem, Table, TableRow, TableCell, TableHeader,
      CodeBlockLowlight, Placeholder, Suggestion, DragHandleReact, Extension,
      { createLowlight, common }
    ]) => ({
      useEditor, EditorContent, BubbleMenu, ReactNodeViewRenderer, NodeViewWrapper, StarterKit, Underline, Link, Image,
      TaskList, TaskItem, Table, TableRow, TableCell, TableHeader,
      CodeBlockLowlight, Placeholder, Suggestion, DragHandleReact, Extension,
      lowlight: createLowlight(common)
    }));
  }
  return tiptapModulesPromise;
}

/* ---------------------------------------------------- slash command menu */

// Only reachable in full mode. `ctx` (see TiptapEditorInner) supplies the
// two actions (table/image) that need React state outside the extension.
const SLASH_ITEMS = [
  { id: 'h1', label: 'Heading 1', badge: 'H1', run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run() },
  { id: 'h2', label: 'Heading 2', badge: 'H2', run: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run() },
  { id: 'h3', label: 'Heading 3', badge: 'H3', run: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run() },
  { id: 'bullet', label: 'Bullet list', icon: 'list', run: (editor) => editor.chain().focus().toggleBulletList().run() },
  { id: 'ordered', label: 'Numbered list', badge: '1.', run: (editor) => editor.chain().focus().toggleOrderedList().run() },
  { id: 'task', label: 'Checklist', icon: 'check', run: (editor) => editor.chain().focus().toggleTaskList().run() },
  { id: 'quote', label: 'Quote', badge: '”', run: (editor) => editor.chain().focus().toggleBlockquote().run() },
  { id: 'code', label: 'Code block', badge: '</>', run: (editor) => editor.chain().focus().toggleCodeBlock({ language: 'plain' }).run() },
  { id: 'table', label: 'Table', icon: 'table', run: (editor, ctx) => ctx.openTablePickerAtCursor() },
  { id: 'image', label: 'Image', icon: 'camera', run: (editor, ctx) => ctx.openImagePicker() },
  { id: 'hr', label: 'Horizontal rule', badge: '—', run: (editor) => editor.chain().focus().setHorizontalRule().run() }
];

function SlashMenuList({ items, selected, onHover, onPick }) {
  return (
    <div className="rte-slash-menu">
      {items.length === 0 && <div className="rte-slash-empty">No matches</div>}
      {items.map((it, i) => (
        <button key={it.id} type="button" className={'rte-slash-item' + (i === selected ? ' active' : '')}
          onMouseEnter={() => onHover(i)} onMouseDown={e => { e.preventDefault(); onPick(it); }}>
          {it.icon ? I[it.icon]({ size: 13 }) : <span className="rte-slash-badge">{it.badge}</span>}
          {it.label}
        </button>
      ))}
    </div>
  );
}

/** Popup lifecycle for @tiptap/suggestion's `render` option — no tippy.js
 * dependency, just a plain fixed-position div mounted with the same
 * ReactDOM.createRoot the app's React 18 UMD build already exposes. */
function buildSlashRenderer() {
  let root = null, el = null, items = [], selected = 0, commandFn = null;

  const paint = () => {
    if (!root) return;
    root.render(React.createElement(SlashMenuList, {
      items, selected,
      onHover: (i) => { selected = i; paint(); },
      onPick: (item) => commandFn(item)
    }));
  };
  const place = (clientRect) => {
    const rect = clientRect && clientRect();
    if (!rect || !el) return;
    el.style.left = rect.left + 'px';
    el.style.top = (rect.bottom + 6) + 'px';
  };

  return {
    onStart: (props) => {
      el = document.createElement('div');
      el.className = 'rte-slash-anchor';
      el.style.position = 'fixed';
      el.style.zIndex = 1000;
      document.body.appendChild(el);
      root = ReactDOM.createRoot(el);
      commandFn = props.command;
      items = props.items; selected = 0;
      place(props.clientRect);
      paint();
    },
    onUpdate: (props) => {
      commandFn = props.command;
      items = props.items; selected = 0;
      place(props.clientRect);
      paint();
    },
    onKeyDown: ({ event }) => {
      if (event.key === 'Escape') return true;
      if (!items.length) return false;
      if (event.key === 'ArrowDown') { selected = (selected + 1) % items.length; paint(); return true; }
      if (event.key === 'ArrowUp') { selected = (selected - 1 + items.length) % items.length; paint(); return true; }
      if (event.key === 'Enter') { commandFn(items[selected]); return true; }
      return false;
    },
    onExit: () => {
      if (root) { root.unmount(); root = null; }
      if (el) { el.remove(); el = null; }
    }
  };
}

function buildSlashCommandExtension(mods, ctxRef) {
  return mods.Extension.create({
    name: 'slashCommand',
    addProseMirrorPlugins() {
      return [
        mods.Suggestion({
          editor: this.editor,
          char: '/',
          allowSpaces: false,
          items: ({ query }) => SLASH_ITEMS.filter(it => it.label.toLowerCase().includes(query.toLowerCase())),
          command: ({ editor, range, props }) => {
            editor.chain().focus().deleteRange(range).run();
            props.run(editor, ctxRef.current);
          },
          render: buildSlashRenderer
        })
      ];
    }
  });
}

/* -------------------------------------------------------- table picker */

function useOutsideClose(ref, onClose) {
  useEffect(() => {
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
  }, [ref, onClose]);
}

function TableGridPicker({ onPick, onClose }) {
  const [hover, setHover] = useState({ r: 3, c: 3 });
  const rootRef = useRef(null);
  useOutsideClose(rootRef, onClose);
  const maxR = 8, maxC = 10;
  return (
    <div className="rte-table-picker" ref={rootRef}>
      <div className="rte-table-picker-label">{hover.r} &times; {hover.c}</div>
      <div>
        {Array.from({ length: maxR }, (_, ri) => ri + 1).map(r => (
          <div key={r} className="rte-table-picker-row">
            {Array.from({ length: maxC }, (_, ci) => ci + 1).map(c => (
              <div key={c} className={'rte-table-picker-cell' + (r <= hover.r && c <= hover.c ? ' on' : '')}
                onMouseEnter={() => setHover({ r, c })}
                onMouseDown={e => { e.preventDefault(); onPick(r, c); }} />
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

/* --------------------------------------------------- resizable images */

const IMAGE_MIN_WIDTH = 60;
const IMAGE_MAX_WIDTH = 1000;

/** A React NodeView for the image node — adds a drag handle (shown only while
 * selected) that continuously calls updateAttributes({width}) as the user
 * drags, and a class encoding alignment for the CSS float rules to key off. */
function buildImageNodeView(mods) {
  const { NodeViewWrapper } = mods;
  return function ImageNodeView({ node, updateAttributes, selected }) {
    const { src, alt, width, align } = node.attrs;
    const imgRef = useRef(null);

    const startResize = (e) => {
      e.preventDefault();
      e.stopPropagation();
      const startX = e.clientX;
      const startWidth = imgRef.current.getBoundingClientRect().width;
      const onMove = (ev) => {
        const next = Math.round(startWidth + (ev.clientX - startX));
        updateAttributes({ width: Math.max(IMAGE_MIN_WIDTH, Math.min(IMAGE_MAX_WIDTH, next)) });
      };
      const onUp = () => {
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
      };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
    };

    return (
      <NodeViewWrapper className={'rte-img-wrap' + (selected ? ' selected' : '') + (align ? ` align-${align}` : '')}
        style={width ? { width } : undefined}>
        <img ref={imgRef} src={src} alt={alt} draggable={false} />
        {selected && <div className="rte-img-handle" onMouseDown={startResize} title="Drag to resize" />}
      </NodeViewWrapper>
    );
  };
}

/** The base Image extension has no size/position controls at all — this adds
 * a `width` (px) and `align` (left/center/right) attribute, rendered as the
 * plain `width` and `data-align` HTML attributes respectively (both explicitly
 * allowlisted in lib/core/richtext.js and 00-core.jsx's rteWalk). */
function buildResizableImage(mods) {
  return mods.Image.extend({
    addAttributes() {
      return {
        ...this.parent(),
        width: {
          default: null,
          parseHTML: el => { const w = el.getAttribute('width'); return w ? parseInt(w, 10) : null; },
          renderHTML: attrs => attrs.width ? { width: attrs.width } : {}
        },
        align: {
          default: null,
          parseHTML: el => el.getAttribute('data-align') || null,
          renderHTML: attrs => attrs.align ? { 'data-align': attrs.align } : {}
        }
      };
    },
    addNodeView() {
      return mods.ReactNodeViewRenderer(buildImageNodeView(mods));
    }
  });
}

/* ------------------------------------------------------------- editor */

function buildExtensions(mods, { mode, placeholder, ctxRef }) {
  const isFull = mode === 'full';
  const ext = [
    mods.StarterKit.configure({
      codeBlock: false, // CodeBlockLowlight replaces it, full mode only
      heading: isFull ? { levels: [1, 2, 3] } : false,
      blockquote: isFull ? {} : false,
      horizontalRule: isFull ? {} : false
    }),
    mods.Underline,
    mods.Link.configure({ openOnClick: false, autolink: false, protocols: ['http', 'https', 'mailto'] }),
    isFull ? buildResizableImage(mods) : mods.Image,
    mods.Placeholder.configure({ placeholder: placeholder || (isFull ? 'Write a description…' : 'Add a comment…') })
  ];
  if (isFull) {
    ext.push(
      mods.TaskList, mods.TaskItem.configure({ nested: false }),
      mods.Table.configure({ resizable: false }), mods.TableRow, mods.TableHeader, mods.TableCell,
      mods.CodeBlockLowlight.configure({ lowlight: mods.lowlight }),
      buildSlashCommandExtension(mods, ctxRef)
    );
  }
  return ext;
}

function RteButton({ title, active, disabled, onAction, children }) {
  return (
    <button type="button" className={'rte-btn' + (active ? ' active' : '')} title={title} disabled={disabled}
      onMouseDown={e => { e.preventDefault(); if (!disabled) onAction(e); }}>
      {children}
    </button>
  );
}

function RteSkeleton({ mode }) {
  return (
    <div className="rte-wrap rte-loading">
      <div className="rte-toolbar"><span className="spinner sm" /><span className="xsmall dim">Loading editor…</span></div>
      <div className="rte-body-wrap"><div className={'rte-body rte-content' + (mode === 'compact' ? ' compact' : '')} /></div>
    </div>
  );
}

/** Public component — same props/behavior contract as before the TipTap migration. */
function RichTextEditor({ value, onChange, mode = 'full', uploadContext, placeholder, toast }) {
  const [mods, setMods] = useState(null);
  const [failed, setFailed] = useState(false);
  const say = toast || (() => {});

  useEffect(() => {
    let cancelled = false;
    loadTiptap()
      .then(m => { if (!cancelled) setMods(m); })
      .catch(err => { if (!cancelled) { setFailed(true); console.error('Rich text editor failed to load', err); } });
    return () => { cancelled = true; };
  }, []);

  if (failed) {
    return (
      <div className="stack" style={{ gap: 6 }}>
        <textarea value={value} onChange={e => onChange(e.target.value)} placeholder={placeholder}
          style={{ minHeight: mode === 'compact' ? 70 : 140 }} />
        <span className="xsmall dim">
          The rich text editor could not load (no internet access?) — editing as plain text for now.
        </span>
      </div>
    );
  }

  if (!mods) return <RteSkeleton mode={mode} />;

  return <TiptapEditorInner mods={mods} value={value} onChange={onChange} mode={mode}
    uploadContext={uploadContext} placeholder={placeholder} toast={say} />;
}

function TiptapEditorInner({ mods, value, onChange, mode, uploadContext, placeholder, toast }) {
  const { useEditor, EditorContent } = mods;
  const isFull = mode === 'full';
  const fileRef = useRef(null);
  const lastEmitted = useRef(null);
  const ctxRef = useRef({});
  const toolbarRef = useRef(null);
  const [, forceRerender] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [linkDlg, setLinkDlg] = useState(null);
  const [tablePickerAt, setTablePickerAt] = useState(null);

  const extensions = useMemo(() => buildExtensions(mods, { mode, placeholder, ctxRef }), [mods, mode, placeholder]);

  // Only read once, on mount — the editor owns its own document after that.
  const initialContent = useMemo(() => {
    const raw = String(value || '');
    if (!raw.trim()) return '';
    return looksLikeRichText(raw) ? rteCleanHtmlString(raw) : rtePlainTextToHtml(raw);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const editor = useEditor({
    extensions,
    content: initialContent,
    onUpdate: ({ editor }) => {
      const html = rteCleanHtmlString(editor.getHTML());
      if (html === lastEmitted.current) return;
      lastEmitted.current = html;
      onChange(html);
    },
    onSelectionUpdate: () => forceRerender(n => n + 1),
    onTransaction: () => forceRerender(n => n + 1),
    editorProps: {
      handleDrop: (view, event) => {
        const file = Array.from((event.dataTransfer && event.dataTransfer.files) || []).find(f => f.type.startsWith('image/'));
        if (!file) return false;
        event.preventDefault();
        if (!uploadContext) { toast('Save the record first to add images', true); return true; }
        const coords = view.posAtCoords({ left: event.clientX, top: event.clientY });
        insertImageFile(file, coords ? coords.pos : undefined);
        return true;
      },
      handlePaste: (view, event) => {
        const file = Array.from((event.clipboardData && event.clipboardData.files) || []).find(f => f.type.startsWith('image/'));
        if (!file) return false;
        event.preventDefault();
        if (!uploadContext) { toast('Save the record first to add images', true); return true; }
        insertImageFile(file);
        return true;
      }
    }
  }, [extensions]);

  if (!editor) return <RteSkeleton mode={mode} />;

  const insertImageFile = async (file, atPos) => {
    if (!uploadContext || !file) return;
    if (!/^image\/(png|jpeg|gif|webp)$/.test(file.type)) { toast('Images must be PNG, JPEG, GIF or WebP', true); return; }
    setUploading(true);
    try {
      const base = RTE_BASE_FOR_KIND[uploadContext.entityKind] + '/' + uploadContext.entityId;
      const params = new URLSearchParams({ filename: file.name, type: file.type });
      const result = await uploadFile(`${base}/attachments?${params}`, file);
      if (!result || !result.id) throw new Error('Upload did not return a file reference');
      const chain = editor.chain().focus();
      if (typeof atPos === 'number') chain.setTextSelection(atPos);
      chain.setImage({ src: `/api/attachments/${result.id}/download`, alt: file.name }).run();
    } catch (err) { toast(err.message, true); }
    setUploading(false);
  };

  const openLinkDialog = () => {
    const { from, to, empty } = editor.state.selection;
    const label = empty ? '' : editor.state.doc.textBetween(from, to, ' ');
    setLinkDlg({ url: editor.getAttributes('link').href || '', label });
  };
  const applyLink = ({ url, label }) => {
    const href = rteSafeHref(url);
    if (!href) { toast('Enter a link starting with http://, https:// or mailto:', true); return; }
    const chain = editor.chain().focus();
    if (editor.state.selection.empty) {
      chain.insertContent({ type: 'text', text: label.trim() || href, marks: [{ type: 'link', attrs: { href } }] }).run();
    } else {
      chain.extendMarkRange('link').setLink({ href }).run();
    }
    setLinkDlg(null);
  };

  const insertTable = (rows, cols) => {
    editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
    setTablePickerAt(null);
  };

  ctxRef.current = {
    openTablePickerAtCursor: () => {
      const coords = editor.view.coordsAtPos(editor.state.selection.from);
      setTablePickerAt({ x: coords.left, y: coords.bottom + 6 });
    },
    openImagePicker: () => fileRef.current && fileRef.current.click()
  };

  const active = (name, attrs) => editor.isActive(name, attrs);
  const BubbleMenu = mods.BubbleMenu;
  const DragHandleReact = mods.DragHandleReact;

  // Selections near the top of the editor have no room for a bubble menu placed
  // above them without it sliding under the toolbar — tell popper's flip modifier
  // the toolbar's height counts as unavailable space, so it flips to below there
  // and back to above once there's genuine room, instead of always picking one side.
  const bubbleTippyOptions = () => ({
    popperOptions: {
      modifiers: [{
        name: 'flip',
        options: { padding: { top: (toolbarRef.current ? toolbarRef.current.offsetHeight : 44) + 8, bottom: 5, left: 5, right: 5 } }
      }]
    }
  });

  return (
    <div className="rte-wrap">
      <div className="rte-toolbar" ref={toolbarRef}>
        <RteButton title="Undo" disabled={!editor.can().undo()} onAction={() => editor.chain().focus().undo().run()}>↶</RteButton>
        <RteButton title="Redo" disabled={!editor.can().redo()} onAction={() => editor.chain().focus().redo().run()}>↷</RteButton>
        <span className="rte-sep" />
        <RteButton title="Bold (Ctrl/Cmd+B)" active={active('bold')} onAction={() => editor.chain().focus().toggleBold().run()}>
          <strong>B</strong></RteButton>
        <RteButton title="Italic (Ctrl/Cmd+I)" active={active('italic')} onAction={() => editor.chain().focus().toggleItalic().run()}>
          <em>I</em></RteButton>
        {isFull &&
          <RteButton title="Underline (Ctrl/Cmd+U)" active={active('underline')}
            onAction={() => editor.chain().focus().toggleUnderline().run()}><u>U</u></RteButton>}
        {isFull &&
          <RteButton title="Strikethrough" active={active('strike')}
            onAction={() => editor.chain().focus().toggleStrike().run()}><s>S</s></RteButton>}
        <span className="rte-sep" />
        {isFull && <>
          <RteButton title="Heading 1" active={active('heading', { level: 1 })}
            onAction={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}>H1</RteButton>
          <RteButton title="Heading 2" active={active('heading', { level: 2 })}
            onAction={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>H2</RteButton>
          <RteButton title="Heading 3" active={active('heading', { level: 3 })}
            onAction={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}>H3</RteButton>
          <span className="rte-sep" />
        </>}
        <RteButton title="Bullet list" active={active('bulletList')}
          onAction={() => editor.chain().focus().toggleBulletList().run()}><I.list size={14} /></RteButton>
        <RteButton title="Numbered list" active={active('orderedList')}
          onAction={() => editor.chain().focus().toggleOrderedList().run()}>1.</RteButton>
        {isFull &&
          <RteButton title="Checklist" active={active('taskList')}
            onAction={() => editor.chain().focus().toggleTaskList().run()}><I.check size={14} /></RteButton>}
        <span className="rte-sep" />
        {isFull &&
          <RteButton title="Quote" active={active('blockquote')}
            onAction={() => editor.chain().focus().toggleBlockquote().run()}>&#8221;</RteButton>}
        <RteButton title="Inline code" active={active('code')}
          onAction={() => editor.chain().focus().toggleCode().run()}>{'</>'}</RteButton>
        {isFull &&
          <select className="rte-lang-pick" value=""
            onChange={e => { if (e.target.value) editor.chain().focus().toggleCodeBlock({ language: e.target.value }).run(); e.target.value = ''; }}
            title="Insert a code block">
            <option value="" disabled>Code block…</option>
            {RTE_LANGUAGE_LABELS.map(([k, label]) => <option key={k} value={k}>{label}</option>)}
          </select>}
        <span className="rte-sep" />
        <RteButton title="Link" active={active('link')} onAction={openLinkDialog}><I.link size={14} /></RteButton>
        {isFull &&
          <RteButton title="Table" onAction={(e) => {
            const r = e.currentTarget.getBoundingClientRect();
            setTablePickerAt({ x: r.left, y: r.bottom + 6 });
          }}><I.table size={14} /></RteButton>}
        {uploadContext &&
          <RteButton title="Insert image" disabled={uploading} onAction={() => fileRef.current.click()}>
            {uploading ? '…' : <I.camera size={14} />}</RteButton>}
        {!uploadContext &&
          <RteButton title="Save the record first to add images" disabled onAction={() => {}}><I.camera size={14} /></RteButton>}
        {isFull && <RteButton title="Horizontal rule" onAction={() => editor.chain().focus().setHorizontalRule().run()}>&mdash;</RteButton>}
      </div>

      <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/gif,image/webp" style={{ display: 'none' }}
        onChange={e => { if (e.target.files[0]) insertImageFile(e.target.files[0]); e.target.value = ''; }} />

      <div className="rte-body-wrap">
        {isFull && DragHandleReact &&
          <DragHandleReact editor={editor}>
            <div className="rte-drag-handle" title="Drag to reorder">{I.grip({ size: 12 })}</div>
          </DragHandleReact>}

        {BubbleMenu &&
          <BubbleMenu editor={editor} pluginKey="rteTextBubble" tippyOptions={bubbleTippyOptions()}
            shouldShow={({ editor, state }) => !state.selection.empty && !editor.isActive('table')
              && !editor.isActive('codeBlock') && !editor.isActive('image')}>
            <div className="rte-bubble">
              <RteButton title="Bold" active={active('bold')} onAction={() => editor.chain().focus().toggleBold().run()}><strong>B</strong></RteButton>
              <RteButton title="Italic" active={active('italic')} onAction={() => editor.chain().focus().toggleItalic().run()}><em>I</em></RteButton>
              {isFull &&
                <RteButton title="Underline" active={active('underline')} onAction={() => editor.chain().focus().toggleUnderline().run()}><u>U</u></RteButton>}
              {isFull &&
                <RteButton title="Strikethrough" active={active('strike')} onAction={() => editor.chain().focus().toggleStrike().run()}><s>S</s></RteButton>}
              <RteButton title="Link" active={active('link')} onAction={openLinkDialog}><I.link size={13} /></RteButton>
            </div>
          </BubbleMenu>}

        {isFull && BubbleMenu &&
          <BubbleMenu editor={editor} pluginKey="rteTableBubble" tippyOptions={bubbleTippyOptions()}
            shouldShow={({ editor }) => editor.isActive('table')}>
            <div className="rte-bubble">
              <RteButton title="Add row" disabled={!editor.can().addRowAfter()} onAction={() => editor.chain().focus().addRowAfter().run()}>+row</RteButton>
              <RteButton title="Add column" disabled={!editor.can().addColumnAfter()} onAction={() => editor.chain().focus().addColumnAfter().run()}>+col</RteButton>
              <RteButton title="Delete row" disabled={!editor.can().deleteRow()} onAction={() => editor.chain().focus().deleteRow().run()}>-row</RteButton>
              <RteButton title="Delete column" disabled={!editor.can().deleteColumn()} onAction={() => editor.chain().focus().deleteColumn().run()}>-col</RteButton>
              <span className="rte-sep" />
              <RteButton title="Delete table" onAction={() => editor.chain().focus().deleteTable().run()}><I.trash size={13} /></RteButton>
            </div>
          </BubbleMenu>}

        {isFull && BubbleMenu &&
          <BubbleMenu editor={editor} pluginKey="rteImageBubble" tippyOptions={bubbleTippyOptions()}
            shouldShow={({ editor }) => editor.isActive('image')}>
            <div className="rte-bubble">
              <RteButton title="Small" active={active('image', { width: 240 })}
                onAction={() => editor.chain().focus().updateAttributes('image', { width: 240 }).run()}>S</RteButton>
              <RteButton title="Medium" active={active('image', { width: 480 })}
                onAction={() => editor.chain().focus().updateAttributes('image', { width: 480 }).run()}>M</RteButton>
              <RteButton title="Large" active={active('image', { width: 720 })}
                onAction={() => editor.chain().focus().updateAttributes('image', { width: 720 }).run()}>L</RteButton>
              <RteButton title="Full width" active={active('image', { width: null })}
                onAction={() => editor.chain().focus().updateAttributes('image', { width: null }).run()}>Full</RteButton>
              <span className="rte-sep" />
              <RteButton title="Align left" active={active('image', { align: 'left' })}
                onAction={() => editor.chain().focus().updateAttributes('image', { align: 'left' }).run()}><I.alignLeft size={13} /></RteButton>
              <RteButton title="Align center" active={active('image', { align: 'center' })}
                onAction={() => editor.chain().focus().updateAttributes('image', { align: 'center' }).run()}><I.alignCenter size={13} /></RteButton>
              <RteButton title="Align right" active={active('image', { align: 'right' })}
                onAction={() => editor.chain().focus().updateAttributes('image', { align: 'right' }).run()}><I.alignRight size={13} /></RteButton>
              <span className="rte-sep" />
              <RteButton title="Remove image" onAction={() => editor.chain().focus().deleteSelection().run()}><I.trash size={13} /></RteButton>
            </div>
          </BubbleMenu>}

        <EditorContent editor={editor} className={'rte-body rte-content' + (mode === 'compact' ? ' compact' : '')} />
      </div>

      {tablePickerAt &&
        <div style={{ position: 'fixed', left: tablePickerAt.x, top: tablePickerAt.y, zIndex: 50 }}>
          <TableGridPicker onPick={insertTable} onClose={() => setTablePickerAt(null)} />
        </div>}

      {linkDlg &&
        <Modal title="Insert link" onClose={() => setLinkDlg(null)}
          footer={<>
            <button className="btn" onClick={() => setLinkDlg(null)}>Cancel</button>
            <button className="btn primary" disabled={!linkDlg.url.trim()} onClick={() => applyLink(linkDlg)}>Insert</button>
          </>}>
          <Field label="URL" hint="http://, https:// or mailto:">
            <input type="text" autoFocus value={linkDlg.url} onChange={e => setLinkDlg(d => ({ ...d, url: e.target.value }))}
              placeholder="https://example.com" />
          </Field>
          <Field label="Text">
            <input type="text" value={linkDlg.label} onChange={e => setLinkDlg(d => ({ ...d, label: e.target.value }))} />
          </Field>
        </Modal>}
    </div>
  );
}
