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>
This commit is contained in:
megaproxy 2026-05-22 18:05:05 +01:00
parent e9015b2790
commit 774b8633dc
32 changed files with 2087 additions and 1825 deletions

View file

@ -0,0 +1,58 @@
import { createContext, useContext, type ReactNode } from "react";
import type { Orientation, NodeId } from "./tree";
import type { PaneId } from "../../ipc";
/**
* Orchestration context every piece of shared state and every operation
* that a Pane / SplitNode / LeafPane might call. Lives in React context so
* descendants can `useOrchestration()` without prop drilling.
*
* activeLeafId comes in as a plain value (re-derived by App's useState).
* React's context is reactive: when the App-level Provider updates the
* value, ALL consumers re-render. No Svelte-style props-don't-propagate
* trap here.
*/
export interface Orchestration {
// Read-only state
activeLeafId: NodeId | null;
distros: string[];
// Tree mutations
split: (leafId: NodeId, orientation: Orientation) => void;
close: (leafId: NodeId) => void;
setDistro: (leafId: NodeId, distro: string) => void;
setLabel: (leafId: NodeId, label: string | undefined) => void;
toggleBroadcast: (leafId: NodeId) => void;
// Per-pane orchestration
setActive: (leafId: NodeId) => void;
registerPaneId: (leafId: NodeId, paneId: PaneId | null) => void;
broadcastFrom: (originLeafId: NodeId, dataB64: string) => void;
notify: (message: string) => void;
}
const OrchestrationContext = createContext<Orchestration | null>(null);
export function OrchestrationProvider({
value,
children,
}: {
value: Orchestration;
children: ReactNode;
}) {
return (
<OrchestrationContext.Provider value={value}>
{children}
</OrchestrationContext.Provider>
);
}
export function useOrchestration(): Orchestration {
const orch = useContext(OrchestrationContext);
if (!orch) {
throw new Error(
"useOrchestration() must be called inside <OrchestrationProvider>",
);
}
return orch;
}