diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f98774d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.{cpp,h,hpp}]
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9c94a93
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,141 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+# Superseded runs of the same PR/branch are cancelled to save runner time.
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+ EMSDK_VERSION: 5.0.7 # keep in sync with scripts/build-wasm.sh
+
+jobs:
+ cpp-tests:
+ name: C++ engine (native tests)
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ - name: Configure
+ run: cmake -S wasm -B wasm/build-native
+ - name: Build
+ run: cmake --build wasm/build-native --parallel
+ - name: Test
+ run: ctest --test-dir wasm/build-native --output-on-failure
+
+ wasm:
+ name: WASM engine build
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: emscripten-core/setup-emsdk@v15
+ with:
+ version: 5.0.7
+ actions-cache-folder: emsdk-cache
+ - name: Build engine to WebAssembly
+ run: ./scripts/build-wasm.sh
+ - name: Conformance vectors against the wasm build (Node)
+ run: |
+ emcmake cmake -S wasm -B wasm/build-wasm -DCMAKE_BUILD_TYPE=Release -DWEBMETAL_BUILD_NODE_TEST=ON
+ cmake --build wasm/build-wasm --parallel
+ node scripts/run-conformance-node.mjs
+ - uses: actions/upload-artifact@v4
+ with:
+ name: webmetal-wasm
+ path: frontend/public/wasm/
+ if-no-files-found: error
+
+ frontend:
+ name: Frontend (lint, typecheck, test, build)
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ defaults:
+ run:
+ working-directory: frontend
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ cache-dependency-path: frontend/package-lock.json
+ - name: Install dependencies
+ run: npm ci
+ - name: Lint
+ run: npm run lint
+ - name: Format check
+ run: npm run format:check
+ - name: Typecheck
+ run: npm run typecheck
+ - name: Test (unit + conformance + bundled examples)
+ run: npm test
+ - name: Build (without wasm - must succeed standalone)
+ run: npm run build
+
+ integration:
+ name: Integration (frontend + wasm together)
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ needs: wasm
+ defaults:
+ run:
+ working-directory: frontend
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ cache-dependency-path: frontend/package-lock.json
+ - uses: actions/download-artifact@v4
+ with:
+ name: webmetal-wasm
+ path: frontend/public/wasm/
+ - name: Install dependencies
+ run: npm ci
+ - name: Build with the engine present
+ run: npm run build
+ - name: Verify the engine ships in dist/
+ run: |
+ test -f dist/wasm/webmetal-engine.mjs
+ test -f dist/wasm/webmetal-engine.wasm
+ - name: Verify dist/ is subdirectory-safe (relative asset paths)
+ run: |
+ grep -q 'src="\./assets/' dist/index.html
+ grep -q 'href="\./' dist/index.html
+ if grep -q '"/assets/' dist/index.html; then
+ echo 'index.html references absolute /assets/ paths' >&2
+ exit 1
+ fi
+ - name: Upload ready-to-deploy site
+ uses: actions/upload-artifact@v4
+ with:
+ name: webmetal-dist
+ path: frontend/dist/
+ if-no-files-found: error
+ retention-days: 30
+
+ # Single aggregate check for branch protection: require only this job.
+ ci-ok:
+ name: CI passed
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ needs: [cpp-tests, wasm, frontend, integration]
+ if: always()
+ steps:
+ - name: Check that every job succeeded
+ run: |
+ results='${{ join(needs.*.result, ' ') }}'
+ echo "job results: $results"
+ for r in $results; do
+ if [ "$r" != "success" ]; then
+ echo "a required job did not succeed" >&2
+ exit 1
+ fi
+ done
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c336fbc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# Dependencies and build output
+node_modules/
+dist/
+*.local
+
+# C++ / CMake build directories
+wasm/build/
+wasm/build-*/
+
+# Local Emscripten SDK (scripts/build-wasm.sh --install)
+.emsdk/
+
+# Generated wasm artifacts consumed by the frontend (scripts/build-wasm.sh)
+frontend/public/wasm/
+
+# Editor and OS noise
+.vscode/*
+!.vscode/extensions.json
+.idea/
+.DS_Store
+*.log
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c8887b2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 ApfelTeeSaft and WebMetal contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..18f2848
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+# WebMetal
+
+**WebMetal** is a browser-only, backend-free web app for designing custom CPU
+architectures visually - a Scratch-3-inspired block/flow editor where you wire
+up registers, ALUs, memory, and control logic; define your own instruction set;
+write assembly for it; and run or step through programs in a built-in
+emulator/debugger. Everything exports to (and re-imports from) a single JSON
+file, with the visual layout preserved.
+
+## What WebMetal does
+
+1. **Design a CPU visually** - registers, register files, program counter,
+ flags, ALU, memory, instruction decoder, control unit, and comment notes as
+ draggable, connectable, documentable blocks, with live structural
+ validation (unwired inputs, bus-width mismatches, missing components).
+2. **Define its instruction set** - mnemonics, operands, bit-level encodings,
+ flags affected, and execution behavior written as micro-ops, all checked
+ against the designed machine.
+3. **Write assembly for it** - a CodeMirror editor whose highlighting and
+ diagnostics are generated from _your_ ISA, with labels, `.org`/`.word`/
+ `.byte` directives, and inline assemble-on-type errors.
+4. **Run and debug** - a C++->WebAssembly emulation core with step/run/pause,
+ speed presets, breakpoints in the source gutter, register/flag/memory
+ views (editable while stopped), and a trace log.
+5. **Export/import everything** - one versioned `.webmetal.json` document
+ (architecture, ISA, programs, layout, docs) with format migrations, plus
+ localStorage autosave.
+6. **Learn from bundled examples** - three original CPUs in difficulty order
+ (EDU-CORE -> TOY-CPU -> RETRO-8).
+
+## Tech stack
+
+| Layer | Choice |
+| ------------------ | ------------------------------------------------------------ |
+| Frontend | React + TypeScript + Vite (static build, no backend) |
+| Graph editor | React Flow (`@xyflow/react`) |
+| Assembly editor | CodeMirror 6 |
+| Simulation core | C++20 -> WebAssembly (Emscripten + embind, CMake) |
+| State / validation | Zustand / Zod |
+| Tests | Vitest (TS), doctest (C++), shared JSON conformance vectors |
+| CI | GitHub Actions (single required `CI passed` aggregate check) |
+
+The execution model is two-layered: the visual graph compiles to a flat
+machine model, instruction behaviors are micro-op sequences, and the same
+micro-op semantics are implemented twice - a TypeScript reference executor
+and the C++/wasm engine - pinned to each other by shared conformance vectors
+(`conformance/`) that run in Vitest, native CTest, and Node-on-wasm.
+
+## Repository layout
+
+```txt
+frontend/ Vite + React + TypeScript app (UI, editors, assembler, model)
+wasm/ C++ emulator core, built to WebAssembly (CMake + Emscripten)
+examples/ Bundled example CPU projects (.webmetal.json)
+conformance/ Micro-op conformance vectors shared by all executors
+scripts/ Build helpers (wasm build, Node conformance runner)
+.github/ CI workflow
+```
+
+## Building
+
+Requires [Node.js](https://nodejs.org/) > 20. From a clean checkout:
+
+```sh
+cd frontend
+npm ci
+npm run dev # development server with hot reload
+```
+
+The app runs without the engine (design/ISA/assembler work; the Run view
+reports the engine as unavailable). To build the emulator core too:
+
+```sh
+scripts/build-wasm.sh --install # first time: installs pinned emsdk into .emsdk/
+scripts/build-wasm.sh # builds into frontend/public/wasm/
+```
+
+Production build (static files in `frontend/dist/`):
+
+```sh
+cd frontend
+npm run build
+npm run preview # serve dist/ locally to inspect it
+```
+
+Checks (all also run in CI):
+
+```sh
+npm run lint && npm run format:check && npm run typecheck && npm test
+```
+
+Native C++ unit tests (no Emscripten required):
+
+```sh
+cmake -S wasm -B wasm/build-native && cmake --build wasm/build-native --parallel
+ctest --test-dir wasm/build-native --output-on-failure
+```
+
+## Deployment
+
+`dist/` is a plain static site - any web server or object store can host it.
+
+## License
+
+[MIT](LICENSE).
diff --git a/conformance/microop-vectors.json b/conformance/microop-vectors.json
new file mode 100644
index 0000000..57c313f
--- /dev/null
+++ b/conformance/microop-vectors.json
@@ -0,0 +1,556 @@
+{
+ "description": "WebMetal micro-op conformance vectors. Every engine implementation (TypeScript reference, C++/WASM) must pass all vectors. 'expect' blocks are partial: only listed values are compared. Semantics: docs/execution-model.md.",
+ "defaultModel": {
+ "modelVersion": 1,
+ "banks": [{ "name": "R", "width": 8, "count": 4 }],
+ "memories": [{ "name": "MAIN", "size": 256, "width": 8 }],
+ "flags": ["Z", "N", "C"],
+ "pc": { "width": 16 },
+ "programMemory": "MAIN"
+ },
+ "vectors": [
+ {
+ "name": "move const to register, masked to bank width",
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "const", "value": 511 }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [255, 0, 0, 0] } }
+ },
+ {
+ "name": "move register to register via operand-selected index",
+ "setup": { "banks": { "R": [0, 42] } },
+ "run": [
+ {
+ "operands": { "rd": 3, "rs": 1 },
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rd" } },
+ "src": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rs" } }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [0, 42, 0, 42] } }
+ },
+ {
+ "name": "operand value through temp into register",
+ "run": [
+ {
+ "operands": { "imm": 77 },
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "temp", "index": 2 },
+ "src": { "kind": "operand", "field": "imm" }
+ },
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "src": { "kind": "temp", "index": 2 }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [0, 77, 0, 0] } }
+ },
+ {
+ "name": "add without setFlags leaves flags untouched",
+ "setup": { "banks": { "R": [5, 7] }, "flags": { "Z": true, "C": true } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": false
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [12, 7] }, "flags": { "Z": true, "C": true } }
+ },
+ {
+ "name": "add overflow sets C and Z (0xFF + 1 = 0x00)",
+ "setup": { "banks": { "R": [255, 1] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [0, 1] }, "flags": { "Z": true, "N": false, "C": true } }
+ },
+ {
+ "name": "add sets N from the result msb (0x40 + 0x40 = 0x80)",
+ "setup": { "banks": { "R": [64, 64] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [128, 64] }, "flags": { "Z": false, "N": true, "C": false } }
+ },
+ {
+ "name": "sub with no borrow sets C (5 - 3, ARM/6502 convention)",
+ "setup": { "banks": { "R": [5, 3] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "sub",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [2, 3] }, "flags": { "Z": false, "N": false, "C": true } }
+ },
+ {
+ "name": "sub with borrow clears C and wraps (3 - 5 = 0xFE)",
+ "setup": { "banks": { "R": [3, 5] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "sub",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [254, 5] }, "flags": { "Z": false, "N": true, "C": false } }
+ },
+ {
+ "name": "compare pattern: sub into a temp only affects flags",
+ "setup": { "banks": { "R": [9, 9] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "sub",
+ "width": 8,
+ "dst": { "kind": "temp", "index": 0 },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [9, 9] }, "flags": { "Z": true, "N": false, "C": true } }
+ },
+ {
+ "name": "and clears C and can set Z",
+ "setup": { "banks": { "R": [240, 15] }, "flags": { "C": true } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "and",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [0, 15] }, "flags": { "Z": true, "N": false, "C": false } }
+ },
+ {
+ "name": "or and xor produce expected bit patterns",
+ "setup": { "banks": { "R": [12, 10, 12, 10] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "or",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "setFlags": false
+ },
+ {
+ "op": "alu",
+ "fn": "xor",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 2 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 2 } },
+ "b": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 3 } },
+ "setFlags": false
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [14, 10, 6, 10] } }
+ },
+ {
+ "name": "not is unary and masks to width",
+ "setup": { "banks": { "R": [15] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "not",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [240] }, "flags": { "Z": false, "N": true, "C": false } }
+ },
+ {
+ "name": "shl shifts one bit; C receives the old msb",
+ "setup": { "banks": { "R": [129] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "shl",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [2] }, "flags": { "Z": false, "N": false, "C": true } }
+ },
+ {
+ "name": "shr shifts one bit; C receives the old bit 0",
+ "setup": { "banks": { "R": [5] } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "shr",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [2] }, "flags": { "Z": false, "N": false, "C": true } }
+ },
+ {
+ "name": "alu inputs are truncated to the op width before computing",
+ "run": [
+ {
+ "operands": { "big": 260 },
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "a": { "kind": "operand", "field": "big" },
+ "b": { "kind": "const", "value": 0 },
+ "setFlags": true
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [4] }, "flags": { "Z": false, "C": false } }
+ },
+ {
+ "name": "load reads a memory word",
+ "setup": { "memories": { "MAIN": { "16": 99 } } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "load",
+ "memory": "MAIN",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "addr": { "kind": "const", "value": 16 }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [99] } }
+ },
+ {
+ "name": "store writes a memory word, masked to memory width",
+ "run": [
+ {
+ "operands": { "addr": 32 },
+ "microOps": [
+ {
+ "op": "store",
+ "memory": "MAIN",
+ "addr": { "kind": "operand", "field": "addr" },
+ "src": { "kind": "const", "value": 300 }
+ }
+ ]
+ }
+ ],
+ "expect": { "memories": { "MAIN": { "32": 44 } } }
+ },
+ {
+ "name": "setFlag stores (value != 0)",
+ "run": [
+ {
+ "microOps": [
+ { "op": "setFlag", "name": "C", "src": { "kind": "const", "value": 2 } },
+ { "op": "setFlag", "name": "Z", "src": { "kind": "const", "value": 0 } }
+ ]
+ }
+ ],
+ "expect": { "flags": { "C": true, "Z": false } }
+ },
+ {
+ "name": "flag reads back as 0 or 1 through a move",
+ "setup": { "flags": { "N": true } },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "flag", "name": "N" }
+ },
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "src": { "kind": "flag", "name": "Z" }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [1, 0] } }
+ },
+ {
+ "name": "jump sets pc, masked to pc width",
+ "run": [
+ {
+ "microOps": [
+ { "op": "jump", "target": { "kind": "const", "value": 65540 } }
+ ]
+ }
+ ],
+ "expect": { "pc": 4 }
+ },
+ {
+ "name": "branch taken when flag matches ifSet",
+ "setup": { "flags": { "Z": true }, "pc": 10 },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "branch",
+ "flag": "Z",
+ "ifSet": true,
+ "target": { "kind": "const", "value": 200 }
+ }
+ ]
+ }
+ ],
+ "expect": { "pc": 200 }
+ },
+ {
+ "name": "branch not taken leaves pc alone",
+ "setup": { "flags": { "Z": false }, "pc": 10 },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "branch",
+ "flag": "Z",
+ "ifSet": true,
+ "target": { "kind": "const", "value": 200 }
+ }
+ ]
+ }
+ ],
+ "expect": { "pc": 10 }
+ },
+ {
+ "name": "halt stops the sequence and freezes the machine",
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "const", "value": 1 }
+ },
+ { "op": "halt" },
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "const", "value": 9 }
+ }
+ ]
+ },
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 1 } },
+ "src": { "kind": "const", "value": 9 }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [1, 0] }, "halted": true }
+ },
+ {
+ "name": "temps are cleared between sequences",
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "temp", "index": 0 },
+ "src": { "kind": "const", "value": 123 }
+ }
+ ]
+ },
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "temp", "index": 0 }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [0] } }
+ },
+ {
+ "name": "pc can be read as a value source",
+ "setup": { "pc": 513 },
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "pc" }
+ }
+ ]
+ }
+ ],
+ "expect": { "banks": { "R": [1] } }
+ },
+ {
+ "name": "error: unknown register bank",
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "GHOST", "index": { "kind": "literal", "value": 0 } },
+ "src": { "kind": "const", "value": 1 }
+ }
+ ]
+ }
+ ],
+ "expectError": "unknown register bank"
+ },
+ {
+ "name": "error: register index out of range",
+ "run": [
+ {
+ "operands": { "rd": 12 },
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "reg", "bank": "R", "index": { "kind": "operand", "field": "rd" } },
+ "src": { "kind": "const", "value": 1 }
+ }
+ ]
+ }
+ ],
+ "expectError": "out of range"
+ },
+ {
+ "name": "error: memory address out of range",
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "load",
+ "memory": "MAIN",
+ "dst": { "kind": "temp", "index": 0 },
+ "addr": { "kind": "const", "value": 256 }
+ }
+ ]
+ }
+ ],
+ "expectError": "out of range"
+ },
+ {
+ "name": "error: unknown operand field",
+ "run": [
+ {
+ "microOps": [
+ {
+ "op": "move",
+ "dst": { "kind": "temp", "index": 0 },
+ "src": { "kind": "operand", "field": "missing" }
+ }
+ ]
+ }
+ ],
+ "expectError": "unknown operand field"
+ }
+ ]
+}
diff --git a/examples/edu-core/edu-core.webmetal.json b/examples/edu-core/edu-core.webmetal.json
new file mode 100644
index 0000000..76a2aeb
--- /dev/null
+++ b/examples/edu-core/edu-core.webmetal.json
@@ -0,0 +1,525 @@
+{
+ "formatVersion": 3,
+ "metadata": {
+ "name": "EDU-CORE",
+ "author": "WebMetal examples",
+ "description": "A minimal educational CPU: one accumulator, six instructions, and a single 256-word memory. The smallest design that can run a real loop.",
+ "createdAt": "2026-07-17T00:00:00.000Z",
+ "generator": {
+ "app": "WebMetal",
+ "version": "0.12.0"
+ }
+ },
+ "graph": {
+ "nodes": [
+ {
+ "id": "note-title",
+ "type": "comment",
+ "position": {
+ "x": 40,
+ "y": -180
+ },
+ "data": {
+ "name": "NOTE1",
+ "doc": "",
+ "params": {
+ "text": "EDU-CORE - a minimal 8-bit accumulator machine.\n\nFollow a wire from PC1 to MEM to DEC1 to see how an instruction is fetched and decoded. Open the Instruction Set tab to see how each of the six instructions is written as micro-ops, then press Run to watch the demo program count down."
+ }
+ }
+ },
+ {
+ "id": "note-execute",
+ "type": "comment",
+ "position": {
+ "x": 940,
+ "y": 20
+ },
+ "data": {
+ "name": "NOTE2",
+ "doc": "",
+ "params": {
+ "text": "Execute path: the ALU combines ACC with the decoded immediate. Results latch back into ACC; Z and N latch into FLAGS1, where JNZ can test them."
+ }
+ }
+ },
+ {
+ "id": "acc",
+ "type": "register",
+ "position": {
+ "x": 40,
+ "y": 40
+ },
+ "data": {
+ "name": "ACC",
+ "doc": "The accumulator - the machine’s only general-purpose register. Every ALU result lands here, and STA writes it to memory.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "alu1",
+ "type": "alu",
+ "position": {
+ "x": 340,
+ "y": 40
+ },
+ "data": {
+ "name": "ALU1",
+ "doc": "Performs ADD and SUB on the accumulator and the instruction’s immediate operand, and reports Zero/Negative to the flags block.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "flags1",
+ "type": "flags",
+ "position": {
+ "x": 640,
+ "y": 40
+ },
+ "data": {
+ "name": "FLAGS1",
+ "doc": "Two status flags: Z (last result was zero) and N (top bit of the last result). JNZ tests Z.",
+ "params": {
+ "flags": "Z,N"
+ }
+ }
+ },
+ {
+ "id": "pc1",
+ "type": "pc",
+ "position": {
+ "x": 40,
+ "y": 320
+ },
+ "data": {
+ "name": "PC1",
+ "doc": "Holds the address of the next instruction word. 8 bits wide because MEM has 256 words (2⁸). The control unit increments it past each instruction; JNZ can load it with a branch target instead.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "mem",
+ "type": "memory",
+ "position": {
+ "x": 340,
+ "y": 320
+ },
+ "data": {
+ "name": "MEM",
+ "doc": "Unified program + data memory: 256 words of 8 bits. Instructions are fetched from here, and STA writes the accumulator back into it.",
+ "params": {
+ "size": 256,
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "dec1",
+ "type": "decoder",
+ "position": {
+ "x": 640,
+ "y": 320
+ },
+ "data": {
+ "name": "DEC1",
+ "doc": "Splits a fetched instruction into its opcode (word 0) and its immediate/address operand (word 1).",
+ "params": {}
+ }
+ },
+ {
+ "id": "ctl1",
+ "type": "control",
+ "position": {
+ "x": 940,
+ "y": 320
+ },
+ "data": {
+ "name": "CTL1",
+ "doc": "Sequences fetch -> decode -> execute: asserts the load, increment, latch, and write signals that make the datapath move.",
+ "params": {}
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "pc1.out->mem.addr",
+ "source": "pc1",
+ "sourceHandle": "out",
+ "target": "mem",
+ "targetHandle": "addr",
+ "kind": "data"
+ },
+ {
+ "id": "mem.dout->dec1.instr",
+ "source": "mem",
+ "sourceHandle": "dout",
+ "target": "dec1",
+ "targetHandle": "instr",
+ "kind": "data"
+ },
+ {
+ "id": "acc.out->alu1.a",
+ "source": "acc",
+ "sourceHandle": "out",
+ "target": "alu1",
+ "targetHandle": "a",
+ "kind": "data"
+ },
+ {
+ "id": "dec1.imm->alu1.b",
+ "source": "dec1",
+ "sourceHandle": "imm",
+ "target": "alu1",
+ "targetHandle": "b",
+ "kind": "data"
+ },
+ {
+ "id": "alu1.result->acc.in",
+ "source": "alu1",
+ "sourceHandle": "result",
+ "target": "acc",
+ "targetHandle": "in",
+ "kind": "data"
+ },
+ {
+ "id": "alu1.flags->flags1.in",
+ "source": "alu1",
+ "sourceHandle": "flags",
+ "target": "flags1",
+ "targetHandle": "in",
+ "kind": "data"
+ },
+ {
+ "id": "acc.out->mem.din",
+ "source": "acc",
+ "sourceHandle": "out",
+ "target": "mem",
+ "targetHandle": "din",
+ "kind": "data"
+ },
+ {
+ "id": "dec1.imm->pc1.next",
+ "source": "dec1",
+ "sourceHandle": "imm",
+ "target": "pc1",
+ "targetHandle": "next",
+ "kind": "data"
+ },
+ {
+ "id": "flags1.out->ctl1.flags",
+ "source": "flags1",
+ "sourceHandle": "out",
+ "target": "ctl1",
+ "targetHandle": "flags",
+ "kind": "data"
+ },
+ {
+ "id": "dec1.ctl->ctl1.decoded",
+ "source": "dec1",
+ "sourceHandle": "ctl",
+ "target": "ctl1",
+ "targetHandle": "decoded",
+ "kind": "control"
+ },
+ {
+ "id": "dec1.ctl->alu1.op",
+ "source": "dec1",
+ "sourceHandle": "ctl",
+ "target": "alu1",
+ "targetHandle": "op",
+ "kind": "control"
+ },
+ {
+ "id": "ctl1.signals->acc.load",
+ "source": "ctl1",
+ "sourceHandle": "signals",
+ "target": "acc",
+ "targetHandle": "load",
+ "kind": "control"
+ },
+ {
+ "id": "ctl1.signals->pc1.inc",
+ "source": "ctl1",
+ "sourceHandle": "signals",
+ "target": "pc1",
+ "targetHandle": "inc",
+ "kind": "control"
+ },
+ {
+ "id": "ctl1.signals->pc1.load",
+ "source": "ctl1",
+ "sourceHandle": "signals",
+ "target": "pc1",
+ "targetHandle": "load",
+ "kind": "control"
+ },
+ {
+ "id": "ctl1.signals->flags1.latch",
+ "source": "ctl1",
+ "sourceHandle": "signals",
+ "target": "flags1",
+ "targetHandle": "latch",
+ "kind": "control"
+ },
+ {
+ "id": "ctl1.signals->mem.write",
+ "source": "ctl1",
+ "sourceHandle": "signals",
+ "target": "mem",
+ "targetHandle": "write",
+ "kind": "control"
+ },
+ {
+ "id": "ctl1.signals->mem.read",
+ "source": "ctl1",
+ "sourceHandle": "signals",
+ "target": "mem",
+ "targetHandle": "read",
+ "kind": "control"
+ }
+ ],
+ "viewport": {
+ "x": 30,
+ "y": 210,
+ "zoom": 0.8
+ }
+ },
+ "isa": {
+ "description": "EDU-CORE: six instructions around a single accumulator. The opcode fills word 0; the operand (if any) fills word 1.",
+ "opcodeField": {
+ "offset": 0,
+ "width": 8
+ },
+ "instructions": [
+ {
+ "id": "edu-hlt",
+ "mnemonic": "HLT",
+ "opcode": 0,
+ "words": 1,
+ "operands": [],
+ "flagsAffected": [],
+ "doc": "Stop the machine.",
+ "microOps": [
+ {
+ "op": "halt"
+ }
+ ]
+ },
+ {
+ "id": "edu-ldi",
+ "mnemonic": "LDI",
+ "opcode": 1,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Load an immediate value into the accumulator.",
+ "microOps": [
+ {
+ "op": "move",
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "src": {
+ "kind": "operand",
+ "field": "value"
+ }
+ }
+ ]
+ },
+ {
+ "id": "edu-add",
+ "mnemonic": "ADD",
+ "opcode": 2,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N"
+ ],
+ "doc": "Add an immediate to the accumulator.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "b": {
+ "kind": "operand",
+ "field": "value"
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "edu-sub",
+ "mnemonic": "SUB",
+ "opcode": 3,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N"
+ ],
+ "doc": "Subtract an immediate from the accumulator.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "sub",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "b": {
+ "kind": "operand",
+ "field": "value"
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "edu-sta",
+ "mnemonic": "STA",
+ "opcode": 4,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Store the accumulator to a memory address.",
+ "microOps": [
+ {
+ "op": "store",
+ "memory": "MEM",
+ "addr": {
+ "kind": "operand",
+ "field": "target"
+ },
+ "src": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "edu-jnz",
+ "mnemonic": "JNZ",
+ "opcode": 5,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Jump to an address when the Z flag is clear (last result was not zero).",
+ "microOps": [
+ {
+ "op": "branch",
+ "flag": "Z",
+ "ifSet": false,
+ "target": {
+ "kind": "operand",
+ "field": "target"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "programs": [
+ {
+ "id": "edu-demo",
+ "name": "countdown",
+ "source": "; EDU-CORE demo - count down from 3.\n; Each pass stores the counter to address 32 (0x20),\n; so you can watch MEM[0x20] change in the Run view.\n\n LDI #3 ; ACC = 3\nloop:\n STA 32 ; MEM[32] = ACC\n SUB #1 ; ACC = ACC - 1, sets Z when it reaches 0\n JNZ loop ; repeat until ACC == 0\n HLT ; done: MEM[32] ends at 1, ACC at 0\n"
+ }
+ ]
+}
diff --git a/examples/retro-8/retro-8.webmetal.json b/examples/retro-8/retro-8.webmetal.json
new file mode 100644
index 0000000..f70a032
--- /dev/null
+++ b/examples/retro-8/retro-8.webmetal.json
@@ -0,0 +1,860 @@
+{
+ "formatVersion": 3,
+ "metadata": {
+ "name": "RETRO-8",
+ "author": "WebMetal examples",
+ "description": "An original 8-bit accumulator machine in the spirit of late-70s home-computer CPUs: Z/N/C flags, immediate, absolute, and X-indexed addressing.",
+ "createdAt": "2026-07-17T00:00:00.000Z",
+ "generator": {
+ "app": "WebMetal",
+ "version": "0.12.0"
+ }
+ },
+ "graph": {
+ "nodes": [
+ {
+ "id": "note-title",
+ "type": "comment",
+ "position": {
+ "x": 40,
+ "y": -190
+ },
+ "data": {
+ "name": "NOTE1",
+ "doc": "",
+ "params": {
+ "text": "RETRO-8 - an original 8-bit accumulator machine with three addressing modes.\n\nImmediate (LDI #5), absolute (LDA 96), and X-indexed (STAX 96 stores to 96 + X). The X register plus INX turn straight-line code into loops over arrays - the demo fills five memory cells that way."
+ }
+ }
+ },
+ {
+ "id": "note-indexed",
+ "type": "comment",
+ "position": {
+ "x": 940,
+ "y": 110
+ },
+ "data": {
+ "name": "NOTE2",
+ "doc": "",
+ "params": {
+ "text": "Indexed addressing: STAX adds the X register to the instruction’s address operand inside the ALU before the store - the classic base + index trick."
+ }
+ }
+ },
+ {
+ "id": "acc",
+ "type": "register",
+ "position": {
+ "x": 40,
+ "y": 20
+ },
+ "data": {
+ "name": "ACC",
+ "doc": "The accumulator: source and destination of all arithmetic, and the value stored by STA/STAX.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "x",
+ "type": "register",
+ "position": {
+ "x": 40,
+ "y": 200
+ },
+ "data": {
+ "name": "X",
+ "doc": "The index register. STAX adds it to the address operand; INX steps it through an array.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "alu",
+ "type": "alu",
+ "position": {
+ "x": 340,
+ "y": 110
+ },
+ "data": {
+ "name": "ALU",
+ "doc": "Adds/subtracts the accumulator with an immediate or a memory operand, and computes indexed addresses for STAX.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "flags",
+ "type": "flags",
+ "position": {
+ "x": 640,
+ "y": 110
+ },
+ "data": {
+ "name": "FLAGS",
+ "doc": "Z (zero), N (negative), C (carry / no-borrow). CMP subtracts without writing ACC, just to set these for BEQ/BNE.",
+ "params": {
+ "flags": "Z,N,C"
+ }
+ }
+ },
+ {
+ "id": "pc",
+ "type": "pc",
+ "position": {
+ "x": 40,
+ "y": 440
+ },
+ "data": {
+ "name": "PC",
+ "doc": "8-bit program counter addressing MEM’s 256 words. Branches and JMP load it with a target address.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "mem",
+ "type": "memory",
+ "position": {
+ "x": 340,
+ "y": 440
+ },
+ "data": {
+ "name": "MEM",
+ "doc": "Unified program + data memory: 256 × 8-bit words. The demo writes its output table at addresses 96-100.",
+ "params": {
+ "size": 256,
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "dec",
+ "type": "decoder",
+ "position": {
+ "x": 640,
+ "y": 440
+ },
+ "data": {
+ "name": "DEC",
+ "doc": "Splits fetched words into the opcode and the immediate/address operand.",
+ "params": {}
+ }
+ },
+ {
+ "id": "ctl",
+ "type": "control",
+ "position": {
+ "x": 940,
+ "y": 440
+ },
+ "data": {
+ "name": "CTL",
+ "doc": "Turns decoded instructions and flag state into register loads, memory read/write strobes, and PC updates.",
+ "params": {}
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "pc.out->mem.addr",
+ "source": "pc",
+ "sourceHandle": "out",
+ "target": "mem",
+ "targetHandle": "addr",
+ "kind": "data"
+ },
+ {
+ "id": "mem.dout->dec.instr",
+ "source": "mem",
+ "sourceHandle": "dout",
+ "target": "dec",
+ "targetHandle": "instr",
+ "kind": "data"
+ },
+ {
+ "id": "acc.out->alu.a",
+ "source": "acc",
+ "sourceHandle": "out",
+ "target": "alu",
+ "targetHandle": "a",
+ "kind": "data"
+ },
+ {
+ "id": "x.out->alu.a",
+ "source": "x",
+ "sourceHandle": "out",
+ "target": "alu",
+ "targetHandle": "a",
+ "kind": "data"
+ },
+ {
+ "id": "mem.dout->alu.b",
+ "source": "mem",
+ "sourceHandle": "dout",
+ "target": "alu",
+ "targetHandle": "b",
+ "kind": "data"
+ },
+ {
+ "id": "dec.imm->alu.b",
+ "source": "dec",
+ "sourceHandle": "imm",
+ "target": "alu",
+ "targetHandle": "b",
+ "kind": "data"
+ },
+ {
+ "id": "alu.result->acc.in",
+ "source": "alu",
+ "sourceHandle": "result",
+ "target": "acc",
+ "targetHandle": "in",
+ "kind": "data"
+ },
+ {
+ "id": "alu.result->x.in",
+ "source": "alu",
+ "sourceHandle": "result",
+ "target": "x",
+ "targetHandle": "in",
+ "kind": "data"
+ },
+ {
+ "id": "alu.flags->flags.in",
+ "source": "alu",
+ "sourceHandle": "flags",
+ "target": "flags",
+ "targetHandle": "in",
+ "kind": "data"
+ },
+ {
+ "id": "acc.out->mem.din",
+ "source": "acc",
+ "sourceHandle": "out",
+ "target": "mem",
+ "targetHandle": "din",
+ "kind": "data"
+ },
+ {
+ "id": "dec.imm->pc.next",
+ "source": "dec",
+ "sourceHandle": "imm",
+ "target": "pc",
+ "targetHandle": "next",
+ "kind": "data"
+ },
+ {
+ "id": "flags.out->ctl.flags",
+ "source": "flags",
+ "sourceHandle": "out",
+ "target": "ctl",
+ "targetHandle": "flags",
+ "kind": "data"
+ },
+ {
+ "id": "dec.ctl->ctl.decoded",
+ "source": "dec",
+ "sourceHandle": "ctl",
+ "target": "ctl",
+ "targetHandle": "decoded",
+ "kind": "control"
+ },
+ {
+ "id": "dec.ctl->alu.op",
+ "source": "dec",
+ "sourceHandle": "ctl",
+ "target": "alu",
+ "targetHandle": "op",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->acc.load",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "acc",
+ "targetHandle": "load",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->x.load",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "x",
+ "targetHandle": "load",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->pc.inc",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "pc",
+ "targetHandle": "inc",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->pc.load",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "pc",
+ "targetHandle": "load",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->flags.latch",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "flags",
+ "targetHandle": "latch",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->mem.write",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "mem",
+ "targetHandle": "write",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->mem.read",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "mem",
+ "targetHandle": "read",
+ "kind": "control"
+ }
+ ],
+ "viewport": {
+ "x": 30,
+ "y": 215,
+ "zoom": 0.75
+ }
+ },
+ "isa": {
+ "description": "RETRO-8: accumulator-implicit instructions with immediate (#n), absolute (n), and X-indexed (STAX) addressing. Opcode in word 0, operand in word 1.",
+ "opcodeField": {
+ "offset": 0,
+ "width": 8
+ },
+ "instructions": [
+ {
+ "id": "r8-hlt",
+ "mnemonic": "HLT",
+ "opcode": 0,
+ "words": 1,
+ "operands": [],
+ "flagsAffected": [],
+ "doc": "Stop the machine.",
+ "microOps": [
+ {
+ "op": "halt"
+ }
+ ]
+ },
+ {
+ "id": "r8-ldi",
+ "mnemonic": "LDI",
+ "opcode": 1,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Load an immediate into the accumulator.",
+ "microOps": [
+ {
+ "op": "move",
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "src": {
+ "kind": "operand",
+ "field": "value"
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-ldx",
+ "mnemonic": "LDX",
+ "opcode": 2,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Load an immediate into the X index register.",
+ "microOps": [
+ {
+ "op": "move",
+ "dst": {
+ "kind": "reg",
+ "bank": "X",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "src": {
+ "kind": "operand",
+ "field": "value"
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-lda",
+ "mnemonic": "LDA",
+ "opcode": 3,
+ "words": 2,
+ "operands": [
+ {
+ "name": "source",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Load the accumulator from a memory address (absolute addressing).",
+ "microOps": [
+ {
+ "op": "load",
+ "memory": "MEM",
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "addr": {
+ "kind": "operand",
+ "field": "source"
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-sta",
+ "mnemonic": "STA",
+ "opcode": 4,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Store the accumulator to a memory address (absolute addressing).",
+ "microOps": [
+ {
+ "op": "store",
+ "memory": "MEM",
+ "addr": {
+ "kind": "operand",
+ "field": "target"
+ },
+ "src": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-stax",
+ "mnemonic": "STAX",
+ "opcode": 5,
+ "words": 2,
+ "operands": [
+ {
+ "name": "base",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Store the accumulator to base + X (indexed addressing).",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": {
+ "kind": "temp",
+ "index": 0
+ },
+ "a": {
+ "kind": "operand",
+ "field": "base"
+ },
+ "b": {
+ "kind": "reg",
+ "bank": "X",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "setFlags": false
+ },
+ {
+ "op": "store",
+ "memory": "MEM",
+ "addr": {
+ "kind": "temp",
+ "index": 0
+ },
+ "src": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-addi",
+ "mnemonic": "ADDI",
+ "opcode": 6,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N",
+ "C"
+ ],
+ "doc": "Add an immediate to the accumulator.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "b": {
+ "kind": "operand",
+ "field": "value"
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "r8-add",
+ "mnemonic": "ADD",
+ "opcode": 7,
+ "words": 2,
+ "operands": [
+ {
+ "name": "source",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N",
+ "C"
+ ],
+ "doc": "Add the value at a memory address to the accumulator.",
+ "microOps": [
+ {
+ "op": "load",
+ "memory": "MEM",
+ "dst": {
+ "kind": "temp",
+ "index": 0
+ },
+ "addr": {
+ "kind": "operand",
+ "field": "source"
+ }
+ },
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "b": {
+ "kind": "temp",
+ "index": 0
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "r8-cmp",
+ "mnemonic": "CMP",
+ "opcode": 8,
+ "words": 2,
+ "operands": [
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N",
+ "C"
+ ],
+ "doc": "Compare: set flags from ACC - value without changing the accumulator.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "sub",
+ "width": 8,
+ "dst": {
+ "kind": "temp",
+ "index": 1
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "ACC",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "b": {
+ "kind": "operand",
+ "field": "value"
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "r8-beq",
+ "mnemonic": "BEQ",
+ "opcode": 9,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Branch when the Z flag is set (last result was zero / compare matched).",
+ "microOps": [
+ {
+ "op": "branch",
+ "flag": "Z",
+ "ifSet": true,
+ "target": {
+ "kind": "operand",
+ "field": "target"
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-bne",
+ "mnemonic": "BNE",
+ "opcode": 10,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Branch when the Z flag is clear.",
+ "microOps": [
+ {
+ "op": "branch",
+ "flag": "Z",
+ "ifSet": false,
+ "target": {
+ "kind": "operand",
+ "field": "target"
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-jmp",
+ "mnemonic": "JMP",
+ "opcode": 11,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Jump unconditionally.",
+ "microOps": [
+ {
+ "op": "jump",
+ "target": {
+ "kind": "operand",
+ "field": "target"
+ }
+ }
+ ]
+ },
+ {
+ "id": "r8-inx",
+ "mnemonic": "INX",
+ "opcode": 12,
+ "words": 1,
+ "operands": [],
+ "flagsAffected": [],
+ "doc": "Increment the X index register.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "X",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "X",
+ "index": {
+ "kind": "literal",
+ "value": 0
+ }
+ },
+ "b": {
+ "kind": "const",
+ "value": 1
+ },
+ "setFlags": false
+ }
+ ]
+ }
+ ]
+ },
+ "programs": [
+ {
+ "id": "r8-demo",
+ "name": "times table",
+ "source": "; RETRO-8 demo - fill MEM[96..100] with 10,20,30,40,50\n; using X-indexed stores.\n\n LDX #0 ; X = table index\n LDI #0 ; ACC = running value\nloop:\n ADDI #10 ; next multiple of ten\n STAX 96 ; MEM[96 + X] = ACC\n INX\n CMP #50 ; reached the last entry?\n BNE loop\n HLT ; MEM[96..100] = 10,20,30,40,50\n"
+ }
+ ]
+}
diff --git a/examples/toy-cpu/toy-cpu.webmetal.json b/examples/toy-cpu/toy-cpu.webmetal.json
new file mode 100644
index 0000000..d4d44fc
--- /dev/null
+++ b/examples/toy-cpu/toy-cpu.webmetal.json
@@ -0,0 +1,661 @@
+{
+ "formatVersion": 3,
+ "metadata": {
+ "name": "TOY-CPU",
+ "author": "WebMetal examples",
+ "description": "A four-register machine with register-to-register arithmetic, load/store, and branching - one step up from EDU-CORE.",
+ "createdAt": "2026-07-17T00:00:00.000Z",
+ "generator": {
+ "app": "WebMetal",
+ "version": "0.12.0"
+ }
+ },
+ "graph": {
+ "nodes": [
+ {
+ "id": "note-title",
+ "type": "comment",
+ "position": {
+ "x": 40,
+ "y": -180
+ },
+ "data": {
+ "name": "NOTE1",
+ "doc": "",
+ "params": {
+ "text": "TOY-CPU - four general registers (R0-R3) instead of a single accumulator.\n\nArithmetic works register-to-register (ADD R0, R1), so the register file has two read ports feeding the ALU. LDR/STR move data between registers and memory; JMP/JNZ redirect the program counter."
+ }
+ }
+ },
+ {
+ "id": "note-regfile",
+ "type": "comment",
+ "position": {
+ "x": 940,
+ "y": 20
+ },
+ "data": {
+ "name": "NOTE2",
+ "doc": "",
+ "params": {
+ "text": "The register file’s Read A port feeds both the ALU and the memory write path (STR stores a register). Write-back always comes from the ALU result bus."
+ }
+ }
+ },
+ {
+ "id": "rf",
+ "type": "registerFile",
+ "position": {
+ "x": 40,
+ "y": 40
+ },
+ "data": {
+ "name": "R",
+ "doc": "Four 8-bit general-purpose registers, R0-R3. Read ports A/B feed the ALU; the write port takes ALU results.",
+ "params": {
+ "count": 4,
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "alu",
+ "type": "alu",
+ "position": {
+ "x": 340,
+ "y": 40
+ },
+ "data": {
+ "name": "ALU",
+ "doc": "Adds or subtracts two registers and reports Z/N/C to the flags block.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "flags",
+ "type": "flags",
+ "position": {
+ "x": 640,
+ "y": 40
+ },
+ "data": {
+ "name": "FLAGS",
+ "doc": "Z (zero), N (negative), C (carry / no-borrow). JNZ tests Z.",
+ "params": {
+ "flags": "Z,N,C"
+ }
+ }
+ },
+ {
+ "id": "pc",
+ "type": "pc",
+ "position": {
+ "x": 40,
+ "y": 320
+ },
+ "data": {
+ "name": "PC",
+ "doc": "8-bit program counter addressing MAIN’s 256 words. JMP and JNZ load it with a target address.",
+ "params": {
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "main",
+ "type": "memory",
+ "position": {
+ "x": 340,
+ "y": 320
+ },
+ "data": {
+ "name": "MAIN",
+ "doc": "Unified program + data memory: 256 × 8-bit words. LDR/STR read and write it below the program.",
+ "params": {
+ "size": 256,
+ "width": 8
+ }
+ }
+ },
+ {
+ "id": "dec",
+ "type": "decoder",
+ "position": {
+ "x": 640,
+ "y": 320
+ },
+ "data": {
+ "name": "DEC",
+ "doc": "Splits fetched words into opcode, register selectors, and the immediate/address operand.",
+ "params": {}
+ }
+ },
+ {
+ "id": "ctl",
+ "type": "control",
+ "position": {
+ "x": 940,
+ "y": 320
+ },
+ "data": {
+ "name": "CTL",
+ "doc": "Drives the register-file write enable, memory read/write, PC increment/load, and flag latch signals.",
+ "params": {}
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "pc.out->main.addr",
+ "source": "pc",
+ "sourceHandle": "out",
+ "target": "main",
+ "targetHandle": "addr",
+ "kind": "data"
+ },
+ {
+ "id": "main.dout->dec.instr",
+ "source": "main",
+ "sourceHandle": "dout",
+ "target": "dec",
+ "targetHandle": "instr",
+ "kind": "data"
+ },
+ {
+ "id": "rf.ra->alu.a",
+ "source": "rf",
+ "sourceHandle": "ra",
+ "target": "alu",
+ "targetHandle": "a",
+ "kind": "data"
+ },
+ {
+ "id": "rf.rb->alu.b",
+ "source": "rf",
+ "sourceHandle": "rb",
+ "target": "alu",
+ "targetHandle": "b",
+ "kind": "data"
+ },
+ {
+ "id": "alu.result->rf.wdata",
+ "source": "alu",
+ "sourceHandle": "result",
+ "target": "rf",
+ "targetHandle": "wdata",
+ "kind": "data"
+ },
+ {
+ "id": "alu.flags->flags.in",
+ "source": "alu",
+ "sourceHandle": "flags",
+ "target": "flags",
+ "targetHandle": "in",
+ "kind": "data"
+ },
+ {
+ "id": "rf.ra->main.din",
+ "source": "rf",
+ "sourceHandle": "ra",
+ "target": "main",
+ "targetHandle": "din",
+ "kind": "data"
+ },
+ {
+ "id": "dec.imm->pc.next",
+ "source": "dec",
+ "sourceHandle": "imm",
+ "target": "pc",
+ "targetHandle": "next",
+ "kind": "data"
+ },
+ {
+ "id": "flags.out->ctl.flags",
+ "source": "flags",
+ "sourceHandle": "out",
+ "target": "ctl",
+ "targetHandle": "flags",
+ "kind": "data"
+ },
+ {
+ "id": "dec.ctl->ctl.decoded",
+ "source": "dec",
+ "sourceHandle": "ctl",
+ "target": "ctl",
+ "targetHandle": "decoded",
+ "kind": "control"
+ },
+ {
+ "id": "dec.ctl->alu.op",
+ "source": "dec",
+ "sourceHandle": "ctl",
+ "target": "alu",
+ "targetHandle": "op",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->rf.wen",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "rf",
+ "targetHandle": "wen",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->rf.sel",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "rf",
+ "targetHandle": "sel",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->pc.inc",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "pc",
+ "targetHandle": "inc",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->pc.load",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "pc",
+ "targetHandle": "load",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->flags.latch",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "flags",
+ "targetHandle": "latch",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->main.write",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "main",
+ "targetHandle": "write",
+ "kind": "control"
+ },
+ {
+ "id": "ctl.signals->main.read",
+ "source": "ctl",
+ "sourceHandle": "signals",
+ "target": "main",
+ "targetHandle": "read",
+ "kind": "control"
+ }
+ ],
+ "viewport": {
+ "x": 30,
+ "y": 210,
+ "zoom": 0.8
+ }
+ },
+ "isa": {
+ "description": "TOY-CPU: register-to-register arithmetic over four registers. Register selectors live in the top bits of word 1; addresses take a full word.",
+ "opcodeField": {
+ "offset": 0,
+ "width": 8
+ },
+ "instructions": [
+ {
+ "id": "toy-hlt",
+ "mnemonic": "HLT",
+ "opcode": 0,
+ "words": 1,
+ "operands": [],
+ "flagsAffected": [],
+ "doc": "Stop the machine.",
+ "microOps": [
+ {
+ "op": "halt"
+ }
+ ]
+ },
+ {
+ "id": "toy-ldi",
+ "mnemonic": "LDI",
+ "opcode": 1,
+ "words": 2,
+ "operands": [
+ {
+ "name": "rd",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 6,
+ "width": 2
+ }
+ },
+ {
+ "name": "value",
+ "kind": "immediate",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 6
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Load a small immediate (0-63) into register rd.",
+ "microOps": [
+ {
+ "op": "move",
+ "dst": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rd"
+ }
+ },
+ "src": {
+ "kind": "operand",
+ "field": "value"
+ }
+ }
+ ]
+ },
+ {
+ "id": "toy-add",
+ "mnemonic": "ADD",
+ "opcode": 2,
+ "words": 2,
+ "operands": [
+ {
+ "name": "rd",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 6,
+ "width": 2
+ }
+ },
+ {
+ "name": "rs",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 4,
+ "width": 2
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N",
+ "C"
+ ],
+ "doc": "rd = rd + rs.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "add",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rd"
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rd"
+ }
+ },
+ "b": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rs"
+ }
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "toy-sub",
+ "mnemonic": "SUB",
+ "opcode": 3,
+ "words": 2,
+ "operands": [
+ {
+ "name": "rd",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 6,
+ "width": 2
+ }
+ },
+ {
+ "name": "rs",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 4,
+ "width": 2
+ }
+ }
+ ],
+ "flagsAffected": [
+ "Z",
+ "N",
+ "C"
+ ],
+ "doc": "rd = rd - rs.",
+ "microOps": [
+ {
+ "op": "alu",
+ "fn": "sub",
+ "width": 8,
+ "dst": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rd"
+ }
+ },
+ "a": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rd"
+ }
+ },
+ "b": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rs"
+ }
+ },
+ "setFlags": true
+ }
+ ]
+ },
+ {
+ "id": "toy-ldr",
+ "mnemonic": "LDR",
+ "opcode": 4,
+ "words": 3,
+ "operands": [
+ {
+ "name": "rd",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 6,
+ "width": 2
+ }
+ },
+ {
+ "name": "source",
+ "kind": "address",
+ "field": {
+ "word": 2,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Load rd from a memory address.",
+ "microOps": [
+ {
+ "op": "load",
+ "memory": "MAIN",
+ "dst": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rd"
+ }
+ },
+ "addr": {
+ "kind": "operand",
+ "field": "source"
+ }
+ }
+ ]
+ },
+ {
+ "id": "toy-str",
+ "mnemonic": "STR",
+ "opcode": 5,
+ "words": 3,
+ "operands": [
+ {
+ "name": "rs",
+ "kind": "register",
+ "bank": "R",
+ "field": {
+ "word": 1,
+ "offset": 6,
+ "width": 2
+ }
+ },
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 2,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Store rs to a memory address.",
+ "microOps": [
+ {
+ "op": "store",
+ "memory": "MAIN",
+ "addr": {
+ "kind": "operand",
+ "field": "target"
+ },
+ "src": {
+ "kind": "reg",
+ "bank": "R",
+ "index": {
+ "kind": "operand",
+ "field": "rs"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "toy-jmp",
+ "mnemonic": "JMP",
+ "opcode": 6,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Jump unconditionally.",
+ "microOps": [
+ {
+ "op": "jump",
+ "target": {
+ "kind": "operand",
+ "field": "target"
+ }
+ }
+ ]
+ },
+ {
+ "id": "toy-jnz",
+ "mnemonic": "JNZ",
+ "opcode": 7,
+ "words": 2,
+ "operands": [
+ {
+ "name": "target",
+ "kind": "address",
+ "field": {
+ "word": 1,
+ "offset": 0,
+ "width": 8
+ }
+ }
+ ],
+ "flagsAffected": [],
+ "doc": "Jump when the Z flag is clear (last result was not zero).",
+ "microOps": [
+ {
+ "op": "branch",
+ "flag": "Z",
+ "ifSet": false,
+ "target": {
+ "kind": "operand",
+ "field": "target"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "programs": [
+ {
+ "id": "toy-demo",
+ "name": "sum 5..1",
+ "source": "; TOY-CPU demo - sum the numbers 5..1 into R0.\n; R1 counts down; R2 holds the constant 1.\n\n LDI R0, #0 ; running total\n LDI R1, #5 ; loop counter\n LDI R2, #1 ; decrement amount\nloop:\n ADD R0, R1 ; total += counter\n SUB R1, R2 ; counter -= 1, sets Z at zero\n JNZ loop\n STR R0, 64 ; MEM[64] = 15 (0x0F)\n HLT\n"
+ }
+ ]
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/frontend/.oxlintrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000..14f3087
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+package-lock.json
+# Emscripten build output (scripts/build-wasm.sh)
+public/wasm/
diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json
new file mode 100644
index 0000000..b2095be
--- /dev/null
+++ b/frontend/.prettierrc.json
@@ -0,0 +1,4 @@
+{
+ "semi": false,
+ "singleQuote": true
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..0cbc25e
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ WebMetal
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..c5b2908
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2170 @@
+{
+ "name": "webmetal-frontend",
+ "version": "0.14.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "webmetal-frontend",
+ "version": "0.14.0",
+ "dependencies": {
+ "@codemirror/commands": "^6.10.4",
+ "@codemirror/language": "^6.12.4",
+ "@codemirror/lint": "^6.9.7",
+ "@codemirror/state": "^6.7.1",
+ "@codemirror/view": "^6.43.6",
+ "@lezer/highlight": "^1.2.3",
+ "@xyflow/react": "^12.11.2",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "zod": "^4.4.3",
+ "zustand": "^5.0.14"
+ },
+ "devDependencies": {
+ "@types/node": "^24.13.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.3",
+ "oxlint": "^1.71.0",
+ "prettier": "^3.9.4",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1",
+ "vitest": "^4.1.10"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.10.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
+ "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.7.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.12.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
+ "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
+ "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.42.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
+ "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.43.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
+ "integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.7.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
+ "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
+ "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
+ "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
+ "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.138.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
+ "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
+ "integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz",
+ "integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz",
+ "integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz",
+ "integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz",
+ "integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz",
+ "integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz",
+ "integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz",
+ "integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz",
+ "integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz",
+ "integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz",
+ "integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz",
+ "integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz",
+ "integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz",
+ "integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz",
+ "integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz",
+ "integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz",
+ "integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz",
+ "integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz",
+ "integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
+ "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
+ "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
+ "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
+ "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
+ "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
+ "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
+ "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
+ "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
+ "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
+ "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
+ "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
+ "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
+ "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
+ "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
+ "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
+ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
+ "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
+ "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
+ "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.10",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
+ "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
+ "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.10",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
+ "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
+ "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
+ "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.10",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@xyflow/react": {
+ "version": "12.11.2",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz",
+ "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.79",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=17",
+ "@types/react-dom": ">=17",
+ "react": ">=17",
+ "react-dom": ">=17"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/react/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.79",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz",
+ "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/crelt": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
+ "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
+ "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/oxlint": {
+ "version": "1.73.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
+ "integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "oxlint": "bin/oxlint"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.73.0",
+ "@oxlint/binding-android-arm64": "1.73.0",
+ "@oxlint/binding-darwin-arm64": "1.73.0",
+ "@oxlint/binding-darwin-x64": "1.73.0",
+ "@oxlint/binding-freebsd-x64": "1.73.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.73.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.73.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.73.0",
+ "@oxlint/binding-linux-arm64-musl": "1.73.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.73.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.73.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.73.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.73.0",
+ "@oxlint/binding-linux-x64-gnu": "1.73.0",
+ "@oxlint/binding-linux-x64-musl": "1.73.0",
+ "@oxlint/binding-openharmony-arm64": "1.73.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.73.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.73.0",
+ "@oxlint/binding-win32-x64-msvc": "1.73.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=0.24.0",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
+ "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
+ "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.138.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.4",
+ "@rolldown/binding-darwin-arm64": "1.1.4",
+ "@rolldown/binding-darwin-x64": "1.1.4",
+ "@rolldown/binding-freebsd-x64": "1.1.4",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.4",
+ "@rolldown/binding-linux-arm64-musl": "1.1.4",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.4",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.4",
+ "@rolldown/binding-linux-x64-gnu": "1.1.4",
+ "@rolldown/binding-linux-x64-musl": "1.1.4",
+ "@rolldown/binding-openharmony-arm64": "1.1.4",
+ "@rolldown/binding-wasm32-wasi": "1.1.4",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.4",
+ "@rolldown/binding-win32-x64-msvc": "1.1.4"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
+ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
+ "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.16",
+ "rolldown": "~1.1.3",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
+ "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.10",
+ "@vitest/mocker": "4.1.10",
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/runner": "4.1.10",
+ "@vitest/snapshot": "4.1.10",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.10",
+ "@vitest/browser-preview": "4.1.10",
+ "@vitest/browser-webdriverio": "4.1.10",
+ "@vitest/coverage-istanbul": "4.1.10",
+ "@vitest/coverage-v8": "4.1.10",
+ "@vitest/ui": "4.1.10",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..879e32f
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "webmetal-frontend",
+ "private": true,
+ "version": "0.14.0",
+ "type": "module",
+ "engines": {
+ "node": ">=20"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "lint": "oxlint",
+ "typecheck": "tsc -b",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "format": "prettier --write .",
+ "format:check": "prettier --check ."
+ },
+ "dependencies": {
+ "@codemirror/commands": "^6.10.4",
+ "@codemirror/language": "^6.12.4",
+ "@codemirror/lint": "^6.9.7",
+ "@codemirror/state": "^6.7.1",
+ "@codemirror/view": "^6.43.6",
+ "@lezer/highlight": "^1.2.3",
+ "@xyflow/react": "^12.11.2",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "zod": "^4.4.3",
+ "zustand": "^5.0.14"
+ },
+ "devDependencies": {
+ "@types/node": "^24.13.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.3",
+ "oxlint": "^1.71.0",
+ "prettier": "^3.9.4",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1",
+ "vitest": "^4.1.10"
+ }
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..6b5293a
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1,11 @@
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..fc5a638
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,7 @@
+import { AppShell } from './app/AppShell'
+
+function App() {
+ return
+}
+
+export default App
diff --git a/frontend/src/app/AboutDialog.tsx b/frontend/src/app/AboutDialog.tsx
new file mode 100644
index 0000000..ba38ed5
--- /dev/null
+++ b/frontend/src/app/AboutDialog.tsx
@@ -0,0 +1,59 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { loadWasmEngine } from '../emulator/wasmLoader'
+import { Button } from '../ui/Button'
+import { APP_NAME, APP_TAGLINE, appTitle } from './meta'
+import { useAppStore } from './store'
+
+/** Help/about dialog (native