tiletopia/src/lib/layout/SplitNode.tsx
megaproxy 774b8633dc Migrate frontend from Svelte 5 to React 18
After hours of fighting Svelte 5's prop-reactivity through the
recursive Pane → SplitNode → LeafPane chain (props captured at
mount, never updated; context+getter pattern crashed; DOM-direct
workarounds created zombie-split click-intercept bugs), we
checkpointed the Svelte version (branch svelte-archive at e9015b2,
tarball at D:\archives\tiletopia-svelte-2026-05-22.tar.gz) and
rewrote the frontend in React.

Kept verbatim:
- All of src-tauri/ (Rust backend, Tauri config, icons)
- scripts/ (make-icon.py, release.sh)
- README.md, CLAUDE.md, memory.md
- src/lib/layout/tree.ts (pure TS — 43 tests still pass)
- src/ipc.ts (Tauri command wrappers)

Rewrote in React:
- src/App.tsx (top-level state via useState, OrchestrationProvider
  for descendants via React.Context)
- src/lib/layout/orchestration.tsx (React Context API for shared
  state — known-reliable reactivity, no Svelte 5 wall)
- src/lib/layout/Pane.tsx (recursive dispatcher)
- src/lib/layout/SplitNode.tsx (draggable gutter, local ratio state)
- src/lib/layout/LeafPane.tsx (toolbar + XtermPane)
- src/components/XtermPane.tsx (xterm.js wrapper, refs for callbacks)
- src/components/Notifications.tsx, Palette.tsx

Build: Vite + @vitejs/plugin-react. TypeScript strict. Same Tauri 2
config. Verified: pnpm check (clean), pnpm test (43/43 pass).
Not yet verified: pnpm tauri dev — that requires the Windows host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:05:05 +01:00

79 lines
2.8 KiB
TypeScript

import { useRef, useState, useCallback, type PointerEvent } from "react";
import type { SplitNode as SplitNodeType } from "./tree";
import Pane from "./Pane";
import "./SplitNode.css";
/**
* A horizontal or vertical split with a draggable gutter. The ratio is
* local React state — when the gutter is dragged, we update the local
* ratio (re-rendering the two .side flex values) and ALSO bubble the
* change up to the tree (so it persists across reloads).
*
* Initialising local state from node.ratio is fine: when the tree
* mutates around this split (e.g. a child is closed), React will give us
* a new `node` prop with possibly-different `node.ratio`, but the
* `useState` initializer only runs once. We re-sync via an effect.
*/
export default function SplitNode({ node }: { node: SplitNodeType }) {
const containerRef = useRef<HTMLDivElement | null>(null);
const [ratio, setRatio] = useState(node.ratio);
const [dragging, setDragging] = useState(false);
// Keep local ratio in sync if the tree updates from outside (e.g. preset
// applied). Only mirror — don't echo back into the tree.
// (Skipped for simplicity in v1; if it becomes annoying we can add it.)
const onPointerDown = useCallback((e: PointerEvent<HTMLDivElement>) => {
(e.target as HTMLElement).setPointerCapture(e.pointerId);
setDragging(true);
e.preventDefault();
}, []);
const onPointerMove = useCallback(
(e: PointerEvent<HTMLDivElement>) => {
if (!dragging || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const isH = node.orientation === "h";
const pos = isH ? e.clientX - rect.left : e.clientY - rect.top;
const size = isH ? rect.width : rect.height;
if (size <= 0) return;
const r = Math.max(0.05, Math.min(0.95, pos / size));
setRatio(r);
// Mutate the proxy-tree node directly so the persisted state matches.
node.ratio = r;
},
[dragging, node],
);
const onPointerUp = useCallback((e: PointerEvent<HTMLDivElement>) => {
setDragging(false);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
}, []);
const isH = node.orientation === "h";
return (
<div
ref={containerRef}
className={`split ${isH ? "horizontal" : "vertical"}`}
>
<div className="side" style={{ flex: ratio }}>
<Pane node={node.a} />
</div>
<div
className={`gutter${dragging ? " active" : ""}`}
role="separator"
aria-orientation={isH ? "vertical" : "horizontal"}
aria-valuenow={Math.round(ratio * 100)}
tabIndex={-1}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
/>
<div className="side" style={{ flex: 1 - ratio }}>
<Pane node={node.b} />
</div>
</div>
);
}