From 4caede2f79032ecdbb6b5875dff3a7c2831068bb Mon Sep 17 00:00:00 2001
From: fireheart3911 <64578352+fireheart3911@users.noreply.github.com>
Date: Sun, 15 Mar 2026 23:36:35 +0100
Subject: [PATCH 1/3] feat(website). #vibecoding
---
.github/workflows/deploy.yml | 35 +
.gitignore | 145 +-
README.md | 34 +-
eslint.config.js | 23 +
index.html | 13 +
package-lock.json | 3362 +++++++++++++++++
package.json | 36 +
src/App.css | 6 +
src/App.tsx | 14 +
src/components/canvas/PrerequisiteEdge.tsx | 31 +
src/components/canvas/ResearchNode.css | 66 +
src/components/canvas/ResearchNode.tsx | 30 +
src/components/canvas/ResearchTreeCanvas.css | 26 +
src/components/canvas/ResearchTreeCanvas.tsx | 156 +
src/components/common/Autocomplete.css | 33 +
src/components/common/ConfirmDialog.tsx | 22 +
src/components/common/CopyButton.tsx | 23 +
src/components/common/FluidAutocomplete.tsx | 90 +
src/components/common/ItemAutocomplete.tsx | 91 +
src/components/common/Modal.css | 40 +
src/components/common/Modal.tsx | 30 +
src/components/common/TierBadge.tsx | 27 +
src/components/dialogs/ExportDialog.tsx | 81 +
src/components/dialogs/ImportDialog.tsx | 100 +
src/components/dialogs/ValidationDialog.tsx | 98 +
src/components/editors/BasicInfoSection.tsx | 75 +
.../editors/DriveCraftingShaped.tsx | 130 +
.../editors/DriveCraftingShapeless.tsx | 57 +
src/components/editors/DurationInput.tsx | 36 +
src/components/editors/EditorPanel.css | 97 +
src/components/editors/FluidCostEditor.tsx | 74 +
src/components/editors/IdeaChipEditor.tsx | 112 +
src/components/editors/ItemCostsEditor.tsx | 56 +
src/components/editors/PrerequisiteEditor.tsx | 61 +
src/components/editors/ProcessingEditor.tsx | 163 +
src/components/editors/RecipeEditorPanel.tsx | 139 +
src/components/editors/RecipePoolEditor.tsx | 102 +
src/components/editors/RecipeResultEditor.tsx | 30 +
.../editors/ResearchEditorPanel.tsx | 71 +
src/components/editors/ShapedGrid.css | 38 +
src/components/layout/AppHeader.css | 28 +
src/components/layout/AppHeader.tsx | 52 +
src/components/layout/MainLayout.css | 18 +
src/components/layout/MainLayout.tsx | 38 +
src/data/categories.ts | 10 +
src/data/exampleTree.ts | 67 +
src/data/fluids.ts | 8 +
src/data/items.ts | 168 +
src/data/tiers.ts | 77 +
src/index.css | 161 +
src/main.tsx | 10 +
src/stores/recipeStore.ts | 54 +
src/stores/researchStore.ts | 130 +
src/stores/uiStore.ts | 67 +
src/types/common.ts | 26 +
src/types/recipe.ts | 67 +
src/types/research.ts | 57 +
src/types/validation.ts | 10 +
src/utils/exportZip.ts | 45 +
src/utils/idHelpers.ts | 29 +
src/utils/importJson.ts | 133 +
src/utils/prerequisites.ts | 83 +
src/utils/serialization.ts | 129 +
src/utils/tickFormatter.ts | 18 +
src/utils/validation.ts | 148 +
tsconfig.app.json | 28 +
tsconfig.json | 7 +
tsconfig.node.json | 26 +
vite.config.ts | 7 +
69 files changed, 7422 insertions(+), 132 deletions(-)
create mode 100644 .github/workflows/deploy.yml
create mode 100644 eslint.config.js
create mode 100644 index.html
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 src/App.css
create mode 100644 src/App.tsx
create mode 100644 src/components/canvas/PrerequisiteEdge.tsx
create mode 100644 src/components/canvas/ResearchNode.css
create mode 100644 src/components/canvas/ResearchNode.tsx
create mode 100644 src/components/canvas/ResearchTreeCanvas.css
create mode 100644 src/components/canvas/ResearchTreeCanvas.tsx
create mode 100644 src/components/common/Autocomplete.css
create mode 100644 src/components/common/ConfirmDialog.tsx
create mode 100644 src/components/common/CopyButton.tsx
create mode 100644 src/components/common/FluidAutocomplete.tsx
create mode 100644 src/components/common/ItemAutocomplete.tsx
create mode 100644 src/components/common/Modal.css
create mode 100644 src/components/common/Modal.tsx
create mode 100644 src/components/common/TierBadge.tsx
create mode 100644 src/components/dialogs/ExportDialog.tsx
create mode 100644 src/components/dialogs/ImportDialog.tsx
create mode 100644 src/components/dialogs/ValidationDialog.tsx
create mode 100644 src/components/editors/BasicInfoSection.tsx
create mode 100644 src/components/editors/DriveCraftingShaped.tsx
create mode 100644 src/components/editors/DriveCraftingShapeless.tsx
create mode 100644 src/components/editors/DurationInput.tsx
create mode 100644 src/components/editors/EditorPanel.css
create mode 100644 src/components/editors/FluidCostEditor.tsx
create mode 100644 src/components/editors/IdeaChipEditor.tsx
create mode 100644 src/components/editors/ItemCostsEditor.tsx
create mode 100644 src/components/editors/PrerequisiteEditor.tsx
create mode 100644 src/components/editors/ProcessingEditor.tsx
create mode 100644 src/components/editors/RecipeEditorPanel.tsx
create mode 100644 src/components/editors/RecipePoolEditor.tsx
create mode 100644 src/components/editors/RecipeResultEditor.tsx
create mode 100644 src/components/editors/ResearchEditorPanel.tsx
create mode 100644 src/components/editors/ShapedGrid.css
create mode 100644 src/components/layout/AppHeader.css
create mode 100644 src/components/layout/AppHeader.tsx
create mode 100644 src/components/layout/MainLayout.css
create mode 100644 src/components/layout/MainLayout.tsx
create mode 100644 src/data/categories.ts
create mode 100644 src/data/exampleTree.ts
create mode 100644 src/data/fluids.ts
create mode 100644 src/data/items.ts
create mode 100644 src/data/tiers.ts
create mode 100644 src/index.css
create mode 100644 src/main.tsx
create mode 100644 src/stores/recipeStore.ts
create mode 100644 src/stores/researchStore.ts
create mode 100644 src/stores/uiStore.ts
create mode 100644 src/types/common.ts
create mode 100644 src/types/recipe.ts
create mode 100644 src/types/research.ts
create mode 100644 src/types/validation.ts
create mode 100644 src/utils/exportZip.ts
create mode 100644 src/utils/idHelpers.ts
create mode 100644 src/utils/importJson.ts
create mode 100644 src/utils/prerequisites.ts
create mode 100644 src/utils/serialization.ts
create mode 100644 src/utils/tickFormatter.ts
create mode 100644 src/utils/validation.ts
create mode 100644 tsconfig.app.json
create mode 100644 tsconfig.json
create mode 100644 tsconfig.node.json
create mode 100644 vite.config.ts
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..9eb5588
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,35 @@
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches: [main]
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ - run: npm ci
+ - run: npm run build
+ - uses: actions/configure-pages@v4
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+ - id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index 9a5aced..a547bf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,136 +4,21 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+pnpm-debug.log*
lerna-debug.log*
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# Snowpack dependency directory (https://snowpack.dev/)
-web_modules/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Optional stylelint cache
-.stylelintcache
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variable files
-.env
-.env.*
-!.env.example
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-.parcel-cache
-
-# Next.js build output
-.next
-out
-
-# Nuxt.js build / generate output
-.nuxt
+node_modules
dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and not Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# vuepress v2.x temp and cache directory
-.temp
-.cache
-
-# Sveltekit cache directory
-.svelte-kit/
-
-# vitepress build output
-**/.vitepress/dist
-
-# vitepress cache directory
-**/.vitepress/cache
-
-# Docusaurus cache and generated files
-.docusaurus
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# Firebase cache directory
-.firebase/
-
-# TernJS port file
-.tern-port
-
-# Stores VSCode versions used for testing VSCode extensions
-.vscode-test
-
-# yarn v3
-.pnp.*
-.yarn/*
-!.yarn/patches
-!.yarn/plugins
-!.yarn/releases
-!.yarn/sdks
-!.yarn/versions
-
-# Vite logs files
-vite.config.js.timestamp-*
-vite.config.ts.timestamp-*
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/README.md b/README.md
index bcbc125..f1cd613 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,32 @@
-# ResearchCubeDatapackCreator
-Datapack creator for the ResearchCube mod
+# ResearchCube Datapack Creator
+
+A visual web tool for designing research trees and exporting datapacks for the [ResearchCube](https://github.com/researchcube) Minecraft mod.
+
+## Features
+
+- Visual node graph editor (React Flow) for research tree design
+- Research node editor with tier, duration, item/fluid costs, prerequisites, idea chips
+- Recipe editor for Drive Crafting (shapeless/shaped) and Processing Station recipes
+- AND/OR prerequisite logic with edge-based connections
+- Full validation engine (cycle detection, tier-fluid matching, slot limits)
+- ZIP export with correct datapack folder structure
+- JSON/ZIP import for editing existing datapacks
+- 23-node example tree included
+- Dark theme, localStorage auto-save
+
+## Development
+
+```bash
+npm install
+npm run dev
+```
+
+## Build
+
+```bash
+npm run build
+```
+
+## Deployment
+
+Automatically deployed to GitHub Pages via GitHub Actions on push to `main`.
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e90215e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ ResearchCube Datapack Creator
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..449408e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3362 @@
+{
+ "name": "temp-scaffold",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "temp-scaffold",
+ "version": "0.0.0",
+ "dependencies": {
+ "@xyflow/react": "^12.10.1",
+ "file-saver": "^2.0.5",
+ "immer": "^11.1.4",
+ "jszip": "^3.10.1",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "zustand": "^5.0.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/file-saver": "^2.0.7",
+ "@types/node": "^24.12.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.0",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.56.1",
+ "vite": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
+ "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
+ "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.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/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/runtime": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
+ "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
+ "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
+ "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
+ "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
+ "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.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
+ "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.0-rc.7",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "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/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/file-saver": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
+ "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.12.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
+ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
+ "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/type-utils": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.57.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
+ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
+ "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.57.0",
+ "@typescript-eslint/types": "^8.57.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
+ "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
+ "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
+ "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
+ "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
+ "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.57.0",
+ "@typescript-eslint/tsconfig-utils": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/visitor-keys": "8.57.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
+ "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.57.0",
+ "@typescript-eslint/types": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
+ "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "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/@xyflow/react": {
+ "version": "12.10.1",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz",
+ "integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.75",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "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.75",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz",
+ "integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==",
+ "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/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.8",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001779",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz",
+ "integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "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/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "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/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "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/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/electron-to-chromium": {
+ "version": "1.5.313",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz",
+ "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
+ "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
+ "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "node_modules/immer": {
+ "version": "11.1.4",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
+ "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "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/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
+ "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.115.0",
+ "@rolldown/pluginutils": "1.0.0-rc.9"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.9",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
+ "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "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/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "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/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz",
+ "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.57.0",
+ "@typescript-eslint/parser": "8.57.0",
+ "@typescript-eslint/typescript-estree": "8.57.0",
+ "@typescript-eslint/utils": "8.57.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "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/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
+ "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/runtime": "0.115.0",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.9",
+ "tinyglobby": "^0.2.15"
+ },
+ "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.0.0-alpha.31",
+ "esbuild": "^0.27.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/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.11",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
+ "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
+ "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/package.json b/package.json
new file mode 100644
index 0000000..e57e008
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "researchcube-datapack-creator",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@xyflow/react": "^12.10.1",
+ "file-saver": "^2.0.5",
+ "immer": "^11.1.4",
+ "jszip": "^3.10.1",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "zustand": "^5.0.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/file-saver": "^2.0.7",
+ "@types/node": "^24.12.0",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.0",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.56.1",
+ "vite": "^8.0.0"
+ }
+}
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..1e5098d
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,6 @@
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..1b02958
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,14 @@
+import './App.css'
+import MainLayout from './components/layout/MainLayout'
+import AppHeader from './components/layout/AppHeader'
+
+function App() {
+ return (
+
+ )
+}
+
+export default App
diff --git a/src/components/canvas/PrerequisiteEdge.tsx b/src/components/canvas/PrerequisiteEdge.tsx
new file mode 100644
index 0000000..19a2885
--- /dev/null
+++ b/src/components/canvas/PrerequisiteEdge.tsx
@@ -0,0 +1,31 @@
+import { memo } from 'react';
+import { BaseEdge, getSmoothStepPath, type EdgeProps } from '@xyflow/react';
+
+function PrerequisiteEdgeComponent(props: EdgeProps) {
+ const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data } = props;
+ const mode = data?.mode as string | null;
+
+ const [edgePath] = getSmoothStepPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ sourcePosition,
+ targetPosition,
+ borderRadius: 8,
+ });
+
+ return (
+
+ );
+}
+
+export default memo(PrerequisiteEdgeComponent);
diff --git a/src/components/canvas/ResearchNode.css b/src/components/canvas/ResearchNode.css
new file mode 100644
index 0000000..32e053a
--- /dev/null
+++ b/src/components/canvas/ResearchNode.css
@@ -0,0 +1,66 @@
+.research-node {
+ min-width: 160px;
+ max-width: 200px;
+ border-radius: var(--radius-md);
+ border: 2px solid var(--border);
+ background: var(--bg-secondary);
+ overflow: hidden;
+ transition: border-color 0.15s, box-shadow 0.15s;
+ cursor: pointer;
+}
+
+.research-node.selected {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 2px rgba(91, 106, 191, 0.3);
+}
+
+.research-node-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 3px 8px;
+ color: #fff;
+}
+
+.research-node-tier {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+}
+
+.research-node-chip-badge {
+ font-size: 9px;
+ font-weight: 700;
+ background: rgba(255, 255, 255, 0.25);
+ padding: 1px 4px;
+ border-radius: 3px;
+}
+
+.research-node-body {
+ padding: 8px 10px;
+}
+
+.research-node-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+ word-break: break-word;
+}
+
+.research-node-category {
+ font-size: 11px;
+ color: var(--text-muted);
+ margin-top: 2px;
+}
+
+.research-handle {
+ width: 8px !important;
+ height: 8px !important;
+ background: var(--border) !important;
+ border: 2px solid var(--bg-secondary) !important;
+}
+
+.research-handle:hover {
+ background: var(--accent) !important;
+}
diff --git a/src/components/canvas/ResearchNode.tsx b/src/components/canvas/ResearchNode.tsx
new file mode 100644
index 0000000..c8d1d18
--- /dev/null
+++ b/src/components/canvas/ResearchNode.tsx
@@ -0,0 +1,30 @@
+import { memo } from 'react';
+import { Handle, Position, type NodeProps } from '@xyflow/react';
+import type { ResearchNodeData } from '../../types/research';
+import { TIER_INFO } from '../../data/tiers';
+import './ResearchNode.css';
+
+function ResearchNodeComponent({ data, selected }: NodeProps & { data: ResearchNodeData }) {
+ const nodeData = data as unknown as ResearchNodeData;
+ const tierInfo = TIER_INFO[nodeData.tier];
+ const displayName = nodeData.name || nodeData.id;
+
+ return (
+
+
+
+ {nodeData.tier}
+ {nodeData.ideaChip && IC}
+
+
+
{displayName}
+ {nodeData.category && (
+
{nodeData.category}
+ )}
+
+
+
+ );
+}
+
+export default memo(ResearchNodeComponent);
diff --git a/src/components/canvas/ResearchTreeCanvas.css b/src/components/canvas/ResearchTreeCanvas.css
new file mode 100644
index 0000000..9c30ad2
--- /dev/null
+++ b/src/components/canvas/ResearchTreeCanvas.css
@@ -0,0 +1,26 @@
+.research-tree-canvas {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+.canvas-add-btn {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ z-index: 10;
+ font-size: 13px;
+ padding: 6px 14px;
+ box-shadow: var(--shadow);
+}
+
+/* Override React Flow dark theme colors */
+.react-flow__controls button {
+ background: var(--bg-tertiary) !important;
+ border-color: var(--border) !important;
+ fill: var(--text-secondary) !important;
+}
+
+.react-flow__controls button:hover {
+ background: var(--bg-hover) !important;
+}
diff --git a/src/components/canvas/ResearchTreeCanvas.tsx b/src/components/canvas/ResearchTreeCanvas.tsx
new file mode 100644
index 0000000..b711139
--- /dev/null
+++ b/src/components/canvas/ResearchTreeCanvas.tsx
@@ -0,0 +1,156 @@
+import { useCallback, useMemo } from 'react';
+import {
+ ReactFlow,
+ Background,
+ Controls,
+ MiniMap,
+ type Node,
+ type NodeTypes,
+ type EdgeTypes,
+ type OnNodesChange,
+ type OnConnect,
+ type OnEdgesDelete,
+ type Connection,
+ BackgroundVariant,
+} from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import { useResearchStore } from '../../stores/researchStore';
+import { useUIStore } from '../../stores/uiStore';
+import { deriveEdges } from '../../utils/prerequisites';
+import { generateUniqueId } from '../../utils/idHelpers';
+import { createDefaultResearchNode } from '../../types/research';
+import ResearchNodeComponent from './ResearchNode';
+import PrerequisiteEdge from './PrerequisiteEdge';
+import { TIER_INFO } from '../../data/tiers';
+import './ResearchTreeCanvas.css';
+
+const nodeTypes: NodeTypes = {
+ research: ResearchNodeComponent as unknown as NodeTypes['research'],
+};
+
+const edgeTypes: EdgeTypes = {
+ prerequisite: PrerequisiteEdge,
+};
+
+export default function ResearchTreeCanvas() {
+ const allNodes = useResearchStore((s) => s.nodes);
+ const updateNodePosition = useResearchStore((s) => s.updateNodePosition);
+ const addNode = useResearchStore((s) => s.addNode);
+ const removeNode = useResearchStore((s) => s.removeNode);
+ const addPrerequisite = useResearchStore((s) => s.addPrerequisite);
+ const removePrerequisite = useResearchStore((s) => s.removePrerequisite);
+ const selectNode = useUIStore((s) => s.selectNode);
+ const selectedNodeId = useUIStore((s) => s.selectedNodeId);
+
+ const flowNodes = useMemo(() => {
+ return Object.values(allNodes).map((node) => ({
+ id: node.id,
+ type: 'research' as const,
+ position: node.position,
+ data: node as unknown as Record,
+ selected: node.id === selectedNodeId,
+ }));
+ }, [allNodes, selectedNodeId]);
+
+ const flowEdges = useMemo(() => deriveEdges(allNodes), [allNodes]);
+
+ const onNodesChange: OnNodesChange = useCallback(
+ (changes) => {
+ // Apply position changes to store
+ for (const change of changes) {
+ if (change.type === 'position' && change.position) {
+ updateNodePosition(change.id, change.position);
+ }
+ if (change.type === 'remove') {
+ removeNode(change.id);
+ selectNode(null);
+ }
+ }
+ },
+ [updateNodePosition, removeNode, selectNode]
+ );
+
+ const onConnect: OnConnect = useCallback(
+ (connection: Connection) => {
+ if (connection.source && connection.target) {
+ addPrerequisite(connection.target, connection.source);
+ }
+ },
+ [addPrerequisite]
+ );
+
+ const onEdgesDelete: OnEdgesDelete = useCallback(
+ (edges) => {
+ for (const edge of edges) {
+ removePrerequisite(edge.target, edge.source);
+ }
+ },
+ [removePrerequisite]
+ );
+
+ const onNodeClick = useCallback(
+ (_: React.MouseEvent, node: Node) => {
+ selectNode(node.id);
+ },
+ [selectNode]
+ );
+
+ const onPaneClick = useCallback(() => {
+ selectNode(null);
+ }, [selectNode]);
+
+ const handleAddNode = useCallback(() => {
+ const existingIds = new Set(Object.keys(allNodes));
+ const id = generateUniqueId('new_research', existingIds);
+ const node = createDefaultResearchNode(id, {
+ x: 200 + Math.random() * 200,
+ y: 200 + Math.random() * 200,
+ });
+ addNode(node);
+ selectNode(id);
+ }, [allNodes, addNode, selectNode]);
+
+ return (
+
+
+
+
+ {
+ const data = node.data as unknown as { tier?: string };
+ const tier = data?.tier;
+ if (tier && tier in TIER_INFO) {
+ return TIER_INFO[tier as keyof typeof TIER_INFO].color;
+ }
+ return 'var(--border)';
+ }}
+ style={{
+ background: 'var(--bg-secondary)',
+ border: '1px solid var(--border)',
+ }}
+ />
+
+
+
+ );
+}
diff --git a/src/components/common/Autocomplete.css b/src/components/common/Autocomplete.css
new file mode 100644
index 0000000..b5a54c8
--- /dev/null
+++ b/src/components/common/Autocomplete.css
@@ -0,0 +1,33 @@
+.autocomplete-wrapper {
+ position: relative;
+ width: 100%;
+}
+
+.autocomplete-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ max-height: 200px;
+ overflow-y: auto;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-top: none;
+ border-radius: 0 0 var(--radius-sm) var(--radius-sm);
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ z-index: 100;
+}
+
+.autocomplete-dropdown li {
+ padding: 5px 10px;
+ font-size: 13px;
+ cursor: pointer;
+ color: var(--text-secondary);
+}
+
+.autocomplete-dropdown li.highlighted {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
diff --git a/src/components/common/ConfirmDialog.tsx b/src/components/common/ConfirmDialog.tsx
new file mode 100644
index 0000000..5475cfb
--- /dev/null
+++ b/src/components/common/ConfirmDialog.tsx
@@ -0,0 +1,22 @@
+import { useUIStore } from '../../stores/uiStore';
+import Modal from './Modal';
+
+export default function ConfirmDialog() {
+ const { open, title, message, onConfirm } = useUIStore((s) => s.confirmDialog);
+ const closeConfirm = useUIStore((s) => s.closeConfirm);
+
+ const handleConfirm = () => {
+ onConfirm?.();
+ closeConfirm();
+ };
+
+ return (
+
+ {message}
+
+
+
+
+
+ );
+}
diff --git a/src/components/common/CopyButton.tsx b/src/components/common/CopyButton.tsx
new file mode 100644
index 0000000..8672cc3
--- /dev/null
+++ b/src/components/common/CopyButton.tsx
@@ -0,0 +1,23 @@
+import { useState, useCallback } from 'react';
+
+interface CopyButtonProps {
+ text: string;
+ label?: string;
+}
+
+export default function CopyButton({ text, label = 'Copy' }: CopyButtonProps) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = useCallback(() => {
+ navigator.clipboard.writeText(text).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ });
+ }, [text]);
+
+ return (
+
+ );
+}
diff --git a/src/components/common/FluidAutocomplete.tsx b/src/components/common/FluidAutocomplete.tsx
new file mode 100644
index 0000000..1c01905
--- /dev/null
+++ b/src/components/common/FluidAutocomplete.tsx
@@ -0,0 +1,90 @@
+import { useState, useRef, useEffect } from 'react';
+import { FLUID_LIST } from '../../data/fluids';
+import './Autocomplete.css';
+
+interface FluidAutocompleteProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export default function FluidAutocomplete({ value, onChange, placeholder = 'namespace:fluid_id' }: FluidAutocompleteProps) {
+ const [query, setQuery] = useState(value);
+ const [open, setOpen] = useState(false);
+ const [highlighted, setHighlighted] = useState(0);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ setQuery(value);
+ }, [value]);
+
+ const filtered = open && query
+ ? FLUID_LIST.filter((f) => f.includes(query.toLowerCase())).slice(0, 20)
+ : [];
+
+ useEffect(() => {
+ function handleClick(e: MouseEvent) {
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClick);
+ return () => document.removeEventListener('mousedown', handleClick);
+ }, []);
+
+ const handleSelect = (item: string) => {
+ setQuery(item);
+ onChange(item);
+ setOpen(false);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!open || filtered.length === 0) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setHighlighted((h) => Math.min(h + 1, filtered.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setHighlighted((h) => Math.max(h - 1, 0));
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSelect(filtered[highlighted]);
+ } else if (e.key === 'Escape') {
+ setOpen(false);
+ }
+ };
+
+ return (
+
+
{
+ setQuery(e.target.value);
+ setOpen(true);
+ setHighlighted(0);
+ }}
+ onFocus={() => setOpen(true)}
+ onBlur={() => {
+ if (query !== value) onChange(query);
+ }}
+ onKeyDown={handleKeyDown}
+ />
+ {open && filtered.length > 0 && (
+
+ {filtered.map((item, i) => (
+ - handleSelect(item)}
+ onMouseEnter={() => setHighlighted(i)}
+ >
+ {item}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/common/ItemAutocomplete.tsx b/src/components/common/ItemAutocomplete.tsx
new file mode 100644
index 0000000..03e2ad2
--- /dev/null
+++ b/src/components/common/ItemAutocomplete.tsx
@@ -0,0 +1,91 @@
+import { useState, useRef, useEffect } from 'react';
+import { ALL_ITEMS } from '../../data/items';
+import './Autocomplete.css';
+
+interface ItemAutocompleteProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export default function ItemAutocomplete({ value, onChange, placeholder = 'minecraft:item_id' }: ItemAutocompleteProps) {
+ const [query, setQuery] = useState(value);
+ const [open, setOpen] = useState(false);
+ const [highlighted, setHighlighted] = useState(0);
+ const wrapperRef = useRef(null);
+
+ useEffect(() => {
+ setQuery(value);
+ }, [value]);
+
+ const filtered = open && query
+ ? ALL_ITEMS.filter((item) => item.includes(query.toLowerCase())).slice(0, 20)
+ : [];
+
+ useEffect(() => {
+ function handleClick(e: MouseEvent) {
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
+ setOpen(false);
+ }
+ }
+ document.addEventListener('mousedown', handleClick);
+ return () => document.removeEventListener('mousedown', handleClick);
+ }, []);
+
+ const handleSelect = (item: string) => {
+ setQuery(item);
+ onChange(item);
+ setOpen(false);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!open || filtered.length === 0) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setHighlighted((h) => Math.min(h + 1, filtered.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setHighlighted((h) => Math.max(h - 1, 0));
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSelect(filtered[highlighted]);
+ } else if (e.key === 'Escape') {
+ setOpen(false);
+ }
+ };
+
+ return (
+
+
{
+ setQuery(e.target.value);
+ setOpen(true);
+ setHighlighted(0);
+ }}
+ onFocus={() => setOpen(true)}
+ onBlur={() => {
+ // Commit on blur
+ if (query !== value) onChange(query);
+ }}
+ onKeyDown={handleKeyDown}
+ />
+ {open && filtered.length > 0 && (
+
+ {filtered.map((item, i) => (
+ - handleSelect(item)}
+ onMouseEnter={() => setHighlighted(i)}
+ >
+ {item}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/common/Modal.css b/src/components/common/Modal.css
new file mode 100644
index 0000000..308379c
--- /dev/null
+++ b/src/components/common/Modal.css
@@ -0,0 +1,40 @@
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ width: 90%;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: var(--shadow);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.modal-title {
+ font-size: 15px;
+ font-weight: 600;
+ margin: 0;
+}
+
+.modal-body {
+ padding: 16px;
+ overflow-y: auto;
+ flex: 1;
+}
diff --git a/src/components/common/Modal.tsx b/src/components/common/Modal.tsx
new file mode 100644
index 0000000..51bef9b
--- /dev/null
+++ b/src/components/common/Modal.tsx
@@ -0,0 +1,30 @@
+import type { ReactNode } from 'react';
+import './Modal.css';
+
+interface ModalProps {
+ open: boolean;
+ onClose: () => void;
+ title: string;
+ children: ReactNode;
+ width?: number;
+}
+
+export default function Modal({ open, onClose, title, children, width = 500 }: ModalProps) {
+ if (!open) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+
{title}
+
+
+
{children}
+
+
+ );
+}
diff --git a/src/components/common/TierBadge.tsx b/src/components/common/TierBadge.tsx
new file mode 100644
index 0000000..c364972
--- /dev/null
+++ b/src/components/common/TierBadge.tsx
@@ -0,0 +1,27 @@
+import type { Tier } from '../../types/research';
+import { TIER_INFO } from '../../data/tiers';
+
+interface TierBadgeProps {
+ tier: Tier;
+ small?: boolean;
+}
+
+export default function TierBadge({ tier, small }: TierBadgeProps) {
+ const info = TIER_INFO[tier];
+ return (
+
+ {tier}
+
+ );
+}
diff --git a/src/components/dialogs/ExportDialog.tsx b/src/components/dialogs/ExportDialog.tsx
new file mode 100644
index 0000000..5fe23e8
--- /dev/null
+++ b/src/components/dialogs/ExportDialog.tsx
@@ -0,0 +1,81 @@
+import { useState } from 'react';
+import { useUIStore } from '../../stores/uiStore';
+import { useResearchStore } from '../../stores/researchStore';
+import { useRecipeStore } from '../../stores/recipeStore';
+import { exportToZip } from '../../utils/exportZip';
+import { validateAll } from '../../utils/validation';
+import Modal from '../common/Modal';
+
+export default function ExportDialog() {
+ const open = useUIStore((s) => s.exportDialogOpen);
+ const close = useUIStore((s) => s.closeExportDialog);
+ const openValidation = useUIStore((s) => s.openValidationDialog);
+ const nodes = useResearchStore((s) => s.nodes);
+ const recipes = useRecipeStore((s) => s.recipes);
+ const [includeAdvancements, setIncludeAdvancements] = useState(true);
+ const [exporting, setExporting] = useState(false);
+
+ const issues = validateAll(nodes, recipes);
+ const errors = issues.filter((i) => i.severity === 'error');
+ const nodeCount = Object.keys(nodes).length;
+ const recipeCount = Object.keys(recipes).length;
+
+ const handleExport = async () => {
+ setExporting(true);
+ try {
+ await exportToZip(nodes, recipes, includeAdvancements);
+ close();
+ } finally {
+ setExporting(false);
+ }
+ };
+
+ return (
+
+
+
+ {nodeCount} research node{nodeCount !== 1 ? 's' : ''}, {recipeCount} recipe{recipeCount !== 1 ? 's' : ''}
+
+
+ {errors.length > 0 && (
+
+ {errors.length} validation error{errors.length !== 1 ? 's' : ''} found.{' '}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/dialogs/ImportDialog.tsx b/src/components/dialogs/ImportDialog.tsx
new file mode 100644
index 0000000..512f841
--- /dev/null
+++ b/src/components/dialogs/ImportDialog.tsx
@@ -0,0 +1,100 @@
+import { useRef, useState } from 'react';
+import { useUIStore } from '../../stores/uiStore';
+import { useResearchStore } from '../../stores/researchStore';
+import { useRecipeStore } from '../../stores/recipeStore';
+import { importFiles } from '../../utils/importJson';
+import Modal from '../common/Modal';
+
+export default function ImportDialog() {
+ const open = useUIStore((s) => s.importDialogOpen);
+ const close = useUIStore((s) => s.closeImportDialog);
+ const showConfirm = useUIStore((s) => s.showConfirm);
+ const setNodes = useResearchStore((s) => s.setNodes);
+ const existingNodes = useResearchStore((s) => s.nodes);
+ const setRecipes = useRecipeStore((s) => s.setRecipes);
+ const existingRecipes = useRecipeStore((s) => s.recipes);
+ const fileInputRef = useRef(null);
+ const [importing, setImporting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleImport = async (merge: boolean) => {
+ if (!fileInputRef.current?.files?.length) return;
+ setImporting(true);
+ setError(null);
+
+ try {
+ const { nodes, recipes } = await importFiles(fileInputRef.current.files);
+ const nodeCount = Object.keys(nodes).length;
+ const recipeCount = Object.keys(recipes).length;
+
+ if (nodeCount === 0 && recipeCount === 0) {
+ setError('No valid research or recipe files found.');
+ return;
+ }
+
+ if (merge) {
+ setNodes({ ...existingNodes, ...nodes });
+ setRecipes({ ...existingRecipes, ...recipes });
+ } else {
+ setNodes(nodes);
+ setRecipes(recipes);
+ }
+ close();
+ } catch (err) {
+ setError(`Import failed: ${(err as Error).message}`);
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ return (
+
+
+
+ Import JSON files or a ZIP containing research and recipe definitions.
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/dialogs/ValidationDialog.tsx b/src/components/dialogs/ValidationDialog.tsx
new file mode 100644
index 0000000..913792b
--- /dev/null
+++ b/src/components/dialogs/ValidationDialog.tsx
@@ -0,0 +1,98 @@
+import { useMemo } from 'react';
+import { useUIStore } from '../../stores/uiStore';
+import { useResearchStore } from '../../stores/researchStore';
+import { useRecipeStore } from '../../stores/recipeStore';
+import { validateAll } from '../../utils/validation';
+import Modal from '../common/Modal';
+
+export default function ValidationDialog() {
+ const open = useUIStore((s) => s.validationDialogOpen);
+ const close = useUIStore((s) => s.closeValidationDialog);
+ const selectNode = useUIStore((s) => s.selectNode);
+ const selectRecipe = useUIStore((s) => s.selectRecipe);
+ const nodes = useResearchStore((s) => s.nodes);
+ const recipes = useRecipeStore((s) => s.recipes);
+
+ const issues = useMemo(() => {
+ if (!open) return [];
+ return validateAll(nodes, recipes);
+ }, [open, nodes, recipes]);
+
+ const errors = issues.filter((i) => i.severity === 'error');
+ const warnings = issues.filter((i) => i.severity === 'warning');
+
+ const handleClick = (target: string, targetId: string) => {
+ if (target === 'research') {
+ selectNode(targetId);
+ } else {
+ selectRecipe(targetId);
+ }
+ close();
+ };
+
+ return (
+
+ {issues.length === 0 ? (
+
+ No issues found. Your datapack is valid!
+
+ ) : (
+
+
+ {errors.length} error{errors.length !== 1 ? 's' : ''}, {warnings.length} warning{warnings.length !== 1 ? 's' : ''}
+
+
+ {errors.map((issue, i) => (
+
handleClick(issue.target, issue.targetId)}
+ >
+
ERR
+
+ {issue.targetId}
+ .{issue.field}
+ {' — '}
+ {issue.message}
+
+
+ ))}
+
+ {warnings.map((issue, i) => (
+
handleClick(issue.target, issue.targetId)}
+ >
+
WARN
+
+ {issue.targetId}
+ .{issue.field}
+ {' — '}
+ {issue.message}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/editors/BasicInfoSection.tsx b/src/components/editors/BasicInfoSection.tsx
new file mode 100644
index 0000000..3589cf1
--- /dev/null
+++ b/src/components/editors/BasicInfoSection.tsx
@@ -0,0 +1,75 @@
+import type { ResearchNodeData, Tier } from '../../types/research';
+import { TIERS } from '../../types/research';
+import { CATEGORY_PRESETS } from '../../data/categories';
+
+interface Props {
+ node: ResearchNodeData;
+ onChange: (updates: Partial) => void;
+}
+
+export default function BasicInfoSection({ node, onChange }: Props) {
+ return (
+
+
Basic Info
+
+
+
+
+ researchcube:{node.id}
+
+
+
+
+ onChange({ name: e.target.value })}
+ />
+
+
+
+
+
+
+
+
+ onChange({ category: e.target.value })}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/editors/DriveCraftingShaped.tsx b/src/components/editors/DriveCraftingShaped.tsx
new file mode 100644
index 0000000..bd3a7b9
--- /dev/null
+++ b/src/components/editors/DriveCraftingShaped.tsx
@@ -0,0 +1,130 @@
+import { useState, useMemo } from 'react';
+import type { DriveCraftingShapedRecipe } from '../../types/recipe';
+import type { Ingredient } from '../../types/common';
+import ItemAutocomplete from '../common/ItemAutocomplete';
+import './ShapedGrid.css';
+
+interface Props {
+ recipe: DriveCraftingShapedRecipe;
+ onChange: (recipe: DriveCraftingShapedRecipe) => void;
+}
+
+export default function DriveCraftingShaped({ recipe, onChange }: Props) {
+ const [selectedCell, setSelectedCell] = useState(null);
+
+ // Decode the pattern+key into a 3x3 grid of item IDs (or null)
+ const grid: (string | null)[] = useMemo(() => {
+ const result: (string | null)[] = Array(9).fill(null);
+ for (let row = 0; row < 3; row++) {
+ const rowStr = recipe.pattern[row] || ' ';
+ for (let col = 0; col < 3; col++) {
+ const char = rowStr[col];
+ if (char && char !== ' ' && recipe.key[char]) {
+ result[row * 3 + col] = recipe.key[char].item;
+ }
+ }
+ }
+ return result;
+ }, [recipe.pattern, recipe.key]);
+
+ const updateCell = (index: number, itemId: string | null) => {
+ const newGrid = [...grid];
+ newGrid[index] = itemId || null;
+
+ // Rebuild pattern and key from grid
+ const letterMap = new Map(); // item -> letter
+ const nextKey: Record = {};
+ let letterIndex = 0;
+ const letters = 'ABCDEFGH';
+
+ const newPattern: [string, string, string] = [' ', ' ', ' '];
+
+ for (let row = 0; row < 3; row++) {
+ let rowStr = '';
+ for (let col = 0; col < 3; col++) {
+ const item = newGrid[row * 3 + col];
+ if (!item) {
+ rowStr += ' ';
+ } else {
+ let letter = letterMap.get(item);
+ if (!letter) {
+ letter = letters[letterIndex++] || 'Z';
+ letterMap.set(item, letter);
+ nextKey[letter] = { item };
+ }
+ rowStr += letter;
+ }
+ }
+ newPattern[row] = rowStr;
+ }
+
+ onChange({ ...recipe, pattern: newPattern, key: nextKey });
+ };
+
+ return (
+
+
Crafting Grid (3x3)
+
+
+ {grid.map((item, i) => (
+
setSelectedCell(i)}
+ >
+ {item ? item.split(':')[1] || item : ''}
+
+ ))}
+
+
+ {selectedCell !== null && (
+
+
+
+
+ updateCell(selectedCell, v || null)}
+ />
+
+
+
+
+ )}
+
+
+
+
+{recipe.pattern.map((row) => `"${row}"`).join('\n')}
+
+
+
+ {Object.keys(recipe.key).length > 0 && (
+
+
+ {Object.entries(recipe.key).map(([char, ing]) => (
+
+ {char} = {ing.item}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/editors/DriveCraftingShapeless.tsx b/src/components/editors/DriveCraftingShapeless.tsx
new file mode 100644
index 0000000..1774dd7
--- /dev/null
+++ b/src/components/editors/DriveCraftingShapeless.tsx
@@ -0,0 +1,57 @@
+import type { DriveCraftingShapelessRecipe } from '../../types/recipe';
+import ItemAutocomplete from '../common/ItemAutocomplete';
+
+interface Props {
+ recipe: DriveCraftingShapelessRecipe;
+ onChange: (recipe: DriveCraftingShapelessRecipe) => void;
+}
+
+export default function DriveCraftingShapeless({ recipe, onChange }: Props) {
+ const addIngredient = () => {
+ if (recipe.ingredients.length >= 8) return;
+ onChange({
+ ...recipe,
+ ingredients: [...recipe.ingredients, { item: '' }],
+ });
+ };
+
+ const updateIngredient = (index: number, item: string) => {
+ const next = recipe.ingredients.map((ing, i) =>
+ i === index ? { item } : ing
+ );
+ onChange({ ...recipe, ingredients: next });
+ };
+
+ const removeIngredient = (index: number) => {
+ onChange({
+ ...recipe,
+ ingredients: recipe.ingredients.filter((_, i) => i !== index),
+ });
+ };
+
+ return (
+
+
+ Ingredients (Shapeless) ({recipe.ingredients.length}/8)
+
+ {recipe.ingredients.map((ing, i) => (
+
+
+ updateIngredient(i, v)}
+ />
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/editors/DurationInput.tsx b/src/components/editors/DurationInput.tsx
new file mode 100644
index 0000000..b222e53
--- /dev/null
+++ b/src/components/editors/DurationInput.tsx
@@ -0,0 +1,36 @@
+import { formatTicks } from '../../utils/tickFormatter';
+
+interface Props {
+ value: number;
+ onChange: (value: number) => void;
+}
+
+const QUICK_VALUES = [600, 1200, 2400, 4800, 9600, 18000];
+
+export default function DurationInput({ value, onChange }: Props) {
+ return (
+
+
Duration
+
+ onChange(Math.max(1, parseInt(e.target.value) || 1))}
+ />
+ {formatTicks(value)}
+
+
+ {QUICK_VALUES.map((v) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/editors/EditorPanel.css b/src/components/editors/EditorPanel.css
new file mode 100644
index 0000000..dae228c
--- /dev/null
+++ b/src/components/editors/EditorPanel.css
@@ -0,0 +1,97 @@
+.editor-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.editor-panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.editor-panel-title {
+ font-size: 14px;
+ font-weight: 600;
+ margin: 0;
+}
+
+.editor-panel-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.editor-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.editor-section-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.editor-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.editor-field-label {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.editor-field-row {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+}
+
+.editor-field-hint {
+ font-size: 11px;
+ color: var(--text-muted);
+}
+
+.editor-field-warning {
+ font-size: 11px;
+ color: var(--warning);
+}
+
+.editor-list-item {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+}
+
+.editor-list-item .btn-icon {
+ flex-shrink: 0;
+}
+
+.editor-badge {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: var(--radius-sm);
+ font-size: 10px;
+ font-weight: 600;
+ background: var(--bg-tertiary);
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+}
+
+.editor-badge-warning {
+ background: rgba(245, 158, 11, 0.15);
+ color: var(--warning);
+ border-color: rgba(245, 158, 11, 0.3);
+}
diff --git a/src/components/editors/FluidCostEditor.tsx b/src/components/editors/FluidCostEditor.tsx
new file mode 100644
index 0000000..34fa8df
--- /dev/null
+++ b/src/components/editors/FluidCostEditor.tsx
@@ -0,0 +1,74 @@
+import type { FluidCost } from '../../types/common';
+import type { Tier } from '../../types/research';
+import { TIER_FLUIDS } from '../../data/tiers';
+import FluidAutocomplete from '../common/FluidAutocomplete';
+
+interface Props {
+ fluidCost: FluidCost | null;
+ tier: Tier;
+ onChange: (fluidCost: FluidCost | null) => void;
+}
+
+export default function FluidCostEditor({ fluidCost, tier, onChange }: Props) {
+ const expectedFluid = TIER_FLUIDS[tier];
+ const mismatch = fluidCost && fluidCost.fluid !== expectedFluid;
+
+ const toggle = () => {
+ if (fluidCost) {
+ onChange(null);
+ } else {
+ onChange({ fluid: expectedFluid, amount: 1000 });
+ }
+ };
+
+ return (
+
+
+ Fluid Cost
+ {mismatch && (
+
+ Tier mismatch
+
+ )}
+
+ {!fluidCost ? (
+
+ ) : (
+ <>
+
+
+ onChange({ ...fluidCost, fluid })}
+ />
+
+
+ onChange({ ...fluidCost, amount: Math.max(1, parseInt(e.target.value) || 1) })
+ }
+ />
+
mB
+
+
+ {mismatch && (
+
+ Expected {expectedFluid} for tier {tier}.{' '}
+
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/editors/IdeaChipEditor.tsx b/src/components/editors/IdeaChipEditor.tsx
new file mode 100644
index 0000000..9107348
--- /dev/null
+++ b/src/components/editors/IdeaChipEditor.tsx
@@ -0,0 +1,112 @@
+import type { IdeaChip } from '../../types/research';
+import CopyButton from '../common/CopyButton';
+import ItemAutocomplete from '../common/ItemAutocomplete';
+
+interface Props {
+ ideaChip: IdeaChip | null;
+ nodeId: string;
+ nodeName: string;
+ onChange: (ideaChip: IdeaChip | null) => void;
+}
+
+export default function IdeaChipEditor({ ideaChip, nodeId, nodeName, onChange }: Props) {
+ const enabled = ideaChip !== null;
+
+ const toggle = () => {
+ if (enabled) {
+ onChange(null);
+ } else {
+ const displayName = nodeName || nodeId.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
+ onChange({
+ item: 'researchcube:metadata_broken',
+ components: {
+ 'minecraft:custom_name': `{"text":"Idea: ${displayName}","italic":false}`,
+ 'minecraft:custom_data': { researchcube_chip_id: nodeId },
+ },
+ });
+ }
+ };
+
+ const chipId = ideaChip?.components?.['minecraft:custom_data']?.researchcube_chip_id as string || nodeId;
+
+ const giveCommand = ideaChip
+ ? `/give @s ${ideaChip.item}[minecraft:custom_name='${ideaChip.components?.['minecraft:custom_name'] || ''}',minecraft:custom_data={researchcube_chip_id:"${chipId}"}]`
+ : '';
+
+ return (
+
+
+ Idea Chip
+ Phase 21 — coming soon
+
+
+
+
+ {enabled && ideaChip && (
+
+
+
+ onChange({ ...ideaChip, item })}
+ />
+
+
+
+
+
+ onChange({
+ ...ideaChip,
+ components: {
+ ...ideaChip.components,
+ 'minecraft:custom_name': e.target.value,
+ },
+ })
+ }
+ />
+
+
+
+
+
+ onChange({
+ ...ideaChip,
+ components: {
+ ...ideaChip.components,
+ 'minecraft:custom_data': { researchcube_chip_id: e.target.value },
+ },
+ })
+ }
+ />
+
+
+
+
+
+ {giveCommand}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/editors/ItemCostsEditor.tsx b/src/components/editors/ItemCostsEditor.tsx
new file mode 100644
index 0000000..92c6eea
--- /dev/null
+++ b/src/components/editors/ItemCostsEditor.tsx
@@ -0,0 +1,56 @@
+import type { ItemCost } from '../../types/common';
+import ItemAutocomplete from '../common/ItemAutocomplete';
+
+interface Props {
+ items: ItemCost[];
+ onChange: (items: ItemCost[]) => void;
+}
+
+export default function ItemCostsEditor({ items, onChange }: Props) {
+ const addItem = () => {
+ if (items.length >= 6) return;
+ onChange([...items, { item: '', count: 1 }]);
+ };
+
+ const updateItem = (index: number, updates: Partial) => {
+ const next = items.map((item, i) => (i === index ? { ...item, ...updates } : item));
+ onChange(next);
+ };
+
+ const removeItem = (index: number) => {
+ onChange(items.filter((_, i) => i !== index));
+ };
+
+ return (
+
+
+ Item Costs ({items.length}/6)
+
+ {items.map((item, i) => (
+
+
+ updateItem(i, { item: v })}
+ />
+
+
updateItem(i, { count: Math.max(1, parseInt(e.target.value) || 1) })}
+ />
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/editors/PrerequisiteEditor.tsx b/src/components/editors/PrerequisiteEditor.tsx
new file mode 100644
index 0000000..c2708a3
--- /dev/null
+++ b/src/components/editors/PrerequisiteEditor.tsx
@@ -0,0 +1,61 @@
+import { useResearchStore } from '../../stores/researchStore';
+import { flattenPrerequisiteIds, getPrerequisiteMode } from '../../utils/prerequisites';
+import { stripNamespace } from '../../utils/idHelpers';
+
+interface Props {
+ nodeId: string;
+}
+
+export default function PrerequisiteEditor({ nodeId }: Props) {
+ const node = useResearchStore((s) => s.nodes[nodeId]);
+ const setMode = useResearchStore((s) => s.setPrerequisiteMode);
+
+ if (!node) return null;
+
+ const prereqIds = node.prerequisites
+ ? flattenPrerequisiteIds(node.prerequisites).map(stripNamespace)
+ : [];
+ const mode = getPrerequisiteMode(node.prerequisites);
+
+ return (
+
+
Prerequisites
+ {prereqIds.length === 0 ? (
+
+ No prerequisites. Connect nodes on the canvas to add.
+
+ ) : (
+ <>
+
+ {prereqIds.map((id) => (
+ {id}
+ ))}
+
+ {prereqIds.length >= 2 && (
+
+ Mode:
+
+
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/editors/ProcessingEditor.tsx b/src/components/editors/ProcessingEditor.tsx
new file mode 100644
index 0000000..6125361
--- /dev/null
+++ b/src/components/editors/ProcessingEditor.tsx
@@ -0,0 +1,163 @@
+import type { ProcessingRecipe } from '../../types/recipe';
+import type { ItemStack, FluidStack } from '../../types/common';
+import ItemAutocomplete from '../common/ItemAutocomplete';
+import FluidAutocomplete from '../common/FluidAutocomplete';
+import { formatTicks } from '../../utils/tickFormatter';
+
+interface Props {
+ recipe: ProcessingRecipe;
+ onChange: (recipe: ProcessingRecipe) => void;
+}
+
+export default function ProcessingEditor({ recipe, onChange }: Props) {
+ // Item inputs
+ const addInput = () => {
+ onChange({ ...recipe, inputs: [...recipe.inputs, { item: '' }] });
+ };
+ const updateInput = (i: number, item: string) => {
+ const next = recipe.inputs.map((inp, idx) => (idx === i ? { item } : inp));
+ onChange({ ...recipe, inputs: next });
+ };
+ const removeInput = (i: number) => {
+ onChange({ ...recipe, inputs: recipe.inputs.filter((_, idx) => idx !== i) });
+ };
+
+ // Fluid inputs
+ const addFluidInput = () => {
+ onChange({ ...recipe, fluidInputs: [...recipe.fluidInputs, { fluid: '', amount: 1000 }] });
+ };
+ const updateFluidInput = (i: number, updates: Partial) => {
+ const next = recipe.fluidInputs.map((f, idx) => (idx === i ? { ...f, ...updates } : f));
+ onChange({ ...recipe, fluidInputs: next });
+ };
+ const removeFluidInput = (i: number) => {
+ onChange({ ...recipe, fluidInputs: recipe.fluidInputs.filter((_, idx) => idx !== i) });
+ };
+
+ // Item outputs
+ const addOutput = () => {
+ onChange({ ...recipe, outputs: [...recipe.outputs, { id: '', count: 1 }] });
+ };
+ const updateOutput = (i: number, updates: Partial) => {
+ const next = recipe.outputs.map((o, idx) => (idx === i ? { ...o, ...updates } : o));
+ onChange({ ...recipe, outputs: next });
+ };
+ const removeOutput = (i: number) => {
+ onChange({ ...recipe, outputs: recipe.outputs.filter((_, idx) => idx !== i) });
+ };
+
+ // Fluid output
+ const toggleFluidOutput = () => {
+ if (recipe.fluidOutput) {
+ onChange({ ...recipe, fluidOutput: null });
+ } else {
+ onChange({ ...recipe, fluidOutput: { fluid: '', amount: 1000 } });
+ }
+ };
+
+ return (
+ <>
+ {/* Duration */}
+
+
Duration
+
+ onChange({ ...recipe, duration: Math.max(1, parseInt(e.target.value) || 1) })}
+ />
+ {formatTicks(recipe.duration)}
+
+
+
+ {/* Item Inputs */}
+
+
Item Inputs
+ {recipe.inputs.map((inp, i) => (
+
+
+ updateInput(i, v)} />
+
+
+
+ ))}
+
+
+
+ {/* Fluid Inputs */}
+
+
Fluid Inputs
+ {recipe.fluidInputs.map((f, i) => (
+
+
+ updateFluidInput(i, { fluid: v })} />
+
+
updateFluidInput(i, { amount: Math.max(1, parseInt(e.target.value) || 1) })}
+ />
+
mB
+
+
+ ))}
+
+
+
+ {/* Item Outputs */}
+
+
Item Outputs
+ {recipe.outputs.map((out, i) => (
+
+
+ updateOutput(i, { id: v })} />
+
+
updateOutput(i, { count: Math.max(1, parseInt(e.target.value) || 1) })}
+ />
+
+
+ ))}
+
+
+
+ {/* Fluid Output */}
+
+
Fluid Output
+ {!recipe.fluidOutput ? (
+
+ ) : (
+
+
+ onChange({ ...recipe, fluidOutput: { ...recipe.fluidOutput!, fluid } })}
+ />
+
+
+ onChange({
+ ...recipe,
+ fluidOutput: { ...recipe.fluidOutput!, amount: Math.max(1, parseInt(e.target.value) || 1) },
+ })
+ }
+ />
+
mB
+
+
+ )}
+
+ >
+ );
+}
diff --git a/src/components/editors/RecipeEditorPanel.tsx b/src/components/editors/RecipeEditorPanel.tsx
new file mode 100644
index 0000000..1ca6e6f
--- /dev/null
+++ b/src/components/editors/RecipeEditorPanel.tsx
@@ -0,0 +1,139 @@
+import { useRecipeStore } from '../../stores/recipeStore';
+import { useUIStore } from '../../stores/uiStore';
+import type { RecipeData, DriveCraftingShapelessRecipe, DriveCraftingShapedRecipe, ProcessingRecipe } from '../../types/recipe';
+import DriveCraftingShapeless from './DriveCraftingShapeless';
+import DriveCraftingShaped from './DriveCraftingShaped';
+import ProcessingEditor from './ProcessingEditor';
+import RecipeResultEditor from './RecipeResultEditor';
+import './EditorPanel.css';
+
+interface Props {
+ recipeId: string;
+}
+
+export default function RecipeEditorPanel({ recipeId }: Props) {
+ const recipe = useRecipeStore((s) => s.recipes[recipeId]);
+ const updateRecipe = useRecipeStore((s) => s.updateRecipe);
+ const removeRecipe = useRecipeStore((s) => s.removeRecipe);
+ const selectRecipe = useUIStore((s) => s.selectRecipe);
+ const showConfirm = useUIStore((s) => s.showConfirm);
+
+ if (!recipe) return null;
+
+ const handleDelete = () => {
+ showConfirm('Delete Recipe', `Delete recipe "${recipe.id}"? This cannot be undone.`, () => {
+ removeRecipe(recipeId);
+ selectRecipe(null);
+ });
+ };
+
+ const switchKind = (newKind: RecipeData['kind']) => {
+ if (recipe.kind === newKind) return;
+ let newRecipe: RecipeData;
+ if (newKind === 'drive_crafting_shapeless') {
+ newRecipe = {
+ kind: 'drive_crafting_shapeless',
+ id: recipe.id,
+ ingredients: [{ item: 'minecraft:stone' }],
+ result: 'result' in recipe ? recipe.result : { id: 'minecraft:stone', count: 1 },
+ group: recipe.group,
+ };
+ } else if (newKind === 'drive_crafting_shaped') {
+ newRecipe = {
+ kind: 'drive_crafting_shaped',
+ id: recipe.id,
+ pattern: [' ', ' ', ' '],
+ key: {},
+ result: 'result' in recipe ? recipe.result : { id: 'minecraft:stone', count: 1 },
+ group: recipe.group,
+ };
+ } else {
+ newRecipe = {
+ kind: 'processing',
+ id: recipe.id,
+ inputs: [{ item: 'minecraft:stone' }],
+ fluidInputs: [],
+ outputs: [{ id: 'minecraft:stone', count: 1 }],
+ fluidOutput: null,
+ duration: 200,
+ group: recipe.group,
+ };
+ }
+ updateRecipe(recipeId, newRecipe);
+ };
+
+ return (
+
+
+
Recipe Editor
+
+
+
+
+
+
+
+
+
Recipe Info
+
+
+
+ researchcube:{recipe.id}
+
+
+
+
+
Recipe Type
+
+
+
+
+
+
+
+ {recipe.kind === 'drive_crafting_shapeless' && (
+
updateRecipe(recipeId, r)}
+ />
+ )}
+
+ {recipe.kind === 'drive_crafting_shaped' && (
+ updateRecipe(recipeId, r)}
+ />
+ )}
+
+ {recipe.kind === 'processing' && (
+ updateRecipe(recipeId, r)}
+ />
+ )}
+
+ {recipe.kind !== 'processing' && (
+ updateRecipe(recipeId, { ...recipe, result } as RecipeData)}
+ />
+ )}
+
+
+ );
+}
diff --git a/src/components/editors/RecipePoolEditor.tsx b/src/components/editors/RecipePoolEditor.tsx
new file mode 100644
index 0000000..15ee0a4
--- /dev/null
+++ b/src/components/editors/RecipePoolEditor.tsx
@@ -0,0 +1,102 @@
+import { useCallback } from 'react';
+import type { WeightedRecipe } from '../../types/research';
+import { useRecipeStore } from '../../stores/recipeStore';
+import { useUIStore } from '../../stores/uiStore';
+import { createDefaultShapelessRecipe } from '../../types/recipe';
+import { generateUniqueId } from '../../utils/idHelpers';
+
+interface Props {
+ recipePool: WeightedRecipe[];
+ onChange: (pool: WeightedRecipe[]) => void;
+}
+
+export default function RecipePoolEditor({ recipePool, onChange }: Props) {
+ const recipes = useRecipeStore((s) => s.recipes);
+ const addRecipe = useRecipeStore((s) => s.addRecipe);
+ const selectRecipe = useUIStore((s) => s.selectRecipe);
+
+ const addExistingRef = () => {
+ onChange([...recipePool, '']);
+ };
+
+ const createAndAdd = useCallback(() => {
+ const existingIds = new Set(Object.keys(recipes));
+ const id = generateUniqueId('new_recipe', existingIds);
+ const recipe = createDefaultShapelessRecipe(id);
+ addRecipe(recipe);
+ onChange([...recipePool, `researchcube:${id}`]);
+ selectRecipe(id);
+ }, [recipes, addRecipe, recipePool, onChange, selectRecipe]);
+
+ const updateEntry = (index: number, value: WeightedRecipe) => {
+ const next = recipePool.map((entry, i) => (i === index ? value : entry));
+ onChange(next);
+ };
+
+ const removeEntry = (index: number) => {
+ onChange(recipePool.filter((_, i) => i !== index));
+ };
+
+ return (
+
+
Recipe Pool
+ {recipePool.length === 0 && (
+
No recipes linked to this research node.
+ )}
+ {recipePool.map((entry, i) => {
+ const id = typeof entry === 'string' ? entry : entry.id;
+ const weight = typeof entry === 'object' ? entry.weight : 1;
+ const cleanId = id.replace('researchcube:', '');
+ const exists = recipes[cleanId];
+
+ return (
+
+ {
+ const newId = e.target.value;
+ if (weight !== 1) {
+ updateEntry(i, { id: newId, weight });
+ } else {
+ updateEntry(i, newId);
+ }
+ }}
+ />
+ {
+ const w = Math.max(1, parseInt(e.target.value) || 1);
+ updateEntry(i, w === 1 ? id : { id, weight: w });
+ }}
+ />
+ {exists && (
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/src/components/editors/RecipeResultEditor.tsx b/src/components/editors/RecipeResultEditor.tsx
new file mode 100644
index 0000000..e4b2ecf
--- /dev/null
+++ b/src/components/editors/RecipeResultEditor.tsx
@@ -0,0 +1,30 @@
+import type { ItemStack } from '../../types/common';
+import ItemAutocomplete from '../common/ItemAutocomplete';
+
+interface Props {
+ result: ItemStack;
+ onChange: (result: ItemStack) => void;
+}
+
+export default function RecipeResultEditor({ result, onChange }: Props) {
+ return (
+
+
Result
+
+
+ onChange({ ...result, id })}
+ />
+
+
onChange({ ...result, count: Math.max(1, parseInt(e.target.value) || 1) })}
+ />
+
+
+ );
+}
diff --git a/src/components/editors/ResearchEditorPanel.tsx b/src/components/editors/ResearchEditorPanel.tsx
new file mode 100644
index 0000000..971e355
--- /dev/null
+++ b/src/components/editors/ResearchEditorPanel.tsx
@@ -0,0 +1,71 @@
+import { useResearchStore } from '../../stores/researchStore';
+import { useUIStore } from '../../stores/uiStore';
+import BasicInfoSection from './BasicInfoSection';
+import DurationInput from './DurationInput';
+import ItemCostsEditor from './ItemCostsEditor';
+import FluidCostEditor from './FluidCostEditor';
+import PrerequisiteEditor from './PrerequisiteEditor';
+import IdeaChipEditor from './IdeaChipEditor';
+import RecipePoolEditor from './RecipePoolEditor';
+import './EditorPanel.css';
+
+interface Props {
+ nodeId: string;
+}
+
+export default function ResearchEditorPanel({ nodeId }: Props) {
+ const node = useResearchStore((s) => s.nodes[nodeId]);
+ const updateNode = useResearchStore((s) => s.updateNode);
+ const removeNode = useResearchStore((s) => s.removeNode);
+ const selectNode = useUIStore((s) => s.selectNode);
+ const showConfirm = useUIStore((s) => s.showConfirm);
+
+ if (!node) return null;
+
+ const handleDelete = () => {
+ showConfirm('Delete Research', `Delete "${node.name || node.id}"? This cannot be undone.`, () => {
+ removeNode(nodeId);
+ selectNode(null);
+ });
+ };
+
+ return (
+
+
+
Research Node
+
+
+
+
+
+
+
+
updateNode(nodeId, updates)} />
+ updateNode(nodeId, { duration })}
+ />
+ updateNode(nodeId, { itemCosts })}
+ />
+ updateNode(nodeId, { fluidCost })}
+ />
+
+ updateNode(nodeId, { ideaChip })}
+ />
+ updateNode(nodeId, { recipePool })}
+ />
+
+
+ );
+}
diff --git a/src/components/editors/ShapedGrid.css b/src/components/editors/ShapedGrid.css
new file mode 100644
index 0000000..ac604b3
--- /dev/null
+++ b/src/components/editors/ShapedGrid.css
@@ -0,0 +1,38 @@
+.shaped-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 3px;
+ max-width: 240px;
+}
+
+.shaped-grid-cell {
+ aspect-ratio: 1;
+ background: var(--bg-tertiary);
+ border: 2px solid var(--border);
+ border-radius: var(--radius-sm);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ color: var(--text-muted);
+ cursor: pointer;
+ transition: border-color 0.15s;
+ padding: 2px;
+ text-align: center;
+ word-break: break-all;
+ overflow: hidden;
+}
+
+.shaped-grid-cell:hover {
+ border-color: var(--text-muted);
+}
+
+.shaped-grid-cell.selected {
+ border-color: var(--accent);
+ background: rgba(91, 106, 191, 0.1);
+}
+
+.shaped-grid-cell.filled {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+}
diff --git a/src/components/layout/AppHeader.css b/src/components/layout/AppHeader.css
new file mode 100644
index 0000000..f2fcd26
--- /dev/null
+++ b/src/components/layout/AppHeader.css
@@ -0,0 +1,28 @@
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border);
+ height: 44px;
+ flex-shrink: 0;
+}
+
+.app-header-brand {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.app-header-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.app-header-actions {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+}
diff --git a/src/components/layout/AppHeader.tsx b/src/components/layout/AppHeader.tsx
new file mode 100644
index 0000000..b047392
--- /dev/null
+++ b/src/components/layout/AppHeader.tsx
@@ -0,0 +1,52 @@
+import { useUIStore } from '../../stores/uiStore';
+import { useResearchStore } from '../../stores/researchStore';
+import { useRecipeStore } from '../../stores/recipeStore';
+import { getExampleTree } from '../../data/exampleTree';
+import './AppHeader.css';
+
+export default function AppHeader() {
+ const openImport = useUIStore((s) => s.openImportDialog);
+ const openExport = useUIStore((s) => s.openExportDialog);
+ const openValidation = useUIStore((s) => s.openValidationDialog);
+ const showConfirm = useUIStore((s) => s.showConfirm);
+ const clearResearch = useResearchStore((s) => s.clearAll);
+ const setNodes = useResearchStore((s) => s.setNodes);
+ const clearRecipes = useRecipeStore((s) => s.clearAll);
+
+ const handleNew = () => {
+ showConfirm(
+ 'New Project',
+ 'This will clear all research nodes and recipes. Continue?',
+ () => {
+ clearResearch();
+ clearRecipes();
+ }
+ );
+ };
+
+ const handleLoadExample = () => {
+ showConfirm(
+ 'Load Example Tree',
+ 'This will replace all current nodes with the 23-node example tree. Continue?',
+ () => {
+ clearRecipes();
+ setNodes(getExampleTree());
+ }
+ );
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/layout/MainLayout.css b/src/components/layout/MainLayout.css
new file mode 100644
index 0000000..6cb2f88
--- /dev/null
+++ b/src/components/layout/MainLayout.css
@@ -0,0 +1,18 @@
+.main-layout {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.main-layout-canvas {
+ flex: 1;
+ position: relative;
+}
+
+.main-layout-panel {
+ width: 380px;
+ flex-shrink: 0;
+ border-left: 1px solid var(--border);
+ background: var(--bg-secondary);
+ overflow-y: auto;
+}
diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx
new file mode 100644
index 0000000..e3ccc5f
--- /dev/null
+++ b/src/components/layout/MainLayout.tsx
@@ -0,0 +1,38 @@
+import { useUIStore } from '../../stores/uiStore';
+import ResearchTreeCanvas from '../canvas/ResearchTreeCanvas';
+import ResearchEditorPanel from '../editors/ResearchEditorPanel';
+import RecipeEditorPanel from '../editors/RecipeEditorPanel';
+import ValidationDialog from '../dialogs/ValidationDialog';
+import ImportDialog from '../dialogs/ImportDialog';
+import ExportDialog from '../dialogs/ExportDialog';
+import ConfirmDialog from '../common/ConfirmDialog';
+import './MainLayout.css';
+
+export default function MainLayout() {
+ const editorPanel = useUIStore((s) => s.editorPanel);
+ const selectedNodeId = useUIStore((s) => s.selectedNodeId);
+ const selectedRecipeId = useUIStore((s) => s.selectedRecipeId);
+
+ return (
+
+
+
+
+ {editorPanel === 'research' && selectedNodeId && (
+
+
+
+ )}
+ {editorPanel === 'recipe' && selectedRecipeId && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/data/categories.ts b/src/data/categories.ts
new file mode 100644
index 0000000..d9da656
--- /dev/null
+++ b/src/data/categories.ts
@@ -0,0 +1,10 @@
+export const CATEGORY_PRESETS = [
+ 'circuits',
+ 'energy',
+ 'optics',
+ 'materials',
+ 'signals',
+ 'temporal',
+ 'computing',
+ 'convergence',
+] as const;
diff --git a/src/data/exampleTree.ts b/src/data/exampleTree.ts
new file mode 100644
index 0000000..7b7aa2b
--- /dev/null
+++ b/src/data/exampleTree.ts
@@ -0,0 +1,67 @@
+import type { ResearchNodeData } from '../types/research';
+
+type NodeDef = Omit;
+
+const defs: NodeDef[] = [
+ { id: 'unstable_signal', name: 'Unstable Signal', description: 'A barely coherent data stream.', category: 'signals', tier: 'UNSTABLE', duration: 600, prerequisites: null, itemCosts: [], fluidCost: { fluid: 'researchcube:thinking_fluid', amount: 500 }, ideaChip: null, recipePool: [] },
+ { id: 'fragmented_data', name: 'Fragmented Data', description: 'Corrupted patterns hinting at something more.', category: 'signals', tier: 'UNSTABLE', duration: 800, prerequisites: null, itemCosts: [], fluidCost: { fluid: 'researchcube:thinking_fluid', amount: 500 }, ideaChip: null, recipePool: [] },
+ { id: 'basic_circuit', name: 'Basic Circuit', description: 'Fundamental circuit logic.', category: 'circuits', tier: 'BASIC', duration: 1200, prerequisites: null, itemCosts: [], fluidCost: { fluid: 'researchcube:thinking_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'energy_handling', name: 'Energy Handling', description: 'stable energy flow control.', category: 'energy', tier: 'BASIC', duration: 2400, prerequisites: 'researchcube:basic_circuit', itemCosts: [], fluidCost: { fluid: 'researchcube:thinking_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'basic_optics', name: 'Basic Optics', description: 'Light manipulation fundamentals.', category: 'optics', tier: 'BASIC', duration: 1800, prerequisites: 'researchcube:basic_circuit', itemCosts: [], fluidCost: { fluid: 'researchcube:thinking_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'material_synthesis', name: 'Material Synthesis', description: 'Creating new materials from base components.', category: 'materials', tier: 'BASIC', duration: 1200, prerequisites: null, itemCosts: [], fluidCost: { fluid: 'researchcube:thinking_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'advanced_processor', name: 'Advanced Processor', description: 'A more powerful data processing unit.', category: 'circuits', tier: 'ADVANCED', duration: 6000, prerequisites: 'researchcube:energy_handling', itemCosts: [], fluidCost: { fluid: 'researchcube:pondering_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'quantum_resonance', name: 'Quantum Resonance', description: 'Harnessing quantum effects for energy.', category: 'energy', tier: 'ADVANCED', duration: 4800, prerequisites: 'researchcube:energy_handling', itemCosts: [], fluidCost: { fluid: 'researchcube:pondering_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'photonic_amplifier', name: 'Photonic Amplifier', description: 'Amplifying light-based signals.', category: 'optics', tier: 'ADVANCED', duration: 5400, prerequisites: { type: 'AND', values: ['researchcube:energy_handling', 'researchcube:basic_optics'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:pondering_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'alloy_fabrication', name: 'Alloy Fabrication', description: 'Advanced metal alloy creation.', category: 'materials', tier: 'ADVANCED', duration: 5400, prerequisites: 'researchcube:material_synthesis', itemCosts: [], fluidCost: { fluid: 'researchcube:pondering_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'neural_interface', name: 'Neural Interface', description: 'Direct brain-computer link technology.', category: 'computing', tier: 'ADVANCED', duration: 6000, prerequisites: 'researchcube:advanced_processor', itemCosts: [], fluidCost: { fluid: 'researchcube:pondering_fluid', amount: 1000 }, ideaChip: null, recipePool: [] },
+ { id: 'waveform_analysis', name: 'Waveform Analysis', description: 'Deep analysis of quantum waveforms.', category: 'energy', tier: 'PRECISE', duration: 9600, prerequisites: 'researchcube:quantum_resonance', itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'precision_engineering', name: 'Precision Engineering', description: 'Sub-atomic precision manufacturing.', category: 'materials', tier: 'PRECISE', duration: 9000, prerequisites: 'researchcube:alloy_fabrication', itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'nanostructures', name: 'Nanostructures', description: 'Self-assembling nanoscale structures.', category: 'materials', tier: 'PRECISE', duration: 9000, prerequisites: { type: 'AND', values: ['researchcube:advanced_processor', 'researchcube:precision_engineering'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'biophotonic_matrix', name: 'Biophotonic Matrix', description: 'Organic-photonic hybrid computing.', category: 'computing', tier: 'PRECISE', duration: 9600, prerequisites: { type: 'AND', values: ['researchcube:neural_interface', 'researchcube:photonic_amplifier'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'temporal_calibration', name: 'Temporal Calibration', description: 'Fine-tuning temporal distortions.', category: 'temporal', tier: 'PRECISE', duration: 10800, prerequisites: 'researchcube:waveform_analysis', itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'molecular_assembly', name: 'Molecular Assembly', description: 'Atom-by-atom construction.', category: 'materials', tier: 'FLAWLESS', duration: 18000, prerequisites: 'researchcube:nanostructures', itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'dimensional_folding', name: 'Dimensional Folding', description: 'Bending space-time locally.', category: 'temporal', tier: 'FLAWLESS', duration: 18000, prerequisites: 'researchcube:temporal_calibration', itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'cognitive_framework', name: 'Cognitive Framework', description: 'True artificial cognition.', category: 'computing', tier: 'FLAWLESS', duration: 18000, prerequisites: { type: 'AND', values: ['researchcube:neural_interface', 'researchcube:biophotonic_matrix'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'zero_point_energy', name: 'Zero Point Energy', description: 'Extracting energy from quantum vacuum.', category: 'energy', tier: 'FLAWLESS', duration: 18000, prerequisites: { type: 'AND', values: ['researchcube:waveform_analysis', 'researchcube:dimensional_folding'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:reasoning_fluid', amount: 2000 }, ideaChip: null, recipePool: [] },
+ { id: 'singularity_core', name: 'Singularity Core', description: 'Harnessing a contained singularity.', category: 'convergence', tier: 'SELF_AWARE', duration: 36000, prerequisites: { type: 'AND', values: ['researchcube:molecular_assembly', 'researchcube:zero_point_energy'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:imagination_fluid', amount: 4000 }, ideaChip: null, recipePool: [] },
+ { id: 'transcendent_logic', name: 'Transcendent Logic', description: 'Beyond binary — beyond reason.', category: 'convergence', tier: 'SELF_AWARE', duration: 36000, prerequisites: { type: 'AND', values: ['researchcube:singularity_core', 'researchcube:cognitive_framework'] }, itemCosts: [], fluidCost: { fluid: 'researchcube:imagination_fluid', amount: 4000 }, ideaChip: null, recipePool: [] },
+ { id: 'omniscient_matrix', name: 'Omniscient Matrix', description: 'Perfect understanding of all systems.', category: 'convergence', tier: 'SELF_AWARE', duration: 72000, prerequisites: 'researchcube:transcendent_logic', itemCosts: [], fluidCost: { fluid: 'researchcube:imagination_fluid', amount: 4000 }, ideaChip: null, recipePool: [] },
+];
+
+// Pre-computed tree layout positions
+const positions: Record = {
+ unstable_signal: { x: 50, y: 50 },
+ fragmented_data: { x: 300, y: 50 },
+ basic_circuit: { x: 550, y: 50 },
+ material_synthesis: { x: 900, y: 50 },
+ energy_handling: { x: 450, y: 200 },
+ basic_optics: { x: 700, y: 200 },
+ advanced_processor: { x: 350, y: 370 },
+ quantum_resonance: { x: 550, y: 370 },
+ photonic_amplifier: { x: 700, y: 370 },
+ alloy_fabrication: { x: 900, y: 250 },
+ neural_interface: { x: 250, y: 520 },
+ waveform_analysis: { x: 550, y: 520 },
+ precision_engineering:{ x: 900, y: 420 },
+ nanostructures: { x: 800, y: 570 },
+ biophotonic_matrix: { x: 350, y: 670 },
+ temporal_calibration: { x: 550, y: 670 },
+ dimensional_folding: { x: 550, y: 820 },
+ cognitive_framework: { x: 250, y: 820 },
+ molecular_assembly: { x: 800, y: 730 },
+ zero_point_energy: { x: 550, y: 970 },
+ singularity_core: { x: 700, y: 970 },
+ transcendent_logic: { x: 500, y: 1120 },
+ omniscient_matrix: { x: 500, y: 1270 },
+};
+
+export function getExampleTree(): Record {
+ const result: Record = {};
+ for (const def of defs) {
+ result[def.id] = {
+ ...def,
+ position: positions[def.id] || { x: 0, y: 0 },
+ };
+ }
+ return result;
+}
diff --git a/src/data/fluids.ts b/src/data/fluids.ts
new file mode 100644
index 0000000..7c77a98
--- /dev/null
+++ b/src/data/fluids.ts
@@ -0,0 +1,8 @@
+export const FLUID_LIST = [
+ 'researchcube:thinking_fluid',
+ 'researchcube:pondering_fluid',
+ 'researchcube:reasoning_fluid',
+ 'researchcube:imagination_fluid',
+ 'minecraft:water',
+ 'minecraft:lava',
+];
diff --git a/src/data/items.ts b/src/data/items.ts
new file mode 100644
index 0000000..e5b45c0
--- /dev/null
+++ b/src/data/items.ts
@@ -0,0 +1,168 @@
+export const MOD_ITEMS = [
+ 'researchcube:metadata_unstable',
+ 'researchcube:metadata_reclaimed',
+ 'researchcube:metadata_enhanced',
+ 'researchcube:metadata_elaborate',
+ 'researchcube:metadata_cybernetic',
+ 'researchcube:metadata_self_aware',
+ 'researchcube:metadata_broken',
+ 'researchcube:cube_unstable',
+ 'researchcube:cube_basic',
+ 'researchcube:cube_advanced',
+ 'researchcube:cube_precise',
+ 'researchcube:cube_flawless',
+ 'researchcube:cube_self_aware',
+ 'researchcube:research_chip',
+ 'researchcube:research_book',
+ 'researchcube:research_station_item',
+ 'researchcube:drive_crafting_table',
+ 'researchcube:processing_station',
+ 'researchcube:thinking_fluid_bucket',
+ 'researchcube:pondering_fluid_bucket',
+ 'researchcube:reasoning_fluid_bucket',
+ 'researchcube:imagination_fluid_bucket',
+];
+
+export const COMMON_VANILLA_ITEMS = [
+ 'minecraft:stone',
+ 'minecraft:cobblestone',
+ 'minecraft:dirt',
+ 'minecraft:sand',
+ 'minecraft:gravel',
+ 'minecraft:glass',
+ 'minecraft:glass_pane',
+ 'minecraft:oak_log',
+ 'minecraft:oak_planks',
+ 'minecraft:stick',
+ 'minecraft:coal',
+ 'minecraft:charcoal',
+ 'minecraft:iron_ingot',
+ 'minecraft:iron_nugget',
+ 'minecraft:iron_block',
+ 'minecraft:raw_iron',
+ 'minecraft:gold_ingot',
+ 'minecraft:gold_nugget',
+ 'minecraft:gold_block',
+ 'minecraft:raw_gold',
+ 'minecraft:copper_ingot',
+ 'minecraft:raw_copper',
+ 'minecraft:copper_block',
+ 'minecraft:diamond',
+ 'minecraft:diamond_block',
+ 'minecraft:emerald',
+ 'minecraft:emerald_block',
+ 'minecraft:lapis_lazuli',
+ 'minecraft:lapis_block',
+ 'minecraft:netherite_ingot',
+ 'minecraft:netherite_scrap',
+ 'minecraft:ancient_debris',
+ 'minecraft:amethyst_shard',
+ 'minecraft:quartz',
+ 'minecraft:quartz_block',
+ 'minecraft:redstone',
+ 'minecraft:redstone_block',
+ 'minecraft:redstone_torch',
+ 'minecraft:repeater',
+ 'minecraft:comparator',
+ 'minecraft:observer',
+ 'minecraft:piston',
+ 'minecraft:sticky_piston',
+ 'minecraft:hopper',
+ 'minecraft:dropper',
+ 'minecraft:dispenser',
+ 'minecraft:lever',
+ 'minecraft:stone_button',
+ 'minecraft:glowstone',
+ 'minecraft:glowstone_dust',
+ 'minecraft:blaze_rod',
+ 'minecraft:blaze_powder',
+ 'minecraft:ender_pearl',
+ 'minecraft:ender_eye',
+ 'minecraft:nether_star',
+ 'minecraft:ghast_tear',
+ 'minecraft:magma_cream',
+ 'minecraft:slime_ball',
+ 'minecraft:slime_block',
+ 'minecraft:string',
+ 'minecraft:leather',
+ 'minecraft:feather',
+ 'minecraft:bone',
+ 'minecraft:bone_meal',
+ 'minecraft:gunpowder',
+ 'minecraft:paper',
+ 'minecraft:book',
+ 'minecraft:clay_ball',
+ 'minecraft:brick',
+ 'minecraft:nether_brick',
+ 'minecraft:prismarine_shard',
+ 'minecraft:prismarine_crystals',
+ 'minecraft:sea_lantern',
+ 'minecraft:obsidian',
+ 'minecraft:crying_obsidian',
+ 'minecraft:end_stone',
+ 'minecraft:netherrack',
+ 'minecraft:soul_sand',
+ 'minecraft:basalt',
+ 'minecraft:blackstone',
+ 'minecraft:chest',
+ 'minecraft:barrel',
+ 'minecraft:furnace',
+ 'minecraft:blast_furnace',
+ 'minecraft:smoker',
+ 'minecraft:crafting_table',
+ 'minecraft:anvil',
+ 'minecraft:enchanting_table',
+ 'minecraft:brewing_stand',
+ 'minecraft:cauldron',
+ 'minecraft:bucket',
+ 'minecraft:water_bucket',
+ 'minecraft:lava_bucket',
+ 'minecraft:iron_sword',
+ 'minecraft:iron_pickaxe',
+ 'minecraft:iron_axe',
+ 'minecraft:iron_shovel',
+ 'minecraft:diamond_sword',
+ 'minecraft:diamond_pickaxe',
+ 'minecraft:diamond_axe',
+ 'minecraft:netherite_sword',
+ 'minecraft:netherite_pickaxe',
+ 'minecraft:wheat',
+ 'minecraft:sugar',
+ 'minecraft:sugar_cane',
+ 'minecraft:egg',
+ 'minecraft:apple',
+ 'minecraft:golden_apple',
+ 'minecraft:enchanted_golden_apple',
+ 'minecraft:spider_eye',
+ 'minecraft:fermented_spider_eye',
+ 'minecraft:phantom_membrane',
+ 'minecraft:rabbit_hide',
+ 'minecraft:honeycomb',
+ 'minecraft:honey_bottle',
+ 'minecraft:ink_sac',
+ 'minecraft:glow_ink_sac',
+ 'minecraft:dye',
+ 'minecraft:torch',
+ 'minecraft:lantern',
+ 'minecraft:chain',
+ 'minecraft:iron_bars',
+ 'minecraft:rail',
+ 'minecraft:powered_rail',
+ 'minecraft:minecart',
+ 'minecraft:tnt',
+ 'minecraft:beacon',
+ 'minecraft:conduit',
+ 'minecraft:shulker_shell',
+ 'minecraft:nautilus_shell',
+ 'minecraft:heart_of_the_sea',
+ 'minecraft:echo_shard',
+ 'minecraft:disc_fragment_5',
+ 'minecraft:sculk_catalyst',
+ 'minecraft:sculk_shrieker',
+ 'minecraft:sculk_sensor',
+ 'minecraft:copper_bulb',
+ 'minecraft:trial_key',
+ 'minecraft:breeze_rod',
+];
+
+export const ALL_ITEMS = [...MOD_ITEMS, ...COMMON_VANILLA_ITEMS];
diff --git a/src/data/tiers.ts b/src/data/tiers.ts
new file mode 100644
index 0000000..fcfcb23
--- /dev/null
+++ b/src/data/tiers.ts
@@ -0,0 +1,77 @@
+import type { Tier } from '../types/research';
+
+export interface TierInfo {
+ name: Tier;
+ ordinal: number;
+ fluid: string;
+ drive: string;
+ cube: string;
+ maxRecipes: number | null;
+ color: string;
+}
+
+export const TIER_INFO: Record = {
+ UNSTABLE: {
+ name: 'UNSTABLE',
+ ordinal: 0,
+ fluid: 'researchcube:thinking_fluid',
+ drive: 'researchcube:metadata_unstable',
+ cube: 'researchcube:cube_unstable',
+ maxRecipes: 2,
+ color: '#6B7280',
+ },
+ BASIC: {
+ name: 'BASIC',
+ ordinal: 1,
+ fluid: 'researchcube:thinking_fluid',
+ drive: 'researchcube:metadata_reclaimed',
+ cube: 'researchcube:cube_basic',
+ maxRecipes: 4,
+ color: '#10B981',
+ },
+ ADVANCED: {
+ name: 'ADVANCED',
+ ordinal: 2,
+ fluid: 'researchcube:pondering_fluid',
+ drive: 'researchcube:metadata_enhanced',
+ cube: 'researchcube:cube_advanced',
+ maxRecipes: 8,
+ color: '#3B82F6',
+ },
+ PRECISE: {
+ name: 'PRECISE',
+ ordinal: 3,
+ fluid: 'researchcube:reasoning_fluid',
+ drive: 'researchcube:metadata_elaborate',
+ cube: 'researchcube:cube_precise',
+ maxRecipes: 12,
+ color: '#F59E0B',
+ },
+ FLAWLESS: {
+ name: 'FLAWLESS',
+ ordinal: 4,
+ fluid: 'researchcube:reasoning_fluid',
+ drive: 'researchcube:metadata_cybernetic',
+ cube: 'researchcube:cube_flawless',
+ maxRecipes: 16,
+ color: '#8B5CF6',
+ },
+ SELF_AWARE: {
+ name: 'SELF_AWARE',
+ ordinal: 5,
+ fluid: 'researchcube:imagination_fluid',
+ drive: 'researchcube:metadata_self_aware',
+ cube: 'researchcube:cube_self_aware',
+ maxRecipes: null,
+ color: '#EF4444',
+ },
+};
+
+export const TIER_FLUIDS: Record = {
+ UNSTABLE: 'researchcube:thinking_fluid',
+ BASIC: 'researchcube:thinking_fluid',
+ ADVANCED: 'researchcube:pondering_fluid',
+ PRECISE: 'researchcube:reasoning_fluid',
+ FLAWLESS: 'researchcube:reasoning_fluid',
+ SELF_AWARE: 'researchcube:imagination_fluid',
+};
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..037f37c
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,161 @@
+:root {
+ --bg-primary: #0f1117;
+ --bg-secondary: #1a1d27;
+ --bg-tertiary: #242835;
+ --bg-hover: #2d3142;
+ --border: #363a4e;
+ --border-focus: #5b6abf;
+ --text-primary: #e1e4ed;
+ --text-secondary: #9ca3b8;
+ --text-muted: #6b7280;
+ --accent: #5b6abf;
+ --accent-hover: #6e7dd4;
+ --error: #ef4444;
+ --warning: #f59e0b;
+ --success: #10b981;
+
+ --tier-unstable: #6b7280;
+ --tier-basic: #10b981;
+ --tier-advanced: #3b82f6;
+ --tier-precise: #f59e0b;
+ --tier-flawless: #8b5cf6;
+ --tier-self-aware: #ef4444;
+
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+
+ --radius-sm: 4px;
+ --radius-md: 6px;
+ --radius-lg: 8px;
+
+ --shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+*, *::before, *::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html, body, #root {
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+}
+
+body {
+ font-family: var(--font-sans);
+ font-size: 14px;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+input, select, textarea, button {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+input[type="text"],
+input[type="number"],
+select,
+textarea {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ padding: 6px 10px;
+ outline: none;
+ transition: border-color 0.15s;
+ width: 100%;
+}
+
+input[type="text"]:focus,
+input[type="number"]:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--border-focus);
+}
+
+input[type="number"] {
+ -moz-appearance: textfield;
+}
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+}
+
+button {
+ cursor: pointer;
+ border: none;
+ border-radius: var(--radius-sm);
+ padding: 6px 14px;
+ font-size: 13px;
+ transition: background-color 0.15s, opacity 0.15s;
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: #fff;
+}
+.btn-primary:hover:not(:disabled) {
+ background: var(--accent-hover);
+}
+
+.btn-secondary {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border);
+}
+.btn-secondary:hover:not(:disabled) {
+ background: var(--bg-hover);
+}
+
+.btn-danger {
+ background: var(--error);
+ color: #fff;
+}
+.btn-danger:hover:not(:disabled) {
+ background: #dc2626;
+}
+
+.btn-sm {
+ padding: 3px 8px;
+ font-size: 12px;
+}
+
+.btn-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px;
+ background: none;
+ color: var(--text-secondary);
+ border-radius: var(--radius-sm);
+}
+.btn-icon:hover:not(:disabled) {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 4px;
+}
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-muted);
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/src/stores/recipeStore.ts b/src/stores/recipeStore.ts
new file mode 100644
index 0000000..e2699a4
--- /dev/null
+++ b/src/stores/recipeStore.ts
@@ -0,0 +1,54 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import { immer } from 'zustand/middleware/immer';
+import type { RecipeData } from '../types/recipe';
+
+interface RecipeStoreState {
+ recipes: Record;
+}
+
+interface RecipeStoreActions {
+ addRecipe: (recipe: RecipeData) => void;
+ updateRecipe: (id: string, recipe: RecipeData) => void;
+ removeRecipe: (id: string) => void;
+ setRecipes: (recipes: Record) => void;
+ clearAll: () => void;
+}
+
+export type RecipeStore = RecipeStoreState & RecipeStoreActions;
+
+export const useRecipeStore = create()(
+ persist(
+ immer((set) => ({
+ recipes: {},
+
+ addRecipe: (recipe) =>
+ set((state) => {
+ state.recipes[recipe.id] = recipe;
+ }),
+
+ updateRecipe: (id, recipe) =>
+ set((state) => {
+ state.recipes[id] = recipe;
+ }),
+
+ removeRecipe: (id) =>
+ set((state) => {
+ delete state.recipes[id];
+ }),
+
+ setRecipes: (recipes) =>
+ set((state) => {
+ state.recipes = recipes;
+ }),
+
+ clearAll: () =>
+ set((state) => {
+ state.recipes = {};
+ }),
+ })),
+ {
+ name: 'researchcube-recipe-store',
+ }
+ )
+);
diff --git a/src/stores/researchStore.ts b/src/stores/researchStore.ts
new file mode 100644
index 0000000..77c1d2a
--- /dev/null
+++ b/src/stores/researchStore.ts
@@ -0,0 +1,130 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import { immer } from 'zustand/middleware/immer';
+import type { ResearchNodeData, Prerequisite } from '../types/research';
+
+interface ResearchStoreState {
+ nodes: Record;
+}
+
+interface ResearchStoreActions {
+ addNode: (node: ResearchNodeData) => void;
+ updateNode: (id: string, updates: Partial) => void;
+ removeNode: (id: string) => void;
+ updateNodePosition: (id: string, position: { x: number; y: number }) => void;
+ addPrerequisite: (targetId: string, sourceId: string) => void;
+ removePrerequisite: (targetId: string, sourceId: string) => void;
+ setPrerequisiteMode: (nodeId: string, mode: 'AND' | 'OR') => void;
+ setNodes: (nodes: Record) => void;
+ clearAll: () => void;
+}
+
+export type ResearchStore = ResearchStoreState & ResearchStoreActions;
+
+export const useResearchStore = create()(
+ persist(
+ immer((set) => ({
+ nodes: {},
+
+ addNode: (node) =>
+ set((state) => {
+ state.nodes[node.id] = node;
+ }),
+
+ updateNode: (id, updates) =>
+ set((state) => {
+ if (state.nodes[id]) {
+ Object.assign(state.nodes[id], updates);
+ }
+ }),
+
+ removeNode: (id) =>
+ set((state) => {
+ delete state.nodes[id];
+ // Clean up prerequisites referencing this node
+ const fullId = `researchcube:${id}`;
+ for (const node of Object.values(state.nodes)) {
+ if (!node.prerequisites) continue;
+ node.prerequisites = removePrereqReference(node.prerequisites, fullId);
+ }
+ }),
+
+ updateNodePosition: (id, position) =>
+ set((state) => {
+ if (state.nodes[id]) {
+ state.nodes[id].position = position;
+ }
+ }),
+
+ addPrerequisite: (targetId, sourceId) =>
+ set((state) => {
+ const node = state.nodes[targetId];
+ if (!node) return;
+ const fullSourceId = `researchcube:${sourceId}`;
+
+ if (!node.prerequisites) {
+ node.prerequisites = fullSourceId;
+ } else if (typeof node.prerequisites === 'string') {
+ if (node.prerequisites === fullSourceId) return;
+ node.prerequisites = {
+ type: 'AND',
+ values: [node.prerequisites, fullSourceId],
+ };
+ } else {
+ const flat = flattenStrings(node.prerequisites);
+ if (flat.includes(fullSourceId)) return;
+ node.prerequisites.values.push(fullSourceId);
+ }
+ }),
+
+ removePrerequisite: (targetId, sourceId) =>
+ set((state) => {
+ const node = state.nodes[targetId];
+ if (!node || !node.prerequisites) return;
+ const fullSourceId = `researchcube:${sourceId}`;
+ node.prerequisites = removePrereqReference(node.prerequisites, fullSourceId);
+ }),
+
+ setPrerequisiteMode: (nodeId, mode) =>
+ set((state) => {
+ const node = state.nodes[nodeId];
+ if (!node || !node.prerequisites || typeof node.prerequisites === 'string') return;
+ node.prerequisites.type = mode;
+ }),
+
+ setNodes: (nodes) =>
+ set((state) => {
+ state.nodes = nodes;
+ }),
+
+ clearAll: () =>
+ set((state) => {
+ state.nodes = {};
+ }),
+ })),
+ {
+ name: 'researchcube-research-store',
+ }
+ )
+);
+
+function flattenStrings(prereq: Prerequisite): string[] {
+ if (typeof prereq === 'string') return [prereq];
+ return prereq.values.flatMap(flattenStrings);
+}
+
+function removePrereqReference(
+ prereq: Prerequisite,
+ idToRemove: string
+): Prerequisite | null {
+ if (typeof prereq === 'string') {
+ return prereq === idToRemove ? null : prereq;
+ }
+ const filtered = prereq.values
+ .map((v) => removePrereqReference(v, idToRemove))
+ .filter((v): v is Prerequisite => v !== null);
+
+ if (filtered.length === 0) return null;
+ if (filtered.length === 1) return filtered[0];
+ return { type: prereq.type, values: filtered };
+}
diff --git a/src/stores/uiStore.ts b/src/stores/uiStore.ts
new file mode 100644
index 0000000..846f871
--- /dev/null
+++ b/src/stores/uiStore.ts
@@ -0,0 +1,67 @@
+import { create } from 'zustand';
+
+interface UIStoreState {
+ selectedNodeId: string | null;
+ selectedRecipeId: string | null;
+ editorPanel: 'research' | 'recipe' | 'none';
+ validationDialogOpen: boolean;
+ importDialogOpen: boolean;
+ exportDialogOpen: boolean;
+ confirmDialog: { open: boolean; title: string; message: string; onConfirm: (() => void) | null };
+}
+
+interface UIStoreActions {
+ selectNode: (id: string | null) => void;
+ selectRecipe: (id: string | null) => void;
+ setEditorPanel: (panel: 'research' | 'recipe' | 'none') => void;
+ openValidationDialog: () => void;
+ closeValidationDialog: () => void;
+ openImportDialog: () => void;
+ closeImportDialog: () => void;
+ openExportDialog: () => void;
+ closeExportDialog: () => void;
+ showConfirm: (title: string, message: string, onConfirm: () => void) => void;
+ closeConfirm: () => void;
+}
+
+export type UIStore = UIStoreState & UIStoreActions;
+
+export const useUIStore = create()((set) => ({
+ selectedNodeId: null,
+ selectedRecipeId: null,
+ editorPanel: 'none',
+ validationDialogOpen: false,
+ importDialogOpen: false,
+ exportDialogOpen: false,
+ confirmDialog: { open: false, title: '', message: '', onConfirm: null },
+
+ selectNode: (id) =>
+ set({
+ selectedNodeId: id,
+ selectedRecipeId: null,
+ editorPanel: id ? 'research' : 'none',
+ }),
+
+ selectRecipe: (id) =>
+ set({
+ selectedRecipeId: id,
+ selectedNodeId: null,
+ editorPanel: id ? 'recipe' : 'none',
+ }),
+
+ setEditorPanel: (panel) => set({ editorPanel: panel }),
+
+ openValidationDialog: () => set({ validationDialogOpen: true }),
+ closeValidationDialog: () => set({ validationDialogOpen: false }),
+
+ openImportDialog: () => set({ importDialogOpen: true }),
+ closeImportDialog: () => set({ importDialogOpen: false }),
+
+ openExportDialog: () => set({ exportDialogOpen: true }),
+ closeExportDialog: () => set({ exportDialogOpen: false }),
+
+ showConfirm: (title, message, onConfirm) =>
+ set({ confirmDialog: { open: true, title, message, onConfirm } }),
+ closeConfirm: () =>
+ set({ confirmDialog: { open: false, title: '', message: '', onConfirm: null } }),
+}));
diff --git a/src/types/common.ts b/src/types/common.ts
new file mode 100644
index 0000000..ea8dec8
--- /dev/null
+++ b/src/types/common.ts
@@ -0,0 +1,26 @@
+/** Minecraft ResourceLocation format: "namespace:path" */
+export type ResourceLocation = string;
+
+export interface ItemCost {
+ item: ResourceLocation;
+ count: number;
+}
+
+export interface FluidCost {
+ fluid: ResourceLocation;
+ amount: number;
+}
+
+export interface Ingredient {
+ item: ResourceLocation;
+}
+
+export interface ItemStack {
+ id: ResourceLocation;
+ count: number;
+}
+
+export interface FluidStack {
+ fluid: ResourceLocation;
+ amount: number;
+}
diff --git a/src/types/recipe.ts b/src/types/recipe.ts
new file mode 100644
index 0000000..a9018da
--- /dev/null
+++ b/src/types/recipe.ts
@@ -0,0 +1,67 @@
+import type { Ingredient, ItemStack, FluidStack } from './common';
+
+export type RecipeKind = 'drive_crafting_shapeless' | 'drive_crafting_shaped' | 'processing';
+
+export interface DriveCraftingShapelessRecipe {
+ kind: 'drive_crafting_shapeless';
+ id: string;
+ ingredients: Ingredient[];
+ result: ItemStack;
+ group: string;
+}
+
+export interface DriveCraftingShapedRecipe {
+ kind: 'drive_crafting_shaped';
+ id: string;
+ pattern: [string, string, string];
+ key: Record;
+ result: ItemStack;
+ group: string;
+}
+
+export interface ProcessingRecipe {
+ kind: 'processing';
+ id: string;
+ inputs: Ingredient[];
+ fluidInputs: FluidStack[];
+ outputs: ItemStack[];
+ fluidOutput: FluidStack | null;
+ duration: number;
+ group: string;
+}
+
+export type RecipeData = DriveCraftingShapelessRecipe | DriveCraftingShapedRecipe | ProcessingRecipe;
+
+export function createDefaultShapelessRecipe(id: string): DriveCraftingShapelessRecipe {
+ return {
+ kind: 'drive_crafting_shapeless',
+ id,
+ ingredients: [{ item: 'minecraft:stone' }],
+ result: { id: 'minecraft:stone', count: 1 },
+ group: '',
+ };
+}
+
+export function createDefaultShapedRecipe(id: string): DriveCraftingShapedRecipe {
+ return {
+ kind: 'drive_crafting_shaped',
+ id,
+ pattern: [' ', ' ', ' '],
+ key: {},
+ result: { id: 'minecraft:stone', count: 1 },
+ group: '',
+ };
+}
+
+export function createDefaultProcessingRecipe(id: string): ProcessingRecipe {
+ return {
+ kind: 'processing',
+ id,
+ inputs: [{ item: 'minecraft:stone' }],
+ fluidInputs: [],
+ outputs: [{ id: 'minecraft:stone', count: 1 }],
+ fluidOutput: null,
+ duration: 200,
+ group: '',
+ };
+}
diff --git a/src/types/research.ts b/src/types/research.ts
new file mode 100644
index 0000000..889b4c4
--- /dev/null
+++ b/src/types/research.ts
@@ -0,0 +1,57 @@
+import type { ItemCost, FluidCost } from './common';
+
+export const TIERS = ['UNSTABLE', 'BASIC', 'ADVANCED', 'PRECISE', 'FLAWLESS', 'SELF_AWARE'] as const;
+export type Tier = typeof TIERS[number];
+
+export const CATEGORIES = ['circuits', 'energy', 'optics', 'materials', 'signals', 'temporal', 'computing', 'convergence'] as const;
+
+export type Prerequisite = string | CompoundPrerequisite;
+
+export interface CompoundPrerequisite {
+ type: 'AND' | 'OR';
+ values: Prerequisite[];
+}
+
+export interface IdeaChipComponents {
+ 'minecraft:custom_name'?: string;
+ 'minecraft:custom_data'?: Record;
+}
+
+export interface IdeaChip {
+ item: string;
+ components?: IdeaChipComponents;
+}
+
+export type WeightedRecipe = string | { id: string; weight: number };
+
+export interface ResearchNodeData {
+ id: string;
+ name: string;
+ description: string;
+ category: string;
+ tier: Tier;
+ duration: number;
+ prerequisites: Prerequisite | null;
+ itemCosts: ItemCost[];
+ fluidCost: FluidCost | null;
+ ideaChip: IdeaChip | null;
+ recipePool: WeightedRecipe[];
+ position: { x: number; y: number };
+}
+
+export function createDefaultResearchNode(id: string, position: { x: number; y: number }): ResearchNodeData {
+ return {
+ id,
+ name: '',
+ description: '',
+ category: '',
+ tier: 'BASIC',
+ duration: 1200,
+ prerequisites: null,
+ itemCosts: [],
+ fluidCost: null,
+ ideaChip: null,
+ recipePool: [],
+ position,
+ };
+}
diff --git a/src/types/validation.ts b/src/types/validation.ts
new file mode 100644
index 0000000..98b8667
--- /dev/null
+++ b/src/types/validation.ts
@@ -0,0 +1,10 @@
+export type ValidationSeverity = 'error' | 'warning';
+export type ValidationTarget = 'research' | 'recipe';
+
+export interface ValidationIssue {
+ severity: ValidationSeverity;
+ target: ValidationTarget;
+ targetId: string;
+ field: string;
+ message: string;
+}
diff --git a/src/utils/exportZip.ts b/src/utils/exportZip.ts
new file mode 100644
index 0000000..7ce0f74
--- /dev/null
+++ b/src/utils/exportZip.ts
@@ -0,0 +1,45 @@
+import JSZip from 'jszip';
+import { saveAs } from 'file-saver';
+import type { ResearchNodeData } from '../types/research';
+import type { RecipeData } from '../types/recipe';
+import { serializeResearch, serializeRecipe, serializeAdvancement } from './serialization';
+
+export async function exportToZip(
+ nodes: Record,
+ recipes: Record,
+ includeAdvancements: boolean
+): Promise {
+ const zip = new JSZip();
+
+ // Research definition files
+ for (const node of Object.values(nodes)) {
+ const json = serializeResearch(node);
+ zip.file(
+ `data/researchcube/research/${node.id}.json`,
+ JSON.stringify(json, null, 2)
+ );
+ }
+
+ // Recipe files
+ for (const recipe of Object.values(recipes)) {
+ const json = serializeRecipe(recipe);
+ zip.file(
+ `data/researchcube/recipe/${recipe.id}.json`,
+ JSON.stringify(json, null, 2)
+ );
+ }
+
+ // Advancement files (optional)
+ if (includeAdvancements) {
+ for (const node of Object.values(nodes)) {
+ const json = serializeAdvancement(node, nodes);
+ zip.file(
+ `data/researchcube/advancement/research/${node.id}.json`,
+ JSON.stringify(json, null, 2)
+ );
+ }
+ }
+
+ const blob = await zip.generateAsync({ type: 'blob' });
+ saveAs(blob, 'researchcube-datapack.zip');
+}
diff --git a/src/utils/idHelpers.ts b/src/utils/idHelpers.ts
new file mode 100644
index 0000000..27b0bfa
--- /dev/null
+++ b/src/utils/idHelpers.ts
@@ -0,0 +1,29 @@
+export function toSnakeCase(str: string): string {
+ return str
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
+ .replace(/[\s\-]+/g, '_')
+ .replace(/[^a-zA-Z0-9_]/g, '')
+ .toLowerCase();
+}
+
+export function toResearchId(snakeCaseId: string): string {
+ return `researchcube:${snakeCaseId}`;
+}
+
+export function stripNamespace(fullId: string): string {
+ const idx = fullId.indexOf(':');
+ return idx >= 0 ? fullId.substring(idx + 1) : fullId;
+}
+
+let nextCounter = 1;
+
+export function generateUniqueId(baseName: string, existingIds: Set): string {
+ let candidate = toSnakeCase(baseName);
+ if (!candidate) candidate = 'new_research';
+ if (!existingIds.has(candidate)) return candidate;
+
+ while (existingIds.has(`${candidate}_${nextCounter}`)) {
+ nextCounter++;
+ }
+ return `${candidate}_${nextCounter++}`;
+}
diff --git a/src/utils/importJson.ts b/src/utils/importJson.ts
new file mode 100644
index 0000000..2f31d36
--- /dev/null
+++ b/src/utils/importJson.ts
@@ -0,0 +1,133 @@
+import JSZip from 'jszip';
+import type { ResearchNodeData, Tier, Prerequisite } from '../types/research';
+import type { RecipeData } from '../types/recipe';
+import { TIERS } from '../types/research';
+
+export async function importFiles(
+ files: FileList
+): Promise<{ nodes: Record; recipes: Record }> {
+ const nodes: Record = {};
+ const recipes: Record = {};
+
+ for (const file of Array.from(files)) {
+ if (file.name.endsWith('.zip')) {
+ const zip = await JSZip.loadAsync(file);
+ for (const [path, zipEntry] of Object.entries(zip.files)) {
+ if (zipEntry.dir || !path.endsWith('.json')) continue;
+ const text = await zipEntry.async('text');
+ const json = JSON.parse(text);
+ const filename = path.split('/').pop()!.replace('.json', '');
+
+ if (path.includes('/research/') && !path.includes('/advancement/')) {
+ const node = parseResearchJson(filename, json);
+ if (node) nodes[node.id] = node;
+ } else if (path.includes('/recipe/')) {
+ const recipe = parseRecipeJson(filename, json);
+ if (recipe) recipes[recipe.id] = recipe;
+ }
+ }
+ } else if (file.name.endsWith('.json')) {
+ const text = await file.text();
+ const json = JSON.parse(text);
+ const filename = file.name.replace('.json', '');
+
+ // Auto-detect file type
+ if (json.tier && TIERS.includes(json.tier)) {
+ const node = parseResearchJson(filename, json);
+ if (node) nodes[node.id] = node;
+ } else if (json.type) {
+ const recipe = parseRecipeJson(filename, json);
+ if (recipe) recipes[recipe.id] = recipe;
+ }
+ }
+ }
+
+ // Auto-layout imported nodes
+ autoLayoutNodes(nodes);
+
+ return { nodes, recipes };
+}
+
+function parseResearchJson(id: string, json: Record): ResearchNodeData | null {
+ if (!json.tier) return null;
+
+ return {
+ id,
+ name: (json.name as string) || '',
+ description: (json.description as string) || '',
+ category: (json.category as string) || '',
+ tier: json.tier as Tier,
+ duration: (json.duration as number) || 1200,
+ prerequisites: (json.prerequisites as Prerequisite) || null,
+ itemCosts: (json.item_costs as Array<{ item: string; count?: number }>)?.map((ic) => ({
+ item: ic.item,
+ count: ic.count ?? 1,
+ })) || [],
+ fluidCost: json.fluid_cost
+ ? {
+ fluid: (json.fluid_cost as { fluid: string }).fluid,
+ amount: (json.fluid_cost as { amount?: number }).amount ?? 1000,
+ }
+ : null,
+ ideaChip: json.idea_chip
+ ? {
+ item: (json.idea_chip as { item: string }).item,
+ components: (json.idea_chip as { components?: Record }).components as ResearchNodeData['ideaChip'] extends null ? never : NonNullable['components'],
+ }
+ : null,
+ recipePool: (json.recipe_pool as ResearchNodeData['recipePool']) || [],
+ position: { x: 0, y: 0 },
+ };
+}
+
+function parseRecipeJson(id: string, json: Record): RecipeData | null {
+ const type = json.type as string;
+
+ if (type === 'researchcube:drive_crafting') {
+ if (json.pattern) {
+ return {
+ kind: 'drive_crafting_shaped',
+ id,
+ pattern: json.pattern as [string, string, string],
+ key: json.key as Record,
+ result: json.result as { id: string; count: number },
+ group: (json.group as string) || '',
+ };
+ }
+ return {
+ kind: 'drive_crafting_shapeless',
+ id,
+ ingredients: json.ingredients as Array<{ item: string }>,
+ result: json.result as { id: string; count: number },
+ group: (json.group as string) || '',
+ };
+ }
+
+ if (type === 'researchcube:processing') {
+ return {
+ kind: 'processing',
+ id,
+ inputs: (json.inputs as Array<{ item: string }>) || [],
+ fluidInputs: (json.fluid_inputs as Array<{ fluid: string; amount: number }>) || [],
+ outputs: json.outputs as Array<{ id: string; count: number }>,
+ fluidOutput: json.fluid_output
+ ? (json.fluid_output as { fluid: string; amount: number })
+ : null,
+ duration: (json.duration as number) || 200,
+ group: (json.group as string) || '',
+ };
+ }
+
+ return null;
+}
+
+function autoLayoutNodes(nodes: Record) {
+ const ids = Object.keys(nodes);
+ const cols = Math.ceil(Math.sqrt(ids.length));
+ ids.forEach((id, i) => {
+ nodes[id].position = {
+ x: (i % cols) * 250 + 100,
+ y: Math.floor(i / cols) * 200 + 100,
+ };
+ });
+}
diff --git a/src/utils/prerequisites.ts b/src/utils/prerequisites.ts
new file mode 100644
index 0000000..366de4a
--- /dev/null
+++ b/src/utils/prerequisites.ts
@@ -0,0 +1,83 @@
+import type { Edge } from '@xyflow/react';
+import type { Prerequisite, ResearchNodeData } from '../types/research';
+import { stripNamespace } from './idHelpers';
+
+export function flattenPrerequisiteIds(prereq: Prerequisite): string[] {
+ if (typeof prereq === 'string') return [prereq];
+ return prereq.values.flatMap(flattenPrerequisiteIds);
+}
+
+export function getPrerequisiteMode(prereq: Prerequisite | null): 'AND' | 'OR' | null {
+ if (!prereq || typeof prereq === 'string') return null;
+ return prereq.type;
+}
+
+export function deriveEdges(nodes: Record): Edge[] {
+ const edges: Edge[] = [];
+
+ for (const node of Object.values(nodes)) {
+ if (!node.prerequisites) continue;
+ const ids = flattenPrerequisiteIds(node.prerequisites);
+ const mode = typeof node.prerequisites === 'object' ? node.prerequisites.type : null;
+
+ for (const sourceFullId of ids) {
+ const sourceId = stripNamespace(sourceFullId);
+ if (!nodes[sourceId]) continue;
+
+ edges.push({
+ id: `${sourceId}->${node.id}`,
+ source: sourceId,
+ target: node.id,
+ type: 'prerequisite',
+ data: { mode },
+ animated: mode === 'OR',
+ });
+ }
+ }
+
+ return edges;
+}
+
+export function detectCycles(nodes: Record): string[][] {
+ const cycles: string[][] = [];
+ const WHITE = 0, GRAY = 1, BLACK = 2;
+ const color: Record = {};
+ const parent: Record = {};
+
+ for (const id of Object.keys(nodes)) {
+ color[id] = WHITE;
+ }
+
+ function dfs(nodeId: string, path: string[]): void {
+ color[nodeId] = GRAY;
+ path.push(nodeId);
+
+ const node = nodes[nodeId];
+ if (node.prerequisites) {
+ const prereqIds = flattenPrerequisiteIds(node.prerequisites).map(stripNamespace);
+ for (const prereqId of prereqIds) {
+ if (!nodes[prereqId]) continue;
+ if (color[prereqId] === GRAY) {
+ // Found a cycle - extract it
+ const cycleStart = path.indexOf(prereqId);
+ cycles.push(path.slice(cycleStart));
+ return;
+ }
+ if (color[prereqId] === WHITE) {
+ parent[prereqId] = nodeId;
+ dfs(prereqId, [...path]);
+ }
+ }
+ }
+
+ color[nodeId] = BLACK;
+ }
+
+ for (const id of Object.keys(nodes)) {
+ if (color[id] === WHITE) {
+ dfs(id, []);
+ }
+ }
+
+ return cycles;
+}
diff --git a/src/utils/serialization.ts b/src/utils/serialization.ts
new file mode 100644
index 0000000..131c900
--- /dev/null
+++ b/src/utils/serialization.ts
@@ -0,0 +1,129 @@
+import type { ResearchNodeData, Prerequisite } from '../types/research';
+import type { RecipeData } from '../types/recipe';
+import { TIER_INFO } from '../data/tiers';
+import { flattenPrerequisiteIds } from './prerequisites';
+import { stripNamespace } from './idHelpers';
+
+export function serializeResearch(node: ResearchNodeData): Record {
+ const json: Record = {};
+
+ if (node.name) json.name = node.name;
+ if (node.description) json.description = node.description;
+ if (node.category) json.category = node.category;
+
+ json.tier = node.tier;
+ json.duration = node.duration;
+
+ if (node.prerequisites) {
+ json.prerequisites = serializePrerequisites(node.prerequisites);
+ }
+
+ if (node.itemCosts.length > 0) {
+ json.item_costs = node.itemCosts.map((ic) => {
+ const entry: Record = { item: ic.item };
+ if (ic.count !== 1) entry.count = ic.count;
+ return entry;
+ });
+ }
+
+ if (node.fluidCost) {
+ json.fluid_cost = {
+ fluid: node.fluidCost.fluid,
+ amount: node.fluidCost.amount,
+ };
+ }
+
+ if (node.ideaChip) {
+ const chip: Record = { item: node.ideaChip.item };
+ if (node.ideaChip.components && Object.keys(node.ideaChip.components).length > 0) {
+ chip.components = node.ideaChip.components;
+ }
+ json.idea_chip = chip;
+ }
+
+ if (node.recipePool.length > 0) {
+ json.recipe_pool = node.recipePool;
+ }
+
+ return json;
+}
+
+function serializePrerequisites(prereq: Prerequisite): unknown {
+ if (typeof prereq === 'string') return prereq;
+ return {
+ type: prereq.type,
+ values: prereq.values.map(serializePrerequisites),
+ };
+}
+
+export function serializeRecipe(recipe: RecipeData): Record {
+ if (recipe.kind === 'drive_crafting_shapeless') {
+ const json: Record = {
+ type: 'researchcube:drive_crafting',
+ recipe_id: `researchcube:${recipe.id}`,
+ ingredients: recipe.ingredients,
+ result: recipe.result,
+ };
+ if (recipe.group) json.group = recipe.group;
+ return json;
+ }
+
+ if (recipe.kind === 'drive_crafting_shaped') {
+ const json: Record = {
+ type: 'researchcube:drive_crafting',
+ recipe_id: `researchcube:${recipe.id}`,
+ pattern: recipe.pattern,
+ key: recipe.key,
+ result: recipe.result,
+ };
+ if (recipe.group) json.group = recipe.group;
+ return json;
+ }
+
+ // Processing
+ const json: Record = {
+ type: 'researchcube:processing',
+ };
+ if (recipe.inputs.length > 0) json.inputs = recipe.inputs;
+ if (recipe.fluidInputs.length > 0) json.fluid_inputs = recipe.fluidInputs;
+ json.outputs = recipe.outputs;
+ if (recipe.fluidOutput) json.fluid_output = recipe.fluidOutput;
+ json.duration = recipe.duration;
+ if (recipe.group) json.group = recipe.group;
+ return json;
+}
+
+export function serializeAdvancement(
+ node: ResearchNodeData,
+ _allNodes: Record
+): Record {
+ const tierInfo = TIER_INFO[node.tier];
+
+ // Determine parent
+ let parent = 'researchcube:root';
+ if (node.prerequisites) {
+ const ids = flattenPrerequisiteIds(node.prerequisites);
+ if (ids.length > 0) {
+ parent = `researchcube:research/${stripNamespace(ids[0])}`;
+ }
+ }
+
+ return {
+ parent,
+ display: {
+ icon: { id: tierInfo.cube },
+ title: node.name || node.id,
+ description: node.description || '',
+ show_toast: true,
+ announce_to_chat: true,
+ },
+ criteria: {
+ complete: {
+ trigger: 'researchcube:complete_research',
+ conditions: {
+ research_id: `researchcube:${node.id}`,
+ },
+ },
+ },
+ };
+}
diff --git a/src/utils/tickFormatter.ts b/src/utils/tickFormatter.ts
new file mode 100644
index 0000000..fc34195
--- /dev/null
+++ b/src/utils/tickFormatter.ts
@@ -0,0 +1,18 @@
+export function formatTicks(ticks: number): string {
+ if (ticks <= 0) return '0 ticks';
+ const totalSeconds = ticks / 20;
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = totalSeconds % 60;
+
+ const parts: string[] = [`${ticks} ticks`];
+
+ if (minutes > 0 && seconds > 0) {
+ parts.push(`${minutes}min ${seconds}s`);
+ } else if (minutes > 0) {
+ parts.push(`${minutes}min`);
+ } else {
+ parts.push(`${totalSeconds}s`);
+ }
+
+ return parts.join(' = ');
+}
diff --git a/src/utils/validation.ts b/src/utils/validation.ts
new file mode 100644
index 0000000..a9b76b4
--- /dev/null
+++ b/src/utils/validation.ts
@@ -0,0 +1,148 @@
+import type { ValidationIssue } from '../types/validation';
+import type { ResearchNodeData } from '../types/research';
+import type { RecipeData } from '../types/recipe';
+import { TIER_FLUIDS } from '../data/tiers';
+import { flattenPrerequisiteIds, detectCycles } from './prerequisites';
+import { stripNamespace } from './idHelpers';
+
+export function validateAll(
+ nodes: Record,
+ recipes: Record
+): ValidationIssue[] {
+ const issues: ValidationIssue[] = [];
+
+ // Research node validations
+ for (const node of Object.values(nodes)) {
+ if (!node.tier) {
+ issues.push({ severity: 'error', target: 'research', targetId: node.id, field: 'tier', message: 'Tier is required.' });
+ }
+
+ if (!node.duration || node.duration <= 0) {
+ issues.push({ severity: 'error', target: 'research', targetId: node.id, field: 'duration', message: 'Duration must be > 0.' });
+ }
+
+ if (node.itemCosts.length > 6) {
+ issues.push({ severity: 'error', target: 'research', targetId: node.id, field: 'item_costs', message: `Too many item costs (${node.itemCosts.length}/6 max).` });
+ }
+
+ // Fluid must match tier
+ if (node.fluidCost && node.tier) {
+ const expected = TIER_FLUIDS[node.tier];
+ if (expected && node.fluidCost.fluid !== expected) {
+ issues.push({ severity: 'error', target: 'research', targetId: node.id, field: 'fluid_cost', message: `Fluid "${node.fluidCost.fluid}" doesn't match tier ${node.tier} (expected ${expected}).` });
+ }
+ }
+
+ // Prerequisites must reference existing nodes
+ if (node.prerequisites) {
+ const ids = flattenPrerequisiteIds(node.prerequisites);
+ for (const fullId of ids) {
+ const cleanId = stripNamespace(fullId);
+ if (!nodes[cleanId]) {
+ issues.push({ severity: 'error', target: 'research', targetId: node.id, field: 'prerequisites', message: `Prerequisite "${fullId}" does not exist.` });
+ }
+ }
+ }
+
+ // Idea chip warnings
+ if (node.ideaChip) {
+ if (!node.ideaChip.item) {
+ issues.push({ severity: 'error', target: 'research', targetId: node.id, field: 'idea_chip', message: 'Idea chip must have a base item.' });
+ }
+ if (!node.ideaChip.components?.['minecraft:custom_data']) {
+ issues.push({ severity: 'warning', target: 'research', targetId: node.id, field: 'idea_chip', message: 'Idea chip missing custom_data (recommended for stable matching).' });
+ }
+ if (!node.ideaChip.components?.['minecraft:custom_name']) {
+ issues.push({ severity: 'warning', target: 'research', targetId: node.id, field: 'idea_chip', message: 'Idea chip missing custom_name (players won\'t know what chip they need).' });
+ }
+ if (!node.ideaChip.components || Object.keys(node.ideaChip.components).length === 0) {
+ issues.push({ severity: 'warning', target: 'research', targetId: node.id, field: 'idea_chip', message: 'Idea chip has no components — will match any item of that type.' });
+ }
+ }
+
+ // Recipe pool references
+ for (const ref of node.recipePool) {
+ const recipeId = typeof ref === 'string' ? ref : ref.id;
+ const cleanId = stripNamespace(recipeId);
+ if (!recipes[cleanId]) {
+ issues.push({ severity: 'warning', target: 'research', targetId: node.id, field: 'recipe_pool', message: `Recipe "${recipeId}" in pool does not exist.` });
+ }
+ }
+ }
+
+ // Cycle detection
+ const cycles = detectCycles(nodes);
+ for (const cycle of cycles) {
+ issues.push({
+ severity: 'error',
+ target: 'research',
+ targetId: cycle[0],
+ field: 'prerequisites',
+ message: `Cycle detected: ${cycle.join(' -> ')} -> ${cycle[0]}`,
+ });
+ }
+
+ // Recipe validations
+ for (const recipe of Object.values(recipes)) {
+ if (recipe.kind === 'drive_crafting_shapeless') {
+ if (recipe.ingredients.length < 1) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'ingredients', message: 'At least 1 ingredient required.' });
+ }
+ if (recipe.ingredients.length > 8) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'ingredients', message: `Too many ingredients (${recipe.ingredients.length}/8 max).` });
+ }
+ if (recipe.result.count < 1) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'result', message: 'Result count must be >= 1.' });
+ }
+ for (const ing of recipe.ingredients) {
+ if (!ing.item) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'ingredients', message: 'Ingredient has empty item ID.' });
+ }
+ }
+ }
+
+ if (recipe.kind === 'drive_crafting_shaped') {
+ // Validate pattern
+ if (recipe.pattern.length !== 3) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'pattern', message: 'Pattern must have exactly 3 rows.' });
+ }
+ for (let r = 0; r < recipe.pattern.length; r++) {
+ if (recipe.pattern[r].length !== 3) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'pattern', message: `Pattern row ${r} must be exactly 3 characters.` });
+ }
+ }
+ // Every non-space char must be in key
+ const usedChars = new Set();
+ for (const row of recipe.pattern) {
+ for (const char of row) {
+ if (char !== ' ') usedChars.add(char);
+ }
+ }
+ for (const char of usedChars) {
+ if (!recipe.key[char]) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'key', message: `Character "${char}" in pattern not defined in key.` });
+ }
+ }
+ if (recipe.result.count < 1) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'result', message: 'Result count must be >= 1.' });
+ }
+ if (usedChars.size === 0) {
+ issues.push({ severity: 'warning', target: 'recipe', targetId: recipe.id, field: 'pattern', message: 'Pattern is empty — recipe has no ingredients.' });
+ }
+ }
+
+ if (recipe.kind === 'processing') {
+ if (recipe.inputs.length === 0 && recipe.fluidInputs.length === 0) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'inputs', message: 'At least one input (item or fluid) required.' });
+ }
+ if (recipe.outputs.length === 0) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'outputs', message: 'At least one output required.' });
+ }
+ if (recipe.duration <= 0) {
+ issues.push({ severity: 'error', target: 'recipe', targetId: recipe.id, field: 'duration', message: 'Duration must be > 0.' });
+ }
+ }
+ }
+
+ return issues;
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..af516fc
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2023",
+ "useDefineForClassFields": true,
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..a87739d
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ base: '/ResearchCubeDatapackCreator/',
+})
From 1b88e7b3d87f93691b07d51b9e59cea4c76e7e92 Mon Sep 17 00:00:00 2001
From: fireheart3911 <64578352+fireheart3911@users.noreply.github.com>
Date: Sun, 15 Mar 2026 23:36:58 +0100
Subject: [PATCH 2/3] fix(deploy): update branch to 'live' for deployment
trigger
---
.github/workflows/deploy.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9eb5588..4389e36 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -2,7 +2,7 @@ name: Deploy to GitHub Pages
on:
push:
- branches: [main]
+ branches: [live]
permissions:
contents: read
From 9c09cf90f134a15a9605c0e00d4d96af7168bc62 Mon Sep 17 00:00:00 2001
From: fireheart <64578352+fireheart3911@users.noreply.github.com>
Date: Sun, 15 Mar 2026 23:40:28 +0100
Subject: [PATCH 3/3] Update deployment instructions for GitHub Pages
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f1cd613..70a4207 100644
--- a/README.md
+++ b/README.md
@@ -29,4 +29,4 @@ npm run build
## Deployment
-Automatically deployed to GitHub Pages via GitHub Actions on push to `main`.
+Automatically deployed to GitHub Pages via GitHub Actions on push to `live`.