agent-ecosystem/src/renderer/components/team/editor/MarkdownSplitView.tsx
iliya 47dac2e8b5 feat: migrate to React 19
Upgrade React 18.3.1 → 19.2.4 with full type compatibility.

Changes:
- react, react-dom → ^19.0.0
- @types/react, @types/react-dom → ^19.0.0
- lucide-react → ^0.577.0 (React 19 type fixes)
- @tiptap/* → ^3.20.4 (React 19 support)
- useRef calls now require explicit initial value (undefined)
- RefObject types updated for React 19 (includes null)
- MutableRefObject → RefObject (deprecated in 19)
- act() import moved from react-dom/test-utils to react
- Scoped JSX namespace imports added where needed
2026-03-24 17:11:55 +02:00

127 lines
4.1 KiB
TypeScript

/**
* Right-side panel for markdown split/preview mode.
*
* In split mode: renders a drag-resizable handle + MarkdownPreviewPane.
* In preview mode: renders MarkdownPreviewPane at full width (no handle).
*
* CodeMirrorEditor is NOT rendered here — it stays in ProjectEditorOverlay
* and is controlled via CSS display/width.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useMarkdownScrollSync } from '@renderer/hooks/useMarkdownScrollSync';
import { MarkdownPreviewPane } from './MarkdownPreviewPane';
// =============================================================================
// Types
// =============================================================================
interface MarkdownSplitViewProps {
content: string;
mode: 'split' | 'preview';
splitRatio: number;
onSplitRatioChange: (ratio: number) => void;
/** Key that changes when the EditorView changes (e.g. activeTabId) — triggers scroll re-attach */
viewKey?: string | null;
/** Base directory for resolving relative image/link URLs */
baseDir?: string;
}
// =============================================================================
// Constants
// =============================================================================
const MIN_RATIO = 0.2;
const MAX_RATIO = 0.8;
const HANDLE_WIDTH = 4; // px
// =============================================================================
// Component
// =============================================================================
export const MarkdownSplitView = React.memo(function MarkdownSplitView({
content,
mode,
splitRatio,
onSplitRatioChange,
viewKey,
baseDir,
}: MarkdownSplitViewProps): React.ReactElement {
const [isResizing, setIsResizing] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Scroll sync auto-manages its own listener lifecycle via viewKey
const scrollSync = useMarkdownScrollSync(mode === 'split', viewKey);
// --- Resize drag logic ---
const handleMouseMove = useCallback(
(e: MouseEvent) => {
const parent = containerRef.current?.parentElement;
if (!parent) return;
const parentRect = parent.getBoundingClientRect();
const relativeX = e.clientX - parentRect.left;
const newRatio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, relativeX / parentRect.width));
onSplitRatioChange(newRatio);
},
[onSplitRatioChange]
);
const handleMouseUp = useCallback(() => {
setIsResizing(false);
}, []);
useEffect(() => {
if (!isResizing) return;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isResizing, handleMouseMove, handleMouseUp]);
const handleMouseDown = (e: React.MouseEvent): void => {
e.preventDefault();
setIsResizing(true);
};
// --- Preview width ---
const previewWidth =
mode === 'preview' ? '100%' : `calc(${(1 - splitRatio) * 100}% - ${HANDLE_WIDTH}px)`;
return (
<div ref={containerRef} className="flex h-full" style={{ width: previewWidth }}>
{/* Resize handle — only in split mode */}
{mode === 'split' && (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- resize handle
<div
className={`shrink-0 cursor-col-resize border-x border-border transition-colors ${
isResizing ? 'bg-blue-500/50' : 'hover:bg-blue-500/30'
}`}
style={{ width: HANDLE_WIDTH }}
onMouseDown={handleMouseDown}
/>
)}
{/* Preview pane */}
<div className="flex-1 overflow-hidden bg-surface">
<MarkdownPreviewPane
content={content}
scrollRef={scrollSync.previewScrollRef}
onScroll={scrollSync.handlePreviewScroll}
baseDir={baseDir}
/>
</div>
</div>
);
});