schemeta/frontend-react/src/App.tsx
2026-02-19 22:32:13 -05:00

193 lines
6.3 KiB
TypeScript

import { useMemo } from "react";
import { analyze, autoLayout, compile, tidyLayout } from "./api/client";
import { CanvasArea } from "./components/CanvasArea";
import { LeftPanel } from "./components/LeftPanel";
import { RightInspector } from "./components/RightInspector";
import { TopBar } from "./components/TopBar";
import type { TopBarStatusTone } from "./components/TopBar";
import sampleFixture from "./fixtures/sample.schemeta.json";
import { useSchemetaActions, useSchemetaSelector } from "./hooks";
import { createSchemetaStore } from "./state";
import type { SchemetaModel } from "./state";
type EndpointActionId = "compile" | "analyze" | "layout-auto" | "layout-tidy";
type LocalActionId = "load-sample" | "reset-sample";
type WorkflowActionId = EndpointActionId | LocalActionId;
type WorkflowAction = {
id: WorkflowActionId;
label: string;
};
const SAMPLE_PAYLOAD = sampleFixture as SchemetaModel;
function getPayloadOrThrow(payload: unknown): SchemetaModel {
if (typeof payload !== "object" || payload === null) {
throw new Error("No model loaded. Run 'Load Sample JSON' first.");
}
return payload as SchemetaModel;
}
function toCompileFromAnalyze(result: Awaited<ReturnType<typeof analyze>>): Record<string, unknown> {
return {
api_version: result.api_version,
schema_version: result.schema_version,
request_id: result.request_id,
ok: result.ok ?? true,
errors: Array.isArray(result.errors) ? result.errors : [],
warnings: Array.isArray(result.warnings) ? result.warnings : [],
topology: result.topology ?? {}
};
}
export function App() {
const store = useMemo(() => createSchemetaStore({ model: SAMPLE_PAYLOAD }), []);
const actionsApi = useSchemetaActions(store);
const model = useSchemetaSelector(store, (state) => state.model);
const selection = useSchemetaSelector(store, (state) => state.selection);
const compileResult = useSchemetaSelector(store, (state) => state.compileResult);
const lifecycle = useSchemetaSelector(store, (state) => state.lifecycle);
const jsonError = useSchemetaSelector(store, (state) => state.uiFlags.jsonError);
const lastApiVersion = useSchemetaSelector(store, (state) => {
const apiVersion = state.compileResult && typeof state.compileResult.api_version === "string" ? state.compileResult.api_version : null;
return apiVersion ?? "-";
});
const actions = useMemo<WorkflowAction[]>(
() => [
{ id: "load-sample", label: "Load Sample JSON" },
{ id: "compile", label: "Compile" },
{ id: "analyze", label: "Analyze" },
{ id: "layout-auto", label: "Auto Layout" },
{ id: "layout-tidy", label: "Auto Tidy" },
{ id: "reset-sample", label: "Reset Sample" }
],
[]
);
const status = jsonError
? `JSON parse failed: ${jsonError}`
: lifecycle.isCompiling
? `${lifecycle.lastAction ?? "Working"}...`
: lifecycle.lastError
? `${lifecycle.lastAction ?? "Action"} failed: ${lifecycle.lastError}`
: lifecycle.lastAction
? `${lifecycle.lastAction} complete`
: "Idle";
const statusTone: TopBarStatusTone = jsonError
? "error"
: lifecycle.isCompiling
? "busy"
: lifecycle.lastError
? "error"
: lifecycle.lastAction
? "success"
: "idle";
async function runAction(actionId: WorkflowActionId) {
const action = actions.find((item) => item.id === actionId);
if (!action) {
return;
}
actionsApi.beginCompile(action.label);
try {
if (action.id === "load-sample" || action.id === "reset-sample") {
const parsed = actionsApi.applyJsonText(JSON.stringify(SAMPLE_PAYLOAD));
if (!parsed.ok) {
throw new Error(parsed.error);
}
actionsApi.setCompileResult(null);
actionsApi.setSelection({ selectedRefs: [], selectedNet: null, selectedPin: null });
actionsApi.setViewport({ scale: 1, panX: 40, panY: 40 });
actionsApi.completeCompile(action.label);
return;
}
const payload = getPayloadOrThrow(store.getState().model);
switch (action.id) {
case "compile":
{
const result = await compile(payload);
actionsApi.setCompileResult(result);
}
break;
case "analyze":
{
const result = await analyze(payload);
actionsApi.setCompileResult(toCompileFromAnalyze(result));
}
break;
case "layout-auto":
{
const result = await autoLayout(payload);
if (result.model) {
actionsApi.setModel(result.model);
}
if (result.compile) {
actionsApi.setCompileResult(result.compile);
}
}
break;
case "layout-tidy":
{
const result = await tidyLayout(payload);
if (result.model) {
actionsApi.setModel(result.model);
}
if (result.compile) {
actionsApi.setCompileResult(result.compile);
}
}
break;
}
actionsApi.completeCompile(action.label);
} catch (err) {
actionsApi.failCompile(action.label, err);
}
}
return (
<div className="app-shell">
<TopBar
title="React Frontend Skeleton"
actions={actions.map((action) => ({
id: action.id,
label: action.label,
onClick: () => {
void runAction(action.id);
},
disabled: lifecycle.isCompiling
}))}
statusMessage={status}
statusTone={statusTone}
apiVersion={lastApiVersion}
/>
<div className="workspace-grid">
<LeftPanel
model={model}
selection={selection}
onSelectInstance={(ref) =>
actionsApi.setSelection({
selectedRefs: selection.selectedRefs.includes(ref) ? [] : [ref],
selectedNet: null,
selectedPin: null
})
}
onSelectNet={(name) =>
actionsApi.setSelection({
selectedRefs: [],
selectedNet: selection.selectedNet === name ? null : name,
selectedPin: null
})
}
/>
<CanvasArea />
<RightInspector model={model} selection={selection} compileResult={compileResult} />
</div>
</div>
);
}