From cafc87e8ad7be191ba03ffd940d9c5154c9f86e7 Mon Sep 17 00:00:00 2001
From: ApfelTeeSaft <91074565+ApfelTeeSaft@users.noreply.github.com>
Date: Sun, 19 Jul 2026 18:05:04 +0200
Subject: [PATCH] Release
---
.editorconfig | 15 +
.github/workflows/ci.yml | 141 +
.gitignore | 21 +
LICENSE | 21 +
README.md | 105 +
conformance/microop-vectors.json | 556 +
examples/edu-core/edu-core.webmetal.json | 525 +
examples/retro-8/retro-8.webmetal.json | 860 +
examples/toy-cpu/toy-cpu.webmetal.json | 661 +
frontend/.gitignore | 24 +
frontend/.oxlintrc.json | 8 +
frontend/.prettierignore | 5 +
frontend/.prettierrc.json | 4 +
frontend/index.html | 33 +
frontend/package-lock.json | 2170 ++
frontend/package.json | 44 +
frontend/public/favicon.svg | 11 +
frontend/src/App.tsx | 7 +
frontend/src/app/AboutDialog.tsx | 59 +
frontend/src/app/AppShell.tsx | 65 +
frontend/src/app/HelpDialog.tsx | 129 +
frontend/src/app/Toolbar.tsx | 205 +
frontend/src/app/meta.test.ts | 13 +
frontend/src/app/meta.ts | 11 +
frontend/src/app/panels.tsx | 221 +
frontend/src/app/shell.css | 330 +
frontend/src/app/shortcuts.ts | 100 +
frontend/src/app/store.test.ts | 51 +
frontend/src/app/store.ts | 120 +
frontend/src/app/theme.test.ts | 36 +
frontend/src/app/theme.ts | 58 +
frontend/src/app/usePanelResize.ts | 59 +
frontend/src/asm/AsmEditor.tsx | 188 +
frontend/src/asm/AsmPanel.tsx | 227 +
frontend/src/asm/asm.css | 177 +
frontend/src/asm/asmLanguage.test.ts | 64 +
frontend/src/asm/asmLanguage.ts | 110 +
frontend/src/asm/assembler.test.ts | 252 +
frontend/src/asm/assembler.ts | 450 +
frontend/src/asm/debugExtensions.ts | 91 +
frontend/src/asm/editorNav.ts | 31 +
frontend/src/asm/programStore.test.ts | 75 +
frontend/src/asm/programStore.ts | 104 +
frontend/src/asm/syntax.test.ts | 42 +
frontend/src/asm/syntax.ts | 40 +
frontend/src/editor/BlockNode.tsx | 98 +
frontend/src/editor/GraphCanvas.tsx | 147 +
frontend/src/editor/Inspector.tsx | 205 +
frontend/src/editor/Toolbox.tsx | 72 +
frontend/src/editor/editor.css | 386 +
frontend/src/editor/graphRules.test.ts | 87 +
frontend/src/editor/graphRules.ts | 60 +
frontend/src/editor/graphStore.test.ts | 105 +
frontend/src/editor/graphStore.ts | 213 +
frontend/src/editor/nodeTypes.test.ts | 66 +
frontend/src/editor/nodeTypes.ts | 285 +
frontend/src/emulator/DebuggerView.tsx | 375 +
frontend/src/emulator/debugStore.ts | 377 +
frontend/src/emulator/debugUtils.test.ts | 70 +
frontend/src/emulator/debugUtils.ts | 75 +
frontend/src/emulator/debugger.css | 321 +
frontend/src/emulator/engine.ts | 49 +
frontend/src/emulator/wasmLoader.ts | 107 +
frontend/src/examples/ExamplesDialog.tsx | 85 +
frontend/src/examples/examples.css | 69 +
frontend/src/examples/examples.test.ts | 123 +
frontend/src/examples/index.ts | 45 +
frontend/src/globals.d.ts | 1 +
frontend/src/index.css | 99 +
frontend/src/isa/InstructionEditor.tsx | 293 +
frontend/src/isa/IsaDesigner.tsx | 182 +
frontend/src/isa/MicroOpEditor.tsx | 567 +
frontend/src/isa/encoding.test.ts | 74 +
frontend/src/isa/encoding.ts | 58 +
frontend/src/isa/isa.css | 376 +
frontend/src/isa/isaModel.ts | 91 +
frontend/src/isa/isaStore.test.ts | 91 +
frontend/src/isa/isaStore.ts | 188 +
frontend/src/isa/microOpDefaults.ts | 50 +
frontend/src/isa/validateIsa.test.ts | 219 +
frontend/src/isa/validateIsa.ts | 302 +
frontend/src/machine/compileGraph.test.ts | 124 +
frontend/src/machine/compileGraph.ts | 155 +
frontend/src/machine/conformance.test.ts | 22 +
frontend/src/machine/conformance.ts | 147 +
frontend/src/machine/cycleReference.ts | 89 +
frontend/src/machine/interpret.test.ts | 124 +
frontend/src/machine/interpret.ts | 298 +
frontend/src/machine/machineModel.ts | 50 +
frontend/src/machine/microOps.ts | 112 +
frontend/src/machine/validateGraph.test.ts | 126 +
frontend/src/machine/validateGraph.ts | Bin 0 -> 4813 bytes
frontend/src/main.tsx | 13 +
frontend/src/model/fileIO.ts | 42 +
frontend/src/model/history.test.ts | 154 +
frontend/src/model/history.ts | 100 +
frontend/src/model/migrations.test.ts | 92 +
frontend/src/model/migrations.ts | 60 +
frontend/src/model/persistence.test.ts | 78 +
frontend/src/model/persistence.ts | 90 +
frontend/src/model/projectSchema.test.ts | 123 +
frontend/src/model/projectSchema.ts | 122 +
frontend/src/model/serialize.test.ts | 166 +
frontend/src/model/serialize.ts | 157 +
frontend/src/ui/Button.tsx | 16 +
frontend/src/ui/PanelHeader.tsx | 17 +
frontend/src/ui/Tabs.tsx | 33 +
frontend/src/ui/icons.tsx | 97 +
frontend/src/ui/ui.css | 114 +
frontend/tsconfig.app.json | 28 +
frontend/tsconfig.json | 7 +
frontend/tsconfig.node.json | 24 +
frontend/vite.config.ts | 30 +
scripts/build-wasm.sh | 42 +
scripts/run-conformance-node.mjs | 110 +
wasm/CMakeLists.txt | 59 +
wasm/include/webmetal/engine.h | 130 +
wasm/include/webmetal/ir.h | 129 +
wasm/src/bindings.cpp | 28 +
wasm/src/engine.cpp | 603 +
wasm/src/ir.cpp | 354 +
wasm/tests/conformance_test.cpp | 211 +
wasm/tests/doctest.h | 7134 ++++++
wasm/tests/engine_test.cpp | 207 +
wasm/third_party/nlohmann/json.hpp | 25526 +++++++++++++++++++
125 files changed, 51873 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .github/workflows/ci.yml
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 conformance/microop-vectors.json
create mode 100644 examples/edu-core/edu-core.webmetal.json
create mode 100644 examples/retro-8/retro-8.webmetal.json
create mode 100644 examples/toy-cpu/toy-cpu.webmetal.json
create mode 100644 frontend/.gitignore
create mode 100644 frontend/.oxlintrc.json
create mode 100644 frontend/.prettierignore
create mode 100644 frontend/.prettierrc.json
create mode 100644 frontend/index.html
create mode 100644 frontend/package-lock.json
create mode 100644 frontend/package.json
create mode 100644 frontend/public/favicon.svg
create mode 100644 frontend/src/App.tsx
create mode 100644 frontend/src/app/AboutDialog.tsx
create mode 100644 frontend/src/app/AppShell.tsx
create mode 100644 frontend/src/app/HelpDialog.tsx
create mode 100644 frontend/src/app/Toolbar.tsx
create mode 100644 frontend/src/app/meta.test.ts
create mode 100644 frontend/src/app/meta.ts
create mode 100644 frontend/src/app/panels.tsx
create mode 100644 frontend/src/app/shell.css
create mode 100644 frontend/src/app/shortcuts.ts
create mode 100644 frontend/src/app/store.test.ts
create mode 100644 frontend/src/app/store.ts
create mode 100644 frontend/src/app/theme.test.ts
create mode 100644 frontend/src/app/theme.ts
create mode 100644 frontend/src/app/usePanelResize.ts
create mode 100644 frontend/src/asm/AsmEditor.tsx
create mode 100644 frontend/src/asm/AsmPanel.tsx
create mode 100644 frontend/src/asm/asm.css
create mode 100644 frontend/src/asm/asmLanguage.test.ts
create mode 100644 frontend/src/asm/asmLanguage.ts
create mode 100644 frontend/src/asm/assembler.test.ts
create mode 100644 frontend/src/asm/assembler.ts
create mode 100644 frontend/src/asm/debugExtensions.ts
create mode 100644 frontend/src/asm/editorNav.ts
create mode 100644 frontend/src/asm/programStore.test.ts
create mode 100644 frontend/src/asm/programStore.ts
create mode 100644 frontend/src/asm/syntax.test.ts
create mode 100644 frontend/src/asm/syntax.ts
create mode 100644 frontend/src/editor/BlockNode.tsx
create mode 100644 frontend/src/editor/GraphCanvas.tsx
create mode 100644 frontend/src/editor/Inspector.tsx
create mode 100644 frontend/src/editor/Toolbox.tsx
create mode 100644 frontend/src/editor/editor.css
create mode 100644 frontend/src/editor/graphRules.test.ts
create mode 100644 frontend/src/editor/graphRules.ts
create mode 100644 frontend/src/editor/graphStore.test.ts
create mode 100644 frontend/src/editor/graphStore.ts
create mode 100644 frontend/src/editor/nodeTypes.test.ts
create mode 100644 frontend/src/editor/nodeTypes.ts
create mode 100644 frontend/src/emulator/DebuggerView.tsx
create mode 100644 frontend/src/emulator/debugStore.ts
create mode 100644 frontend/src/emulator/debugUtils.test.ts
create mode 100644 frontend/src/emulator/debugUtils.ts
create mode 100644 frontend/src/emulator/debugger.css
create mode 100644 frontend/src/emulator/engine.ts
create mode 100644 frontend/src/emulator/wasmLoader.ts
create mode 100644 frontend/src/examples/ExamplesDialog.tsx
create mode 100644 frontend/src/examples/examples.css
create mode 100644 frontend/src/examples/examples.test.ts
create mode 100644 frontend/src/examples/index.ts
create mode 100644 frontend/src/globals.d.ts
create mode 100644 frontend/src/index.css
create mode 100644 frontend/src/isa/InstructionEditor.tsx
create mode 100644 frontend/src/isa/IsaDesigner.tsx
create mode 100644 frontend/src/isa/MicroOpEditor.tsx
create mode 100644 frontend/src/isa/encoding.test.ts
create mode 100644 frontend/src/isa/encoding.ts
create mode 100644 frontend/src/isa/isa.css
create mode 100644 frontend/src/isa/isaModel.ts
create mode 100644 frontend/src/isa/isaStore.test.ts
create mode 100644 frontend/src/isa/isaStore.ts
create mode 100644 frontend/src/isa/microOpDefaults.ts
create mode 100644 frontend/src/isa/validateIsa.test.ts
create mode 100644 frontend/src/isa/validateIsa.ts
create mode 100644 frontend/src/machine/compileGraph.test.ts
create mode 100644 frontend/src/machine/compileGraph.ts
create mode 100644 frontend/src/machine/conformance.test.ts
create mode 100644 frontend/src/machine/conformance.ts
create mode 100644 frontend/src/machine/cycleReference.ts
create mode 100644 frontend/src/machine/interpret.test.ts
create mode 100644 frontend/src/machine/interpret.ts
create mode 100644 frontend/src/machine/machineModel.ts
create mode 100644 frontend/src/machine/microOps.ts
create mode 100644 frontend/src/machine/validateGraph.test.ts
create mode 100644 frontend/src/machine/validateGraph.ts
create mode 100644 frontend/src/main.tsx
create mode 100644 frontend/src/model/fileIO.ts
create mode 100644 frontend/src/model/history.test.ts
create mode 100644 frontend/src/model/history.ts
create mode 100644 frontend/src/model/migrations.test.ts
create mode 100644 frontend/src/model/migrations.ts
create mode 100644 frontend/src/model/persistence.test.ts
create mode 100644 frontend/src/model/persistence.ts
create mode 100644 frontend/src/model/projectSchema.test.ts
create mode 100644 frontend/src/model/projectSchema.ts
create mode 100644 frontend/src/model/serialize.test.ts
create mode 100644 frontend/src/model/serialize.ts
create mode 100644 frontend/src/ui/Button.tsx
create mode 100644 frontend/src/ui/PanelHeader.tsx
create mode 100644 frontend/src/ui/Tabs.tsx
create mode 100644 frontend/src/ui/icons.tsx
create mode 100644 frontend/src/ui/ui.css
create mode 100644 frontend/tsconfig.app.json
create mode 100644 frontend/tsconfig.json
create mode 100644 frontend/tsconfig.node.json
create mode 100644 frontend/vite.config.ts
create mode 100755 scripts/build-wasm.sh
create mode 100644 scripts/run-conformance-node.mjs
create mode 100644 wasm/CMakeLists.txt
create mode 100644 wasm/include/webmetal/engine.h
create mode 100644 wasm/include/webmetal/ir.h
create mode 100644 wasm/src/bindings.cpp
create mode 100644 wasm/src/engine.cpp
create mode 100644 wasm/src/ir.cpp
create mode 100644 wasm/tests/conformance_test.cpp
create mode 100644 wasm/tests/doctest.h
create mode 100644 wasm/tests/engine_test.cpp
create mode 100644 wasm/third_party/nlohmann/json.hpp
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