Explorar o código

init node.js app

clovis hai 1 mes
pai
achega
0f6a3be20a
Modificáronse 72 ficheiros con 13091 adicións e 7828 borrados
  1. 24 24
      .gitignore
  2. 75 75
      README.md
  3. 23 23
      eslint.config.js
  4. 19 19
      index.html
  5. 4215 4146
      package-lock.json
  6. 48 42
      package.json
  7. BIN=BIN
      public/assets/empty-module-slot.png
  8. BIN=BIN
      public/assets/no-item.png
  9. BIN=BIN
      public/data/2.0/icon_128.webp
  10. BIN=BIN
      public/data/2.0/icon_64.webp
  11. 85 85
      scripts/factorio-dump/helpers/command.helper.ts
  12. 31 31
      scripts/factorio-dump/helpers/file.helper.ts
  13. 164 161
      scripts/factorio-dump/helpers/groups.helper.ts
  14. 166 165
      scripts/factorio-dump/helpers/icons.helper.ts
  15. 96 96
      scripts/factorio-dump/helpers/locale.helper.ts
  16. 70 70
      scripts/factorio-dump/helpers/recipes.helper.ts
  17. 46 0
      scripts/factorio-dump/index.ts
  18. 178 0
      scripts/factorio-dump/lua-api/const.ts
  19. 32 0
      scripts/factorio-dump/lua-api/index.ts
  20. 118 118
      scripts/factorio-dump/lua-api/lua-api.models.d.ts
  21. 246 1
      scripts/factorio-dump/lua-api/models.ts
  22. 205 284
      scripts/factorio-dump/lua-api/parse-factorio-model.ts
  23. 73 0
      scripts/factorio-dump/lua-api/prototype-tree.ts
  24. 57 0
      scripts/factorio-dump/lua-api/template.ts
  25. 85 0
      scripts/factorio-dump/process-data.models.ts
  26. 187 0
      scripts/factorio-dump/process-data.ts
  27. 0 114
      scripts/factorio-process-data.models.ts
  28. 0 172
      scripts/factorio-process-data.ts
  29. 27 0
      scripts/helpers/logger.ts
  30. 0 16
      scripts/index.ts
  31. 9 16
      scripts/tsconfig.json
  32. 0 615
      src/App.css
  33. 7 39
      src/App.tsx
  34. 109 0
      src/ClockBuilder.module.css
  35. 211 0
      src/ClockBuilder.tsx
  36. 65 0
      src/ComponentTests.tsx
  37. 31 0
      src/Layout.tsx
  38. 252 0
      src/assets/ClockTimeline.module.css
  39. 380 0
      src/assets/ClockTimeline.tsx
  40. 9 0
      src/assets/Icon.module.css
  41. 4 0
      src/assets/MachineSelector.module.css
  42. 93 0
      src/assets/MachineSelector.tsx
  43. 0 0
      src/assets/ModuleSelector.module.css
  44. 16 0
      src/assets/ModuleSelector.tsx
  45. 169 156
      src/assets/SelectFactorioMenu.module.css
  46. 188 174
      src/assets/SelectFactorioMenu.tsx
  47. 50 0
      src/assets/SelectSignal.module.css
  48. 74 0
      src/assets/SelectSignal.tsx
  49. 19 19
      src/assets/Tooltip.css
  50. 39 48
      src/assets/Tooltip.tsx
  51. 14 0
      src/assets/components/ExpressionInput.module.css
  52. 65 0
      src/assets/components/ExpressionInpux.tsx
  53. 80 0
      src/assets/components/SelectedBlockPanel.module.css
  54. 80 0
      src/assets/components/SelectedBlockPanel.tsx
  55. 215 355
      src/assets/data/2.0/data.json
  56. 162 4
      src/assets/data/2.0/i18n/de.json
  57. 162 4
      src/assets/data/2.0/i18n/en.json
  58. 162 4
      src/assets/data/2.0/i18n/fr.json
  59. 162 4
      src/assets/data/2.0/i18n/ja.json
  60. 1486 0
      src/assets/data/2.0/i18n/zh-CH.json
  61. 1348 604
      src/assets/data/2.0/iconMap.json
  62. 50 42
      src/assets/icon.tsx
  63. 78 0
      src/assets/theme.ts
  64. 139 0
      src/assets/types.ts
  65. 141 0
      src/blueprint/Blueprintbuilder.ts
  66. 38 0
      src/blueprint/parser.ts
  67. 610 21
      src/index.css
  68. 24 10
      src/main.tsx
  69. 30 30
      tsconfig.app.json
  70. 7 7
      tsconfig.json
  71. 29 20
      tsconfig.node.json
  72. 14 14
      vite.config.ts

+ 24 - 24
.gitignore

@@ -1,24 +1,24 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 75 - 75
README.md

@@ -1,75 +1,75 @@
-# React + TypeScript + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
-
-## React Compiler
-
-The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
-
-Note: This will impact Vite dev & build performances.
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
-
-```js
-export default defineConfig([
-  globalIgnores(['dist']),
-  {
-    files: ['**/*.{ts,tsx}'],
-    extends: [
-      // Other configs...
-
-      // Remove tseslint.configs.recommended and replace with this
-      tseslint.configs.recommendedTypeChecked,
-      // Alternatively, use this for stricter rules
-      tseslint.configs.strictTypeChecked,
-      // Optionally, add this for stylistic rules
-      tseslint.configs.stylisticTypeChecked,
-
-      // Other configs...
-    ],
-    languageOptions: {
-      parserOptions: {
-        project: ['./tsconfig.node.json', './tsconfig.app.json'],
-        tsconfigRootDir: import.meta.dirname,
-      },
-      // other options...
-    },
-  },
-])
-```
-
-You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
-
-```js
-// eslint.config.js
-import reactX from 'eslint-plugin-react-x'
-import reactDom from 'eslint-plugin-react-dom'
-
-export default defineConfig([
-  globalIgnores(['dist']),
-  {
-    files: ['**/*.{ts,tsx}'],
-    extends: [
-      // Other configs...
-      // Enable lint rules for React
-      reactX.configs['recommended-typescript'],
-      // Enable lint rules for React DOM
-      reactDom.configs.recommended,
-    ],
-    languageOptions: {
-      parserOptions: {
-        project: ['./tsconfig.node.json', './tsconfig.app.json'],
-        tsconfigRootDir: import.meta.dirname,
-      },
-      // other options...
-    },
-  },
-])
-```
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
+
+Note: This will impact Vite dev & build performances.
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+  globalIgnores(['dist']),
+  {
+    files: ['**/*.{ts,tsx}'],
+    extends: [
+      // Other configs...
+
+      // Remove tseslint.configs.recommended and replace with this
+      tseslint.configs.recommendedTypeChecked,
+      // Alternatively, use this for stricter rules
+      tseslint.configs.strictTypeChecked,
+      // Optionally, add this for stylistic rules
+      tseslint.configs.stylisticTypeChecked,
+
+      // Other configs...
+    ],
+    languageOptions: {
+      parserOptions: {
+        project: ['./tsconfig.node.json', './tsconfig.app.json'],
+        tsconfigRootDir: import.meta.dirname,
+      },
+      // other options...
+    },
+  },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+  globalIgnores(['dist']),
+  {
+    files: ['**/*.{ts,tsx}'],
+    extends: [
+      // Other configs...
+      // Enable lint rules for React
+      reactX.configs['recommended-typescript'],
+      // Enable lint rules for React DOM
+      reactDom.configs.recommended,
+    ],
+    languageOptions: {
+      parserOptions: {
+        project: ['./tsconfig.node.json', './tsconfig.app.json'],
+        tsconfigRootDir: import.meta.dirname,
+      },
+      // other options...
+    },
+  },
+])
+```

+ 23 - 23
eslint.config.js

@@ -1,23 +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,
-    },
-  },
-])
+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,
+    },
+  },
+])

+ 19 - 19
index.html

@@ -1,19 +1,19 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <link rel="preconnect" href="https://fonts.googleapis.com" />
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
-    <link
-      href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap"
-      rel="stylesheet"
-    />
-    <title>factorio-clockmaster</title>
-  </head>
-  <body>
-    <div id="root"></div>
-    <script type="module" src="/src/main.tsx"></script>
-  </body>
-</html>
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap"
+      rel="stylesheet"
+    />
+    <title>factorio-clockmaster</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>

+ 4215 - 4146
package-lock.json

@@ -1,4146 +1,4215 @@
-{
-  "name": "factorio-clockmaster",
-  "version": "0.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "factorio-clockmaster",
-      "version": "0.0.0",
-      "dependencies": {
-        "@emotion/react": "^11.14.0",
-        "@emotion/styled": "^11.14.1",
-        "@mui/material": "^7.3.5",
-        "react": "^19.2.0",
-        "react-dom": "^19.2.0",
-        "simplebar-react": "^3.3.2"
-      },
-      "devDependencies": {
-        "@eslint/js": "^9.39.1",
-        "@types/node": "^24.10.0",
-        "@types/react": "^19.2.2",
-        "@types/react-dom": "^19.2.2",
-        "@vitejs/plugin-react": "^5.1.0",
-        "babel-plugin-react-compiler": "^1.0.0",
-        "eslint": "^9.39.1",
-        "eslint-plugin-react-hooks": "^7.0.1",
-        "eslint-plugin-react-refresh": "^0.4.24",
-        "globals": "^16.5.0",
-        "handlebars": "^4.7.8",
-        "prettier": "^3.6.2",
-        "ts-node": "^10.9.2",
-        "tsconfig-paths": "^4.2.0",
-        "typescript": "~5.9.3",
-        "typescript-eslint": "^8.46.3",
-        "vite": "^7.2.2"
-      }
-    },
-    "node_modules/@babel/code-frame": {
-      "version": "7.27.1",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
-      "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
-      "dependencies": {
-        "@babel/helper-validator-identifier": "^7.27.1",
-        "js-tokens": "^4.0.0",
-        "picocolors": "^1.1.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/compat-data": {
-      "version": "7.28.5",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
-      "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/core": {
-      "version": "7.28.5",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
-      "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "@babel/code-frame": "^7.27.1",
-        "@babel/generator": "^7.28.5",
-        "@babel/helper-compilation-targets": "^7.27.2",
-        "@babel/helper-module-transforms": "^7.28.3",
-        "@babel/helpers": "^7.28.4",
-        "@babel/parser": "^7.28.5",
-        "@babel/template": "^7.27.2",
-        "@babel/traverse": "^7.28.5",
-        "@babel/types": "^7.28.5",
-        "@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.28.5",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
-      "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
-      "dependencies": {
-        "@babel/parser": "^7.28.5",
-        "@babel/types": "^7.28.5",
-        "@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.27.2",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
-      "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
-      "dev": true,
-      "dependencies": {
-        "@babel/compat-data": "^7.27.2",
-        "@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==",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-module-imports": {
-      "version": "7.27.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
-      "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
-      "dependencies": {
-        "@babel/traverse": "^7.27.1",
-        "@babel/types": "^7.27.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-module-transforms": {
-      "version": "7.28.3",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
-      "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-module-imports": "^7.27.1",
-        "@babel/helper-validator-identifier": "^7.27.1",
-        "@babel/traverse": "^7.28.3"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0"
-      }
-    },
-    "node_modules/@babel/helper-plugin-utils": {
-      "version": "7.27.1",
-      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
-      "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
-      "dev": true,
-      "engines": {
-        "node": ">=6.9.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==",
-      "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==",
-      "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,
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helpers": {
-      "version": "7.28.4",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
-      "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
-      "dev": true,
-      "dependencies": {
-        "@babel/template": "^7.27.2",
-        "@babel/types": "^7.28.4"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/parser": {
-      "version": "7.28.5",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
-      "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
-      "dependencies": {
-        "@babel/types": "^7.28.5"
-      },
-      "bin": {
-        "parser": "bin/babel-parser.js"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/@babel/plugin-transform-react-jsx-self": {
-      "version": "7.27.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
-      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.27.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/plugin-transform-react-jsx-source": {
-      "version": "7.27.1",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
-      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/helper-plugin-utils": "^7.27.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      },
-      "peerDependencies": {
-        "@babel/core": "^7.0.0-0"
-      }
-    },
-    "node_modules/@babel/runtime": {
-      "version": "7.28.4",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
-      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/template": {
-      "version": "7.27.2",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
-      "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
-      "dependencies": {
-        "@babel/code-frame": "^7.27.1",
-        "@babel/parser": "^7.27.2",
-        "@babel/types": "^7.27.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/traverse": {
-      "version": "7.28.5",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
-      "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
-      "dependencies": {
-        "@babel/code-frame": "^7.27.1",
-        "@babel/generator": "^7.28.5",
-        "@babel/helper-globals": "^7.28.0",
-        "@babel/parser": "^7.28.5",
-        "@babel/template": "^7.27.2",
-        "@babel/types": "^7.28.5",
-        "debug": "^4.3.1"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/types": {
-      "version": "7.28.5",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
-      "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
-      "dependencies": {
-        "@babel/helper-string-parser": "^7.27.1",
-        "@babel/helper-validator-identifier": "^7.28.5"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@cspotcode/source-map-support": {
-      "version": "0.8.1",
-      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
-      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/trace-mapping": "0.3.9"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
-      "version": "0.3.9",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
-      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
-      "dev": true,
-      "dependencies": {
-        "@jridgewell/resolve-uri": "^3.0.3",
-        "@jridgewell/sourcemap-codec": "^1.4.10"
-      }
-    },
-    "node_modules/@emotion/babel-plugin": {
-      "version": "11.13.5",
-      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
-      "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
-      "dependencies": {
-        "@babel/helper-module-imports": "^7.16.7",
-        "@babel/runtime": "^7.18.3",
-        "@emotion/hash": "^0.9.2",
-        "@emotion/memoize": "^0.9.0",
-        "@emotion/serialize": "^1.3.3",
-        "babel-plugin-macros": "^3.1.0",
-        "convert-source-map": "^1.5.0",
-        "escape-string-regexp": "^4.0.0",
-        "find-root": "^1.1.0",
-        "source-map": "^0.5.7",
-        "stylis": "4.2.0"
-      }
-    },
-    "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
-      "version": "1.9.0",
-      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
-      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
-    },
-    "node_modules/@emotion/babel-plugin/node_modules/source-map": {
-      "version": "0.5.7",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/@emotion/cache": {
-      "version": "11.14.0",
-      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
-      "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
-      "dependencies": {
-        "@emotion/memoize": "^0.9.0",
-        "@emotion/sheet": "^1.4.0",
-        "@emotion/utils": "^1.4.2",
-        "@emotion/weak-memoize": "^0.4.0",
-        "stylis": "4.2.0"
-      }
-    },
-    "node_modules/@emotion/hash": {
-      "version": "0.9.2",
-      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
-      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
-    },
-    "node_modules/@emotion/is-prop-valid": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
-      "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
-      "dependencies": {
-        "@emotion/memoize": "^0.9.0"
-      }
-    },
-    "node_modules/@emotion/memoize": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
-      "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
-    },
-    "node_modules/@emotion/react": {
-      "version": "11.14.0",
-      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
-      "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
-      "peer": true,
-      "dependencies": {
-        "@babel/runtime": "^7.18.3",
-        "@emotion/babel-plugin": "^11.13.5",
-        "@emotion/cache": "^11.14.0",
-        "@emotion/serialize": "^1.3.3",
-        "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
-        "@emotion/utils": "^1.4.2",
-        "@emotion/weak-memoize": "^0.4.0",
-        "hoist-non-react-statics": "^3.3.1"
-      },
-      "peerDependencies": {
-        "react": ">=16.8.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@emotion/serialize": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
-      "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
-      "dependencies": {
-        "@emotion/hash": "^0.9.2",
-        "@emotion/memoize": "^0.9.0",
-        "@emotion/unitless": "^0.10.0",
-        "@emotion/utils": "^1.4.2",
-        "csstype": "^3.0.2"
-      }
-    },
-    "node_modules/@emotion/sheet": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
-      "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
-    },
-    "node_modules/@emotion/styled": {
-      "version": "11.14.1",
-      "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
-      "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
-      "peer": true,
-      "dependencies": {
-        "@babel/runtime": "^7.18.3",
-        "@emotion/babel-plugin": "^11.13.5",
-        "@emotion/is-prop-valid": "^1.3.0",
-        "@emotion/serialize": "^1.3.3",
-        "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
-        "@emotion/utils": "^1.4.2"
-      },
-      "peerDependencies": {
-        "@emotion/react": "^11.0.0-rc.0",
-        "react": ">=16.8.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@emotion/unitless": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
-      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
-    },
-    "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
-      "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
-      "peerDependencies": {
-        "react": ">=16.8.0"
-      }
-    },
-    "node_modules/@emotion/utils": {
-      "version": "1.4.2",
-      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
-      "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
-    },
-    "node_modules/@emotion/weak-memoize": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
-      "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
-    },
-    "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
-      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "aix"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-arm": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
-      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
-      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/android-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
-      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
-      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/darwin-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
-      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
-      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
-      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-arm": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
-      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
-      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-ia32": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
-      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-loong64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
-      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
-      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
-      "cpu": [
-        "mips64el"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
-      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
-      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-s390x": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
-      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/linux-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
-      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/netbsd-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
-      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
-      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "netbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openbsd-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
-      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
-      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openbsd"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/openharmony-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
-      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openharmony"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/sunos-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
-      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "sunos"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-arm64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
-      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-ia32": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
-      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@esbuild/win32-x64": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
-      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.9.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
-      "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
-      "dev": true,
-      "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,
-      "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,
-      "engines": {
-        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
-      }
-    },
-    "node_modules/@eslint/config-array": {
-      "version": "0.21.1",
-      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
-      "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
-      "dev": true,
-      "dependencies": {
-        "@eslint/object-schema": "^2.1.7",
-        "debug": "^4.3.1",
-        "minimatch": "^3.1.2"
-      },
-      "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,
-      "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,
-      "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.1",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
-      "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
-      "dev": true,
-      "dependencies": {
-        "ajv": "^6.12.4",
-        "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.0",
-        "minimatch": "^3.1.2",
-        "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,
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/@eslint/js": {
-      "version": "9.39.1",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
-      "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
-      "dev": true,
-      "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,
-      "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,
-      "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,
-      "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,
-      "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,
-      "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,
-      "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==",
-      "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,
-      "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==",
-      "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=="
-    },
-    "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==",
-      "dependencies": {
-        "@jridgewell/resolve-uri": "^3.1.0",
-        "@jridgewell/sourcemap-codec": "^1.4.14"
-      }
-    },
-    "node_modules/@mui/core-downloads-tracker": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz",
-      "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      }
-    },
-    "node_modules/@mui/material": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
-      "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.28.4",
-        "@mui/core-downloads-tracker": "^7.3.5",
-        "@mui/system": "^7.3.5",
-        "@mui/types": "^7.4.8",
-        "@mui/utils": "^7.3.5",
-        "@popperjs/core": "^2.11.8",
-        "@types/react-transition-group": "^4.4.12",
-        "clsx": "^2.1.1",
-        "csstype": "^3.1.3",
-        "prop-types": "^15.8.1",
-        "react-is": "^19.2.0",
-        "react-transition-group": "^4.4.5"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      },
-      "peerDependencies": {
-        "@emotion/react": "^11.5.0",
-        "@emotion/styled": "^11.3.0",
-        "@mui/material-pigment-css": "^7.3.5",
-        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@emotion/react": {
-          "optional": true
-        },
-        "@emotion/styled": {
-          "optional": true
-        },
-        "@mui/material-pigment-css": {
-          "optional": true
-        },
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@mui/private-theming": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz",
-      "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==",
-      "dependencies": {
-        "@babel/runtime": "^7.28.4",
-        "@mui/utils": "^7.3.5",
-        "prop-types": "^15.8.1"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      },
-      "peerDependencies": {
-        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@mui/styled-engine": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz",
-      "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.28.4",
-        "@emotion/cache": "^11.14.0",
-        "@emotion/serialize": "^1.3.3",
-        "@emotion/sheet": "^1.4.0",
-        "csstype": "^3.1.3",
-        "prop-types": "^15.8.1"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      },
-      "peerDependencies": {
-        "@emotion/react": "^11.4.1",
-        "@emotion/styled": "^11.3.0",
-        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@emotion/react": {
-          "optional": true
-        },
-        "@emotion/styled": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@mui/system": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz",
-      "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==",
-      "dependencies": {
-        "@babel/runtime": "^7.28.4",
-        "@mui/private-theming": "^7.3.5",
-        "@mui/styled-engine": "^7.3.5",
-        "@mui/types": "^7.4.8",
-        "@mui/utils": "^7.3.5",
-        "clsx": "^2.1.1",
-        "csstype": "^3.1.3",
-        "prop-types": "^15.8.1"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      },
-      "peerDependencies": {
-        "@emotion/react": "^11.5.0",
-        "@emotion/styled": "^11.3.0",
-        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@emotion/react": {
-          "optional": true
-        },
-        "@emotion/styled": {
-          "optional": true
-        },
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@mui/types": {
-      "version": "7.4.8",
-      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz",
-      "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==",
-      "dependencies": {
-        "@babel/runtime": "^7.28.4"
-      },
-      "peerDependencies": {
-        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@mui/utils": {
-      "version": "7.3.5",
-      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz",
-      "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==",
-      "dependencies": {
-        "@babel/runtime": "^7.28.4",
-        "@mui/types": "^7.4.8",
-        "@types/prop-types": "^15.7.15",
-        "clsx": "^2.1.1",
-        "prop-types": "^15.8.1",
-        "react-is": "^19.2.0"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      },
-      "peerDependencies": {
-        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@nodelib/fs.scandir": {
-      "version": "2.1.5",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-      "dev": true,
-      "dependencies": {
-        "@nodelib/fs.stat": "2.0.5",
-        "run-parallel": "^1.1.9"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@nodelib/fs.stat": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-      "dev": true,
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@nodelib/fs.walk": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-      "dev": true,
-      "dependencies": {
-        "@nodelib/fs.scandir": "2.1.5",
-        "fastq": "^1.6.0"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@popperjs/core": {
-      "version": "2.11.8",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
-      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
-    "node_modules/@rolldown/pluginutils": {
-      "version": "1.0.0-beta.47",
-      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
-      "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==",
-      "dev": true
-    },
-    "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
-      "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ]
-    },
-    "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz",
-      "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "android"
-      ]
-    },
-    "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz",
-      "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz",
-      "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ]
-    },
-    "node_modules/@rollup/rollup-freebsd-arm64": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz",
-      "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ]
-    },
-    "node_modules/@rollup/rollup-freebsd-x64": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz",
-      "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "freebsd"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz",
-      "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz",
-      "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz",
-      "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz",
-      "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-loong64-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz",
-      "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==",
-      "cpu": [
-        "loong64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz",
-      "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==",
-      "cpu": [
-        "ppc64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz",
-      "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-riscv64-musl": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz",
-      "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==",
-      "cpu": [
-        "riscv64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz",
-      "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==",
-      "cpu": [
-        "s390x"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz",
-      "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz",
-      "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "linux"
-      ]
-    },
-    "node_modules/@rollup/rollup-openharmony-arm64": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz",
-      "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "openharmony"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz",
-      "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz",
-      "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==",
-      "cpu": [
-        "ia32"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-x64-gnu": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz",
-      "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz",
-      "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "optional": true,
-      "os": [
-        "win32"
-      ]
-    },
-    "node_modules/@tsconfig/node10": {
-      "version": "1.0.12",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
-      "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
-      "dev": true
-    },
-    "node_modules/@tsconfig/node12": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
-      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
-      "dev": true
-    },
-    "node_modules/@tsconfig/node14": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
-      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
-      "dev": true
-    },
-    "node_modules/@tsconfig/node16": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
-      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
-      "dev": true
-    },
-    "node_modules/@types/babel__core": {
-      "version": "7.20.5",
-      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
-      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
-      "dev": true,
-      "dependencies": {
-        "@babel/parser": "^7.20.7",
-        "@babel/types": "^7.20.7",
-        "@types/babel__generator": "*",
-        "@types/babel__template": "*",
-        "@types/babel__traverse": "*"
-      }
-    },
-    "node_modules/@types/babel__generator": {
-      "version": "7.27.0",
-      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
-      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "node_modules/@types/babel__template": {
-      "version": "7.4.4",
-      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
-      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
-      "dev": true,
-      "dependencies": {
-        "@babel/parser": "^7.1.0",
-        "@babel/types": "^7.0.0"
-      }
-    },
-    "node_modules/@types/babel__traverse": {
-      "version": "7.28.0",
-      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
-      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.28.2"
-      }
-    },
-    "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
-    },
-    "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
-    },
-    "node_modules/@types/node": {
-      "version": "24.10.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
-      "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "undici-types": "~7.16.0"
-      }
-    },
-    "node_modules/@types/parse-json": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
-      "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
-    },
-    "node_modules/@types/prop-types": {
-      "version": "15.7.15",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
-      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
-    },
-    "node_modules/@types/react": {
-      "version": "19.2.5",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
-      "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
-      "peer": true,
-      "dependencies": {
-        "csstype": "^3.0.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,
-      "peerDependencies": {
-        "@types/react": "^19.2.0"
-      }
-    },
-    "node_modules/@types/react-transition-group": {
-      "version": "4.4.12",
-      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
-      "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
-      "peerDependencies": {
-        "@types/react": "*"
-      }
-    },
-    "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz",
-      "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.46.4",
-        "@typescript-eslint/type-utils": "8.46.4",
-        "@typescript-eslint/utils": "8.46.4",
-        "@typescript-eslint/visitor-keys": "8.46.4",
-        "graphemer": "^1.4.0",
-        "ignore": "^7.0.0",
-        "natural-compare": "^1.4.0",
-        "ts-api-utils": "^2.1.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.46.4",
-        "eslint": "^8.57.0 || ^9.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,
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/@typescript-eslint/parser": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz",
-      "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "@typescript-eslint/scope-manager": "8.46.4",
-        "@typescript-eslint/types": "8.46.4",
-        "@typescript-eslint/typescript-estree": "8.46.4",
-        "@typescript-eslint/visitor-keys": "8.46.4",
-        "debug": "^4.3.4"
-      },
-      "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",
-        "typescript": ">=4.8.4 <6.0.0"
-      }
-    },
-    "node_modules/@typescript-eslint/project-service": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz",
-      "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/tsconfig-utils": "^8.46.4",
-        "@typescript-eslint/types": "^8.46.4",
-        "debug": "^4.3.4"
-      },
-      "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.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz",
-      "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "8.46.4",
-        "@typescript-eslint/visitor-keys": "8.46.4"
-      },
-      "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.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz",
-      "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==",
-      "dev": true,
-      "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.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz",
-      "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "8.46.4",
-        "@typescript-eslint/typescript-estree": "8.46.4",
-        "@typescript-eslint/utils": "8.46.4",
-        "debug": "^4.3.4",
-        "ts-api-utils": "^2.1.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",
-        "typescript": ">=4.8.4 <6.0.0"
-      }
-    },
-    "node_modules/@typescript-eslint/types": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz",
-      "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==",
-      "dev": true,
-      "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.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz",
-      "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/project-service": "8.46.4",
-        "@typescript-eslint/tsconfig-utils": "8.46.4",
-        "@typescript-eslint/types": "8.46.4",
-        "@typescript-eslint/visitor-keys": "8.46.4",
-        "debug": "^4.3.4",
-        "fast-glob": "^3.3.2",
-        "is-glob": "^4.0.3",
-        "minimatch": "^9.0.4",
-        "semver": "^7.6.0",
-        "ts-api-utils": "^2.1.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/brace-expansion": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
-      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
-      "dev": true,
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
-      "version": "9.0.5",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
-      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
-      "version": "7.7.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
-      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@typescript-eslint/utils": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz",
-      "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==",
-      "dev": true,
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.7.0",
-        "@typescript-eslint/scope-manager": "8.46.4",
-        "@typescript-eslint/types": "8.46.4",
-        "@typescript-eslint/typescript-estree": "8.46.4"
-      },
-      "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",
-        "typescript": ">=4.8.4 <6.0.0"
-      }
-    },
-    "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz",
-      "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/types": "8.46.4",
-        "eslint-visitor-keys": "^4.2.1"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      }
-    },
-    "node_modules/@vitejs/plugin-react": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
-      "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==",
-      "dev": true,
-      "dependencies": {
-        "@babel/core": "^7.28.5",
-        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
-        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
-        "@rolldown/pluginutils": "1.0.0-beta.47",
-        "@types/babel__core": "^7.20.5",
-        "react-refresh": "^0.18.0"
-      },
-      "engines": {
-        "node": "^20.19.0 || >=22.12.0"
-      },
-      "peerDependencies": {
-        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
-      }
-    },
-    "node_modules/acorn": {
-      "version": "8.15.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
-      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
-      "dev": true,
-      "peer": true,
-      "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,
-      "peerDependencies": {
-        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
-      }
-    },
-    "node_modules/acorn-walk": {
-      "version": "8.3.4",
-      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
-      "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.11.0"
-      },
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "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,
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/arg": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
-      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
-      "dev": true
-    },
-    "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
-    },
-    "node_modules/babel-plugin-macros": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
-      "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
-      "dependencies": {
-        "@babel/runtime": "^7.12.5",
-        "cosmiconfig": "^7.0.0",
-        "resolve": "^1.19.0"
-      },
-      "engines": {
-        "node": ">=10",
-        "npm": ">=6"
-      }
-    },
-    "node_modules/babel-plugin-react-compiler": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
-      "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
-      "dev": true,
-      "dependencies": {
-        "@babel/types": "^7.26.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
-    },
-    "node_modules/baseline-browser-mapping": {
-      "version": "2.8.28",
-      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz",
-      "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==",
-      "dev": true,
-      "bin": {
-        "baseline-browser-mapping": "dist/cli.js"
-      }
-    },
-    "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,
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "node_modules/braces": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
-      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-      "dev": true,
-      "dependencies": {
-        "fill-range": "^7.1.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/browserslist": {
-      "version": "4.28.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
-      "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
-      "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"
-        }
-      ],
-      "peer": true,
-      "dependencies": {
-        "baseline-browser-mapping": "^2.8.25",
-        "caniuse-lite": "^1.0.30001754",
-        "electron-to-chromium": "^1.5.249",
-        "node-releases": "^2.0.27",
-        "update-browserslist-db": "^1.1.4"
-      },
-      "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==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/caniuse-lite": {
-      "version": "1.0.30001755",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz",
-      "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==",
-      "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"
-        }
-      ]
-    },
-    "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,
-      "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/clsx": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
-      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "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,
-      "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
-    },
-    "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
-    },
-    "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
-    },
-    "node_modules/cosmiconfig": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
-      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
-      "dependencies": {
-        "@types/parse-json": "^4.0.0",
-        "import-fresh": "^3.2.1",
-        "parse-json": "^5.0.0",
-        "path-type": "^4.0.0",
-        "yaml": "^1.10.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/cosmiconfig/node_modules/yaml": {
-      "version": "1.10.2",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
-      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/create-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
-      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
-      "dev": true
-    },
-    "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,
-      "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=="
-    },
-    "node_modules/debug": {
-      "version": "4.4.3",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
-      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
-      "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
-    },
-    "node_modules/diff": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
-      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.3.1"
-      }
-    },
-    "node_modules/dom-helpers": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
-      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
-      "dependencies": {
-        "@babel/runtime": "^7.8.7",
-        "csstype": "^3.0.2"
-      }
-    },
-    "node_modules/electron-to-chromium": {
-      "version": "1.5.254",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
-      "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==",
-      "dev": true
-    },
-    "node_modules/error-ex": {
-      "version": "1.3.4",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
-      "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
-      "dependencies": {
-        "is-arrayish": "^0.2.1"
-      }
-    },
-    "node_modules/esbuild": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
-      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
-      "dev": true,
-      "hasInstallScript": true,
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=18"
-      },
-      "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.25.12",
-        "@esbuild/android-arm": "0.25.12",
-        "@esbuild/android-arm64": "0.25.12",
-        "@esbuild/android-x64": "0.25.12",
-        "@esbuild/darwin-arm64": "0.25.12",
-        "@esbuild/darwin-x64": "0.25.12",
-        "@esbuild/freebsd-arm64": "0.25.12",
-        "@esbuild/freebsd-x64": "0.25.12",
-        "@esbuild/linux-arm": "0.25.12",
-        "@esbuild/linux-arm64": "0.25.12",
-        "@esbuild/linux-ia32": "0.25.12",
-        "@esbuild/linux-loong64": "0.25.12",
-        "@esbuild/linux-mips64el": "0.25.12",
-        "@esbuild/linux-ppc64": "0.25.12",
-        "@esbuild/linux-riscv64": "0.25.12",
-        "@esbuild/linux-s390x": "0.25.12",
-        "@esbuild/linux-x64": "0.25.12",
-        "@esbuild/netbsd-arm64": "0.25.12",
-        "@esbuild/netbsd-x64": "0.25.12",
-        "@esbuild/openbsd-arm64": "0.25.12",
-        "@esbuild/openbsd-x64": "0.25.12",
-        "@esbuild/openharmony-arm64": "0.25.12",
-        "@esbuild/sunos-x64": "0.25.12",
-        "@esbuild/win32-arm64": "0.25.12",
-        "@esbuild/win32-ia32": "0.25.12",
-        "@esbuild/win32-x64": "0.25.12"
-      }
-    },
-    "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,
-      "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==",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint": {
-      "version": "9.39.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
-      "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.8.0",
-        "@eslint-community/regexpp": "^4.12.1",
-        "@eslint/config-array": "^0.21.1",
-        "@eslint/config-helpers": "^0.4.2",
-        "@eslint/core": "^0.17.0",
-        "@eslint/eslintrc": "^3.3.1",
-        "@eslint/js": "9.39.1",
-        "@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.12.4",
-        "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.2",
-        "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,
-      "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.4.24",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
-      "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
-      "dev": true,
-      "peerDependencies": {
-        "eslint": ">=8.40"
-      }
-    },
-    "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,
-      "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,
-      "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,
-      "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.6.0",
-      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
-      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
-      "dev": true,
-      "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,
-      "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,
-      "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,
-      "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
-    },
-    "node_modules/fast-glob": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
-      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
-      "dev": true,
-      "dependencies": {
-        "@nodelib/fs.stat": "^2.0.2",
-        "@nodelib/fs.walk": "^1.2.3",
-        "glob-parent": "^5.1.2",
-        "merge2": "^1.3.0",
-        "micromatch": "^4.0.8"
-      },
-      "engines": {
-        "node": ">=8.6.0"
-      }
-    },
-    "node_modules/fast-glob/node_modules/glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
-      "dependencies": {
-        "is-glob": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "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
-    },
-    "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
-    },
-    "node_modules/fastq": {
-      "version": "1.19.1",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
-      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
-      "dev": true,
-      "dependencies": {
-        "reusify": "^1.0.4"
-      }
-    },
-    "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,
-      "dependencies": {
-        "flat-cache": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=16.0.0"
-      }
-    },
-    "node_modules/fill-range": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
-      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-      "dev": true,
-      "dependencies": {
-        "to-regex-range": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/find-root": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
-      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
-    },
-    "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,
-      "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,
-      "dependencies": {
-        "flatted": "^3.2.9",
-        "keyv": "^4.5.4"
-      },
-      "engines": {
-        "node": ">=16"
-      }
-    },
-    "node_modules/flatted": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
-      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
-      "dev": true
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-      "dev": true,
-      "hasInstallScript": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "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,
-      "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,
-      "dependencies": {
-        "is-glob": "^4.0.3"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/globals": {
-      "version": "16.5.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
-      "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/graphemer": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
-      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-      "dev": true
-    },
-    "node_modules/handlebars": {
-      "version": "4.7.8",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
-      "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
-      "dev": true,
-      "dependencies": {
-        "minimist": "^1.2.5",
-        "neo-async": "^2.6.2",
-        "source-map": "^0.6.1",
-        "wordwrap": "^1.0.0"
-      },
-      "bin": {
-        "handlebars": "bin/handlebars"
-      },
-      "engines": {
-        "node": ">=0.4.7"
-      },
-      "optionalDependencies": {
-        "uglify-js": "^3.1.4"
-      }
-    },
-    "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,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/hasown": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
-      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
-      "dependencies": {
-        "function-bind": "^1.1.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "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
-    },
-    "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,
-      "dependencies": {
-        "hermes-estree": "0.25.1"
-      }
-    },
-    "node_modules/hoist-non-react-statics": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
-      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
-      "dependencies": {
-        "react-is": "^16.7.0"
-      }
-    },
-    "node_modules/hoist-non-react-statics/node_modules/react-is": {
-      "version": "16.13.1",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
-      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
-    },
-    "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,
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/import-fresh": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
-      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
-      "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,
-      "engines": {
-        "node": ">=0.8.19"
-      }
-    },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
-    },
-    "node_modules/is-core-module": {
-      "version": "2.16.1",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
-      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
-      "dependencies": {
-        "hasown": "^2.0.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "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,
-      "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,
-      "dependencies": {
-        "is-extglob": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.12.0"
-      }
-    },
-    "node_modules/isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true
-    },
-    "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=="
-    },
-    "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,
-      "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==",
-      "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
-    },
-    "node_modules/json-parse-even-better-errors": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
-      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
-    },
-    "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
-    },
-    "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
-    },
-    "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,
-      "bin": {
-        "json5": "lib/cli.js"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "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,
-      "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,
-      "dependencies": {
-        "prelude-ls": "^1.2.1",
-        "type-check": "~0.4.0"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/lines-and-columns": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
-      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
-    },
-    "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,
-      "dependencies": {
-        "p-locate": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/lodash": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-      "license": "MIT"
-    },
-    "node_modules/lodash-es": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
-      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
-      "license": "MIT"
-    },
-    "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
-    },
-    "node_modules/loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "dependencies": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      },
-      "bin": {
-        "loose-envify": "cli.js"
-      }
-    },
-    "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,
-      "dependencies": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "node_modules/make-error": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
-      "dev": true
-    },
-    "node_modules/merge2": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
-      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/micromatch": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
-      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
-      "dev": true,
-      "dependencies": {
-        "braces": "^3.0.3",
-        "picomatch": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=8.6"
-      }
-    },
-    "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/minimist": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "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=="
-    },
-    "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"
-        }
-      ],
-      "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
-    },
-    "node_modules/neo-async": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
-      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
-      "dev": true
-    },
-    "node_modules/node-releases": {
-      "version": "2.0.27",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
-      "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
-      "dev": true
-    },
-    "node_modules/object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/optionator": {
-      "version": "0.9.4",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
-      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
-      "dev": true,
-      "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,
-      "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,
-      "dependencies": {
-        "p-limit": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "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==",
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/parse-json": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
-      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
-      "dependencies": {
-        "@babel/code-frame": "^7.0.0",
-        "error-ex": "^1.3.1",
-        "json-parse-even-better-errors": "^2.3.0",
-        "lines-and-columns": "^1.1.6"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "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,
-      "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,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
-    },
-    "node_modules/path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-      "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=="
-    },
-    "node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/postcss": {
-      "version": "8.5.6",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
-      "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"
-        }
-      ],
-      "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,
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/prettier": {
-      "version": "3.6.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
-      "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
-      "dev": true,
-      "bin": {
-        "prettier": "bin/prettier.cjs"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/prettier/prettier?sponsor=1"
-      }
-    },
-    "node_modules/prop-types": {
-      "version": "15.8.1",
-      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
-      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
-      "dependencies": {
-        "loose-envify": "^1.4.0",
-        "object-assign": "^4.1.1",
-        "react-is": "^16.13.1"
-      }
-    },
-    "node_modules/prop-types/node_modules/react-is": {
-      "version": "16.13.1",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
-      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
-    },
-    "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,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/queue-microtask": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/react": {
-      "version": "19.2.0",
-      "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
-      "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
-      "peer": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/react-dom": {
-      "version": "19.2.0",
-      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
-      "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
-      "peer": true,
-      "dependencies": {
-        "scheduler": "^0.27.0"
-      },
-      "peerDependencies": {
-        "react": "^19.2.0"
-      }
-    },
-    "node_modules/react-is": {
-      "version": "19.2.0",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
-      "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="
-    },
-    "node_modules/react-refresh": {
-      "version": "0.18.0",
-      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
-      "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/react-transition-group": {
-      "version": "4.4.5",
-      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
-      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
-      "dependencies": {
-        "@babel/runtime": "^7.5.5",
-        "dom-helpers": "^5.0.1",
-        "loose-envify": "^1.4.0",
-        "prop-types": "^15.6.2"
-      },
-      "peerDependencies": {
-        "react": ">=16.6.0",
-        "react-dom": ">=16.6.0"
-      }
-    },
-    "node_modules/resolve": {
-      "version": "1.22.11",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
-      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
-      "dependencies": {
-        "is-core-module": "^2.16.1",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
-      },
-      "bin": {
-        "resolve": "bin/resolve"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "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==",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/reusify": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
-      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
-      "dev": true,
-      "engines": {
-        "iojs": ">=1.0.0",
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/rollup": {
-      "version": "4.53.2",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
-      "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==",
-      "dev": true,
-      "dependencies": {
-        "@types/estree": "1.0.8"
-      },
-      "bin": {
-        "rollup": "dist/bin/rollup"
-      },
-      "engines": {
-        "node": ">=18.0.0",
-        "npm": ">=8.0.0"
-      },
-      "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.53.2",
-        "@rollup/rollup-android-arm64": "4.53.2",
-        "@rollup/rollup-darwin-arm64": "4.53.2",
-        "@rollup/rollup-darwin-x64": "4.53.2",
-        "@rollup/rollup-freebsd-arm64": "4.53.2",
-        "@rollup/rollup-freebsd-x64": "4.53.2",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.53.2",
-        "@rollup/rollup-linux-arm-musleabihf": "4.53.2",
-        "@rollup/rollup-linux-arm64-gnu": "4.53.2",
-        "@rollup/rollup-linux-arm64-musl": "4.53.2",
-        "@rollup/rollup-linux-loong64-gnu": "4.53.2",
-        "@rollup/rollup-linux-ppc64-gnu": "4.53.2",
-        "@rollup/rollup-linux-riscv64-gnu": "4.53.2",
-        "@rollup/rollup-linux-riscv64-musl": "4.53.2",
-        "@rollup/rollup-linux-s390x-gnu": "4.53.2",
-        "@rollup/rollup-linux-x64-gnu": "4.53.2",
-        "@rollup/rollup-linux-x64-musl": "4.53.2",
-        "@rollup/rollup-openharmony-arm64": "4.53.2",
-        "@rollup/rollup-win32-arm64-msvc": "4.53.2",
-        "@rollup/rollup-win32-ia32-msvc": "4.53.2",
-        "@rollup/rollup-win32-x64-gnu": "4.53.2",
-        "@rollup/rollup-win32-x64-msvc": "4.53.2",
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/run-parallel": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
-      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "dependencies": {
-        "queue-microtask": "^1.2.2"
-      }
-    },
-    "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=="
-    },
-    "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,
-      "bin": {
-        "semver": "bin/semver.js"
-      }
-    },
-    "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,
-      "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,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/simplebar-core": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/simplebar-core/-/simplebar-core-1.3.2.tgz",
-      "integrity": "sha512-qKgTTuTqapjsFGkNhCjyPhysnbZGpQqNmjk0nOYjFN5ordC/Wjvg+RbYCyMSnW60l/Z0ZS82GbNltly6PMUH1w==",
-      "license": "MIT",
-      "dependencies": {
-        "lodash": "^4.17.21",
-        "lodash-es": "^4.17.21"
-      }
-    },
-    "node_modules/simplebar-react": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-3.3.2.tgz",
-      "integrity": "sha512-ZsgcQhKLtt5ra0BRIJeApfkTBQCa1vUPA/WXI4HcYReFt+oCEOvdVz6rR/XsGJcKxTlCRPmdGx1uJIUChupo+A==",
-      "license": "MIT",
-      "dependencies": {
-        "simplebar-core": "^1.3.2"
-      },
-      "peerDependencies": {
-        "react": ">=16.8.0"
-      }
-    },
-    "node_modules/source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "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,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/strip-bom": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "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,
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/stylis": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
-      "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
-    },
-    "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,
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/tinyglobby": {
-      "version": "0.2.15",
-      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
-      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
-      "dev": true,
-      "dependencies": {
-        "fdir": "^6.5.0",
-        "picomatch": "^4.0.3"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/SuperchupuDev"
-      }
-    },
-    "node_modules/tinyglobby/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,
-      "engines": {
-        "node": ">=12.0.0"
-      },
-      "peerDependencies": {
-        "picomatch": "^3 || ^4"
-      },
-      "peerDependenciesMeta": {
-        "picomatch": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/tinyglobby/node_modules/picomatch": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
-      "dev": true,
-      "peer": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
-      "dependencies": {
-        "is-number": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=8.0"
-      }
-    },
-    "node_modules/ts-api-utils": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
-      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=18.12"
-      },
-      "peerDependencies": {
-        "typescript": ">=4.8.4"
-      }
-    },
-    "node_modules/ts-node": {
-      "version": "10.9.2",
-      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
-      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
-      "dev": true,
-      "dependencies": {
-        "@cspotcode/source-map-support": "^0.8.0",
-        "@tsconfig/node10": "^1.0.7",
-        "@tsconfig/node12": "^1.0.7",
-        "@tsconfig/node14": "^1.0.0",
-        "@tsconfig/node16": "^1.0.2",
-        "acorn": "^8.4.1",
-        "acorn-walk": "^8.1.1",
-        "arg": "^4.1.0",
-        "create-require": "^1.1.0",
-        "diff": "^4.0.1",
-        "make-error": "^1.1.1",
-        "v8-compile-cache-lib": "^3.0.1",
-        "yn": "3.1.1"
-      },
-      "bin": {
-        "ts-node": "dist/bin.js",
-        "ts-node-cwd": "dist/bin-cwd.js",
-        "ts-node-esm": "dist/bin-esm.js",
-        "ts-node-script": "dist/bin-script.js",
-        "ts-node-transpile-only": "dist/bin-transpile.js",
-        "ts-script": "dist/bin-script-deprecated.js"
-      },
-      "peerDependencies": {
-        "@swc/core": ">=1.2.50",
-        "@swc/wasm": ">=1.2.50",
-        "@types/node": "*",
-        "typescript": ">=2.7"
-      },
-      "peerDependenciesMeta": {
-        "@swc/core": {
-          "optional": true
-        },
-        "@swc/wasm": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/tsconfig-paths": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
-      "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
-      "dev": true,
-      "dependencies": {
-        "json5": "^2.2.2",
-        "minimist": "^1.2.6",
-        "strip-bom": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "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,
-      "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,
-      "peer": true,
-      "bin": {
-        "tsc": "bin/tsc",
-        "tsserver": "bin/tsserver"
-      },
-      "engines": {
-        "node": ">=14.17"
-      }
-    },
-    "node_modules/typescript-eslint": {
-      "version": "8.46.4",
-      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz",
-      "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==",
-      "dev": true,
-      "dependencies": {
-        "@typescript-eslint/eslint-plugin": "8.46.4",
-        "@typescript-eslint/parser": "8.46.4",
-        "@typescript-eslint/typescript-estree": "8.46.4",
-        "@typescript-eslint/utils": "8.46.4"
-      },
-      "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",
-        "typescript": ">=4.8.4 <6.0.0"
-      }
-    },
-    "node_modules/uglify-js": {
-      "version": "3.19.3",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
-      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
-      "dev": true,
-      "optional": true,
-      "bin": {
-        "uglifyjs": "bin/uglifyjs"
-      },
-      "engines": {
-        "node": ">=0.8.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
-    },
-    "node_modules/update-browserslist-db": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
-      "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
-      "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"
-        }
-      ],
-      "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,
-      "dependencies": {
-        "punycode": "^2.1.0"
-      }
-    },
-    "node_modules/v8-compile-cache-lib": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
-      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
-      "dev": true
-    },
-    "node_modules/vite": {
-      "version": "7.2.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
-      "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
-      "dev": true,
-      "peer": true,
-      "dependencies": {
-        "esbuild": "^0.25.0",
-        "fdir": "^6.5.0",
-        "picomatch": "^4.0.3",
-        "postcss": "^8.5.6",
-        "rollup": "^4.43.0",
-        "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",
-        "jiti": ">=1.21.0",
-        "less": "^4.0.0",
-        "lightningcss": "^1.21.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
-        },
-        "jiti": {
-          "optional": true
-        },
-        "less": {
-          "optional": true
-        },
-        "lightningcss": {
-          "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/vite/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,
-      "engines": {
-        "node": ">=12.0.0"
-      },
-      "peerDependencies": {
-        "picomatch": "^3 || ^4"
-      },
-      "peerDependenciesMeta": {
-        "picomatch": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/vite/node_modules/picomatch": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
-      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
-      "dev": true,
-      "peer": true,
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "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,
-      "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,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/wordwrap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
-      "dev": true
-    },
-    "node_modules/yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-      "dev": true
-    },
-    "node_modules/yn": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "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,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/zod": {
-      "version": "4.1.12",
-      "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
-      "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
-      "dev": true,
-      "peer": true,
-      "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,
-      "engines": {
-        "node": ">=18.0.0"
-      },
-      "peerDependencies": {
-        "zod": "^3.25.0 || ^4.0.0"
-      }
-    }
-  }
-}
+{
+  "name": "factorio-clockmaster",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "factorio-clockmaster",
+      "version": "0.0.0",
+      "dependencies": {
+        "@emotion/react": "^11.14.0",
+        "@emotion/styled": "^11.14.1",
+        "@mui/material": "^7.3.5",
+        "pako": "^2.1.0",
+        "react": "^19.2.0",
+        "react-dom": "^19.2.0",
+        "react-router": "^8.3.0",
+        "simplebar-react": "^3.3.2"
+      },
+      "devDependencies": {
+        "@commander-js/extra-typings": "^14.0.0",
+        "@eslint/js": "^9.39.1",
+        "@types/node": "^24.10.0",
+        "@types/pako": "^2.0.4",
+        "@types/react": "^19.2.2",
+        "@types/react-dom": "^19.2.2",
+        "@vitejs/plugin-react": "^6.0.5",
+        "babel-plugin-react-compiler": "^1.0.0",
+        "commander": "^14.0.2",
+        "eslint": "^9.39.1",
+        "eslint-plugin-react-hooks": "^7.0.1",
+        "eslint-plugin-react-refresh": "^0.4.24",
+        "globals": "^16.5.0",
+        "handlebars": "^4.7.8",
+        "ora": "^9.0.0",
+        "prettier": "^3.6.2",
+        "ts-node": "^10.9.2",
+        "tsconfig-paths": "^4.2.0",
+        "typescript": "~5.9.3",
+        "typescript-eslint": "^8.46.3",
+        "vite": "^8.2.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+      "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-compilation-targets": "^7.29.7",
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helpers": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@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.8",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+      "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@babel/types": "^7.29.8",
+        "@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.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "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.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+      "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+      "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.8",
+        "@babel/helper-globals": "^7.29.7",
+        "@babel/parser": "^7.29.8",
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.8",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@commander-js/extra-typings": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz",
+      "integrity": "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "commander": "~14.0.0"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "dev": true,
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
+    "node_modules/@emotion/babel-plugin": {
+      "version": "11.13.5",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+      "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/serialize": "^1.3.3",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.2.0"
+      }
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+    },
+    "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/@emotion/cache": {
+      "version": "11.14.0",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+      "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+      "dependencies": {
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/sheet": "^1.4.0",
+        "@emotion/utils": "^1.4.2",
+        "@emotion/weak-memoize": "^0.4.0",
+        "stylis": "4.2.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
+    },
+    "node_modules/@emotion/is-prop-valid": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
+      "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
+      "dependencies": {
+        "@emotion/memoize": "^0.9.0"
+      }
+    },
+    "node_modules/@emotion/memoize": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+      "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
+    },
+    "node_modules/@emotion/react": {
+      "version": "11.14.0",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+      "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.13.5",
+        "@emotion/cache": "^11.14.0",
+        "@emotion/serialize": "^1.3.3",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+        "@emotion/utils": "^1.4.2",
+        "@emotion/weak-memoize": "^0.4.0",
+        "hoist-non-react-statics": "^3.3.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@emotion/serialize": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+      "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+      "dependencies": {
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/unitless": "^0.10.0",
+        "@emotion/utils": "^1.4.2",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@emotion/sheet": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+      "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
+    },
+    "node_modules/@emotion/styled": {
+      "version": "11.14.1",
+      "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
+      "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.13.5",
+        "@emotion/is-prop-valid": "^1.3.0",
+        "@emotion/serialize": "^1.3.3",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+        "@emotion/utils": "^1.4.2"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.0.0-rc.0",
+        "react": ">=16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
+    },
+    "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+      "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@emotion/utils": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+      "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
+    },
+    "node_modules/@emotion/weak-memoize": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+      "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+      "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+      "dev": true,
+      "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,
+      "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,
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.21.1",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+      "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+      "dev": true,
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.7",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.2"
+      },
+      "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,
+      "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,
+      "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.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+      "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+      "dev": true,
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "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.0",
+        "minimatch": "^3.1.2",
+        "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,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.39.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
+      "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+      "dev": true,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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,
+      "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==",
+      "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,
+      "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==",
+      "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=="
+    },
+    "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==",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@mui/core-downloads-tracker": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz",
+      "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      }
+    },
+    "node_modules/@mui/material": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
+      "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "@mui/core-downloads-tracker": "^7.3.5",
+        "@mui/system": "^7.3.5",
+        "@mui/types": "^7.4.8",
+        "@mui/utils": "^7.3.5",
+        "@popperjs/core": "^2.11.8",
+        "@types/react-transition-group": "^4.4.12",
+        "clsx": "^2.1.1",
+        "csstype": "^3.1.3",
+        "prop-types": "^15.8.1",
+        "react-is": "^19.2.0",
+        "react-transition-group": "^4.4.5"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.5.0",
+        "@emotion/styled": "^11.3.0",
+        "@mui/material-pigment-css": "^7.3.5",
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        },
+        "@mui/material-pigment-css": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/private-theming": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz",
+      "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "@mui/utils": "^7.3.5",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/styled-engine": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz",
+      "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "@emotion/cache": "^11.14.0",
+        "@emotion/serialize": "^1.3.3",
+        "@emotion/sheet": "^1.4.0",
+        "csstype": "^3.1.3",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.4.1",
+        "@emotion/styled": "^11.3.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/system": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz",
+      "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "@mui/private-theming": "^7.3.5",
+        "@mui/styled-engine": "^7.3.5",
+        "@mui/types": "^7.4.8",
+        "@mui/utils": "^7.3.5",
+        "clsx": "^2.1.1",
+        "csstype": "^3.1.3",
+        "prop-types": "^15.8.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@emotion/react": "^11.5.0",
+        "@emotion/styled": "^11.3.0",
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/types": {
+      "version": "7.4.8",
+      "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz",
+      "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@mui/utils": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz",
+      "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==",
+      "dependencies": {
+        "@babel/runtime": "^7.28.4",
+        "@mui/types": "^7.4.8",
+        "@types/prop-types": "^15.7.15",
+        "clsx": "^2.1.1",
+        "prop-types": "^15.8.1",
+        "react-is": "^19.2.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/mui-org"
+      },
+      "peerDependencies": {
+        "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.142.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
+      "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.8",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz",
+      "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz",
+      "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz",
+      "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz",
+      "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz",
+      "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz",
+      "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz",
+      "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz",
+      "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz",
+      "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz",
+      "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz",
+      "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz",
+      "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz",
+      "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz",
+      "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node10": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+      "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node12": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node14": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+      "dev": true
+    },
+    "node_modules/@tsconfig/node16": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+      "dev": true
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/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
+    },
+    "node_modules/@types/node": {
+      "version": "24.10.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+      "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+      "dev": true,
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@types/pako": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
+      "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/parse-json": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+      "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
+    },
+    "node_modules/@types/react": {
+      "version": "19.2.5",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
+      "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
+      "dependencies": {
+        "csstype": "^3.0.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,
+      "peerDependencies": {
+        "@types/react": "^19.2.0"
+      }
+    },
+    "node_modules/@types/react-transition-group": {
+      "version": "4.4.12",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+      "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+      "peerDependencies": {
+        "@types/react": "*"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz",
+      "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.46.4",
+        "@typescript-eslint/type-utils": "8.46.4",
+        "@typescript-eslint/utils": "8.46.4",
+        "@typescript-eslint/visitor-keys": "8.46.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^7.0.0",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.1.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.46.4",
+        "eslint": "^8.57.0 || ^9.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,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz",
+      "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.46.4",
+        "@typescript-eslint/types": "8.46.4",
+        "@typescript-eslint/typescript-estree": "8.46.4",
+        "@typescript-eslint/visitor-keys": "8.46.4",
+        "debug": "^4.3.4"
+      },
+      "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",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz",
+      "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.46.4",
+        "@typescript-eslint/types": "^8.46.4",
+        "debug": "^4.3.4"
+      },
+      "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.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz",
+      "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.46.4",
+        "@typescript-eslint/visitor-keys": "8.46.4"
+      },
+      "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.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz",
+      "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==",
+      "dev": true,
+      "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.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz",
+      "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.46.4",
+        "@typescript-eslint/typescript-estree": "8.46.4",
+        "@typescript-eslint/utils": "8.46.4",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^2.1.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",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz",
+      "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==",
+      "dev": true,
+      "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.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz",
+      "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/project-service": "8.46.4",
+        "@typescript-eslint/tsconfig-utils": "8.46.4",
+        "@typescript-eslint/types": "8.46.4",
+        "@typescript-eslint/visitor-keys": "8.46.4",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^2.1.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/brace-expansion": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+      "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "9.0.9",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+      "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz",
+      "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.7.0",
+        "@typescript-eslint/scope-manager": "8.46.4",
+        "@typescript-eslint/types": "8.46.4",
+        "@typescript-eslint/typescript-estree": "8.46.4"
+      },
+      "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",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz",
+      "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "8.46.4",
+        "eslint-visitor-keys": "^4.2.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
+      "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+        "babel-plugin-react-compiler": "^1.0.0",
+        "vite": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rolldown/plugin-babel": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+      "dev": true,
+      "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,
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+      "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.15.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+      "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+      "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-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "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,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true
+    },
+    "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
+    },
+    "node_modules/babel-plugin-macros": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+      "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "cosmiconfig": "^7.0.0",
+        "resolve": "^1.19.0"
+      },
+      "engines": {
+        "node": ">=10",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/babel-plugin-react-compiler": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
+      "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/types": "^7.26.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
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.11.12",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz",
+      "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==",
+      "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.18",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+      "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.7",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
+      "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+      "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.10.44",
+        "caniuse-lite": "^1.0.30001806",
+        "electron-to-chromium": "^1.5.393",
+        "node-releases": "^2.0.51",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "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==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001806",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+      "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+      "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,
+      "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/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz",
+      "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "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,
+      "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
+    },
+    "node_modules/commander": {
+      "version": "14.0.2",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
+      "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "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
+    },
+    "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
+    },
+    "node_modules/cookie-es": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
+      "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
+      "license": "MIT"
+    },
+    "node_modules/cosmiconfig": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+      "dependencies": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.2.1",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.10.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/cosmiconfig/node_modules/yaml": {
+      "version": "1.10.3",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
+      "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true
+    },
+    "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,
+      "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=="
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "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
+    },
+    "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/diff": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+      "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.400",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz",
+      "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+      "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "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==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.39.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
+      "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.8.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.21.1",
+        "@eslint/config-helpers": "^0.4.2",
+        "@eslint/core": "^0.17.0",
+        "@eslint/eslintrc": "^3.3.1",
+        "@eslint/js": "9.39.1",
+        "@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.12.4",
+        "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.2",
+        "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,
+      "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.4.24",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+      "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
+      "dev": true,
+      "peerDependencies": {
+        "eslint": ">=8.40"
+      }
+    },
+    "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,
+      "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,
+      "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,
+      "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.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "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,
+      "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,
+      "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,
+      "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
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "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
+    },
+    "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
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "dev": true,
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "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,
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-root": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.4.4",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+      "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+      "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/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+      "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "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,
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "16.5.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+      "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true
+    },
+    "node_modules/handlebars": {
+      "version": "4.7.9",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+      "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "wordwrap": "^1.0.0"
+      },
+      "bin": {
+        "handlebars": "bin/handlebars"
+      },
+      "engines": {
+        "node": ">=0.4.7"
+      },
+      "optionalDependencies": {
+        "uglify-js": "^3.1.4"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "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
+    },
+    "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,
+      "dependencies": {
+        "hermes-estree": "0.25.1"
+      }
+    },
+    "node_modules/hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "dependencies": {
+        "react-is": "^16.7.0"
+      }
+    },
+    "node_modules/hoist-non-react-statics/node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
+    "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,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "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,
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true
+    },
+    "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=="
+    },
+    "node_modules/js-yaml": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+      "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/nodeca"
+        }
+      ],
+      "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==",
+      "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
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+    },
+    "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
+    },
+    "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
+    },
+    "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,
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+      "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+      "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.33.0",
+        "lightningcss-darwin-arm64": "1.33.0",
+        "lightningcss-darwin-x64": "1.33.0",
+        "lightningcss-freebsd-x64": "1.33.0",
+        "lightningcss-linux-arm-gnueabihf": "1.33.0",
+        "lightningcss-linux-arm64-gnu": "1.33.0",
+        "lightningcss-linux-arm64-musl": "1.33.0",
+        "lightningcss-linux-x64-gnu": "1.33.0",
+        "lightningcss-linux-x64-musl": "1.33.0",
+        "lightningcss-win32-arm64-msvc": "1.33.0",
+        "lightningcss-win32-x64-msvc": "1.33.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+      "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+      "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+      "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+      "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+      "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+      "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+      "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+      "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+      "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+      "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+      "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.33.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+      "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+      "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/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+    },
+    "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,
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "license": "MIT"
+    },
+    "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
+    },
+    "node_modules/log-symbols": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
+      "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-unicode-supported": "^2.0.0",
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "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/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "dev": true
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "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/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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=="
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.17",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
+      "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
+      "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
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.52",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz",
+      "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "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/ora": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz",
+      "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.6.2",
+        "cli-cursor": "^5.0.0",
+        "cli-spinners": "^3.2.0",
+        "is-interactive": "^2.0.0",
+        "is-unicode-supported": "^2.1.0",
+        "log-symbols": "^7.0.1",
+        "stdin-discarder": "^0.2.2",
+        "string-width": "^8.1.0",
+        "strip-ansi": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/chalk": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "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,
+      "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,
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+      "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==",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "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,
+      "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,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "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=="
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.25",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
+      "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
+      "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.16",
+        "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,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+      "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "node_modules/prop-types/node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
+    "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,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/react": {
+      "version": "19.2.8",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+      "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.2.8",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+      "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.27.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.8"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
+      "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="
+    },
+    "node_modules/react-router": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz",
+      "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie-es": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=22.22.0"
+      },
+      "peerDependencies": {
+        "react": ">=19.2.7",
+        "react-dom": ">=19.2.7"
+      },
+      "peerDependenciesMeta": {
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.11",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+      "dependencies": {
+        "is-core-module": "^2.16.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "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==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rolldown": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz",
+      "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.142.0",
+        "@rolldown/pluginutils": "^1.0.0"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.2.2",
+        "@rolldown/binding-darwin-arm64": "1.2.2",
+        "@rolldown/binding-darwin-x64": "1.2.2",
+        "@rolldown/binding-freebsd-x64": "1.2.2",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.2.2",
+        "@rolldown/binding-linux-arm64-gnu": "1.2.2",
+        "@rolldown/binding-linux-arm64-musl": "1.2.2",
+        "@rolldown/binding-linux-ppc64-gnu": "1.2.2",
+        "@rolldown/binding-linux-s390x-gnu": "1.2.2",
+        "@rolldown/binding-linux-x64-gnu": "1.2.2",
+        "@rolldown/binding-linux-x64-musl": "1.2.2",
+        "@rolldown/binding-openharmony-arm64": "1.2.2",
+        "@rolldown/binding-win32-arm64-msvc": "1.2.2",
+        "@rolldown/binding-win32-x64-msvc": "1.2.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "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=="
+    },
+    "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/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,
+      "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,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/simplebar-core": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/simplebar-core/-/simplebar-core-1.3.2.tgz",
+      "integrity": "sha512-qKgTTuTqapjsFGkNhCjyPhysnbZGpQqNmjk0nOYjFN5ordC/Wjvg+RbYCyMSnW60l/Z0ZS82GbNltly6PMUH1w==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21"
+      }
+    },
+    "node_modules/simplebar-react": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-3.3.2.tgz",
+      "integrity": "sha512-ZsgcQhKLtt5ra0BRIJeApfkTBQCa1vUPA/WXI4HcYReFt+oCEOvdVz6rR/XsGJcKxTlCRPmdGx1uJIUChupo+A==",
+      "license": "MIT",
+      "dependencies": {
+        "simplebar-core": "^1.3.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stdin-discarder": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+      "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
+      "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.3.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/stylis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+      "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+    },
+    "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,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/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/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/ts-node": {
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+      "dev": true,
+      "dependencies": {
+        "@cspotcode/source-map-support": "^0.8.0",
+        "@tsconfig/node10": "^1.0.7",
+        "@tsconfig/node12": "^1.0.7",
+        "@tsconfig/node14": "^1.0.0",
+        "@tsconfig/node16": "^1.0.2",
+        "acorn": "^8.4.1",
+        "acorn-walk": "^8.1.1",
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "v8-compile-cache-lib": "^3.0.1",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-cwd": "dist/bin-cwd.js",
+        "ts-node-esm": "dist/bin-esm.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "peerDependencies": {
+        "@swc/core": ">=1.2.50",
+        "@swc/wasm": ">=1.2.50",
+        "@types/node": "*",
+        "typescript": ">=2.7"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/wasm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tsconfig-paths": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
+      "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+      "dev": true,
+      "dependencies": {
+        "json5": "^2.2.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "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,
+      "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,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/typescript-eslint": {
+      "version": "8.46.4",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz",
+      "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "8.46.4",
+        "@typescript-eslint/parser": "8.46.4",
+        "@typescript-eslint/typescript-estree": "8.46.4",
+        "@typescript-eslint/utils": "8.46.4"
+      },
+      "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",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/uglify-js": {
+      "version": "3.19.3",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+      "dev": true,
+      "optional": true,
+      "bin": {
+        "uglifyjs": "bin/uglifyjs"
+      },
+      "engines": {
+        "node": ">=0.8.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
+    },
+    "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,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+      "dev": true
+    },
+    "node_modules/vite": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz",
+      "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.33.0",
+        "picomatch": "^4.0.5",
+        "postcss": "^8.5.23",
+        "rolldown": "~1.2.0",
+        "tinyglobby": "^0.2.17"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.4.0",
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite/node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "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,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+      "dev": true
+    },
+    "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/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "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,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yoctocolors": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
+      "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "4.1.12",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
+      "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
+      "dev": true,
+      "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,
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "peerDependencies": {
+        "zod": "^3.25.0 || ^4.0.0"
+      }
+    }
+  }
+}

+ 48 - 42
package.json

@@ -1,42 +1,48 @@
-{
-  "name": "factorio-clockmaster",
-  "private": true,
-  "version": "0.0.0",
-  "type": "module",
-  "scripts": {
-    "dev": "vite",
-    "build": "tsc -b && vite build",
-    "lint": "eslint .",
-    "preview": "vite preview",
-    "factorio-api": "ts-node -r tsconfig-paths/register scripts/factorio-api.ts",
-    "postfactorio-api": "prettier --write \"scripts/factorio-dump.models.ts\"",
-    "factorio-process": "ts-node -r tsconfig-paths/register scripts/factorio-process-data.ts"
-  },
-  "dependencies": {
-    "@emotion/react": "^11.14.0",
-    "@emotion/styled": "^11.14.1",
-    "@mui/material": "^7.3.5",
-    "react": "^19.2.0",
-    "react-dom": "^19.2.0",
-    "simplebar-react": "^3.3.2"
-  },
-  "devDependencies": {
-    "@eslint/js": "^9.39.1",
-    "@types/node": "^24.10.0",
-    "@types/react": "^19.2.2",
-    "@types/react-dom": "^19.2.2",
-    "@vitejs/plugin-react": "^5.1.0",
-    "babel-plugin-react-compiler": "^1.0.0",
-    "eslint": "^9.39.1",
-    "eslint-plugin-react-hooks": "^7.0.1",
-    "eslint-plugin-react-refresh": "^0.4.24",
-    "globals": "^16.5.0",
-    "handlebars": "^4.7.8",
-    "prettier": "^3.6.2",
-    "ts-node": "^10.9.2",
-    "tsconfig-paths": "^4.2.0",
-    "typescript": "~5.9.3",
-    "typescript-eslint": "^8.46.3",
-    "vite": "^7.2.2"
-  }
-}
+{
+  "name": "factorio-clockmaster",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "lint": "eslint .",
+    "preview": "vite preview",
+    "factorio-api": "ts-node -r tsconfig-paths/register scripts/factorio-dump/lua-api/index.ts",
+    "postfactorio-api": "prettier --write \"scripts/factorio-dump/lua-api/models.ts\"",
+    "factorio-process": "ts-node -r tsconfig-paths/register scripts/factorio-dump/index.ts"
+  },
+  "dependencies": {
+    "@emotion/react": "^11.14.0",
+    "@emotion/styled": "^11.14.1",
+    "@mui/material": "^7.3.5",
+    "pako": "^2.1.0",
+    "react": "^19.2.0",
+    "react-dom": "^19.2.0",
+    "react-router": "^8.3.0",
+    "simplebar-react": "^3.3.2"
+  },
+  "devDependencies": {
+    "@commander-js/extra-typings": "^14.0.0",
+    "@eslint/js": "^9.39.1",
+    "@types/node": "^24.10.0",
+    "@types/pako": "^2.0.4",
+    "@types/react": "^19.2.2",
+    "@types/react-dom": "^19.2.2",
+    "@vitejs/plugin-react": "^6.0.5",
+    "babel-plugin-react-compiler": "^1.0.0",
+    "commander": "^14.0.2",
+    "eslint": "^9.39.1",
+    "eslint-plugin-react-hooks": "^7.0.1",
+    "eslint-plugin-react-refresh": "^0.4.24",
+    "globals": "^16.5.0",
+    "handlebars": "^4.7.8",
+    "ora": "^9.0.0",
+    "prettier": "^3.6.2",
+    "ts-node": "^10.9.2",
+    "tsconfig-paths": "^4.2.0",
+    "typescript": "~5.9.3",
+    "typescript-eslint": "^8.46.3",
+    "vite": "^8.2.0"
+  }
+}

BIN=BIN
public/assets/empty-module-slot.png


BIN=BIN
public/assets/no-item.png


BIN=BIN
public/data/2.0/icon_128.webp


BIN=BIN
public/data/2.0/icon_64.webp


+ 85 - 85
scripts/helpers/command.helper.ts → scripts/factorio-dump/helpers/command.helper.ts

@@ -1,85 +1,85 @@
-import { exec, spawn } from "child_process";
-import fs from "fs";
-
-const logsPath = `./scripts/logs`;
-const logPath = `${logsPath}/factorio-update.log`;
-if (!fs.existsSync(logsPath)) fs.mkdirSync(logsPath);
-if (fs.existsSync(logPath)) fs.rmSync(logPath);
-/** Log command output to separate log file to minimize console output */
-function logDebug(msg: string): void {
-  fs.appendFileSync(logPath, msg);
-}
-
-async function runCommand(command: string, args?: string[]): Promise<number | null> {
-  return new Promise((resolve, reject) => {
-    const proc = spawn(command, args);
-    proc.stdout.on("data", (data: string) => {
-      logDebug(data);
-    });
-    proc.stderr.on("data", (data: string) => {
-      logDebug(data);
-    });
-    proc.on("error", (code) => {
-      reject(code);
-    });
-    proc.on("exit", (code) => {
-      resolve(code);
-    });
-  });
-}
-
-const FACTORIO_BIN_PATH = `C:\\Program Files\\Factorio\\bin\\x64\\factorio.exe`;
-/**
- * Dumps data.raw as JSON to the script output folder and exits.
- * the output are genereate in Factorio data folder under script-output/.
- * more at https://wiki.factorio.com/Command_line_parameters
- * @returns
- */
-export const dumpFactorioData = async () => {
-  await runCommand(FACTORIO_BIN_PATH, ["--dump-data"]);
-  await waitForFactorio(true, 10000);
-  await waitForFactorio(false, 30000);
-};
-/**
- * Dumps all prototypes name and description (if they have a valid value) to the script output folder and exits.
- * the output are genereate in Factorio data folder under script-output.
- * more at https://wiki.factorio.com/Command_line_parameters
- * @returns
- */
-export const dumpFactorioLocale = async () => {
-  await runCommand(FACTORIO_BIN_PATH, ["--dump-prototype-locale"]);
-  await waitForFactorio(true, 10000);
-  await waitForFactorio(false, 30000);
-};
-export const dumpFactorioIcon = async () => {
-  await runCommand(FACTORIO_BIN_PATH, ["--dump-icon-sprites"]);
-  await waitForFactorio(true, 10000);
-  await waitForFactorio(false, 60000);
-};
-
-/** Check whether Factorio is running after a delay */
-export async function checkIfFactorioIsRunning(delayMs = 1000): Promise<boolean> {
-  return new Promise((resolve, reject) => {
-    setTimeout(() => {
-      exec("tasklist", (err, stdout, _) => {
-        if (err != null) reject(err);
-        resolve(stdout.toLowerCase().includes("factorio.exe"));
-      });
-    }, delayMs);
-  });
-}
-
-/**
- * Wait for factorio.exe to exit, since the image dump continues to run after
- * the batch script exits.
- */
-async function waitForFactorio(running: boolean, waitMs: number): Promise<void> {
-  const start = Date.now();
-  let result = false;
-  let runtime = 0;
-  do {
-    result = await checkIfFactorioIsRunning();
-    runtime = Date.now() - start;
-  } while (result !== running && runtime < waitMs);
-  logDebug(`Waited ${runtime.toString()} for Factorio to ${running ? "start" : "exit"}\n`);
-}
+import { exec, spawn } from "child_process";
+import fs from "fs";
+
+const logsPath = `./scripts/logs`;
+const logPath = `${logsPath}/factorio-update.log`;
+if (!fs.existsSync(logsPath)) fs.mkdirSync(logsPath);
+if (fs.existsSync(logPath)) fs.rmSync(logPath);
+/** Log command output to separate log file to minimize console output */
+function logDebug(msg: string): void {
+  fs.appendFileSync(logPath, msg);
+}
+
+async function runCommand(command: string, args?: string[]): Promise<number | null> {
+  return new Promise((resolve, reject) => {
+    const proc = spawn(command, args);
+    proc.stdout.on("data", (data: string) => {
+      logDebug(data);
+    });
+    proc.stderr.on("data", (data: string) => {
+      logDebug(data);
+    });
+    proc.on("error", (code) => {
+      reject(code);
+    });
+    proc.on("exit", (code) => {
+      resolve(code);
+    });
+  });
+}
+
+const FACTORIO_BIN_PATH = `C:\\Program Files\\Factorio\\bin\\x64\\factorio.exe`;
+/**
+ * Dumps data.raw as JSON to the script output folder and exits.
+ * the output are genereate in Factorio data folder under script-output/.
+ * more at https://wiki.factorio.com/Command_line_parameters
+ * @returns
+ */
+export const dumpFactorioData = async () => {
+  await runCommand(FACTORIO_BIN_PATH, ["--dump-data"]);
+  await waitForFactorio(true, 10000);
+  await waitForFactorio(false, 30000);
+};
+/**
+ * Dumps all prototypes name and description (if they have a valid value) to the script output folder and exits.
+ * the output are genereate in Factorio data folder under script-output.
+ * more at https://wiki.factorio.com/Command_line_parameters
+ * @returns
+ */
+export const dumpFactorioLocale = async () => {
+  await runCommand(FACTORIO_BIN_PATH, ["--dump-prototype-locale"]);
+  await waitForFactorio(true, 10000);
+  await waitForFactorio(false, 30000);
+};
+export const dumpFactorioIcon = async () => {
+  await runCommand(FACTORIO_BIN_PATH, ["--dump-icon-sprites"]);
+  await waitForFactorio(true, 10000);
+  await waitForFactorio(false, 60000);
+};
+
+/** Check whether Factorio is running after a delay */
+export async function checkIfFactorioIsRunning(delayMs = 1000): Promise<boolean> {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      exec("tasklist", (err, stdout, _) => {
+        if (err != null) reject(err);
+        resolve(stdout.toLowerCase().includes("factorio.exe"));
+      });
+    }, delayMs);
+  });
+}
+
+/**
+ * Wait for factorio.exe to exit, since the image dump continues to run after
+ * the batch script exits.
+ */
+async function waitForFactorio(running: boolean, waitMs: number): Promise<void> {
+  const start = Date.now();
+  let result = false;
+  let runtime = 0;
+  do {
+    result = await checkIfFactorioIsRunning();
+    runtime = Date.now() - start;
+  } while (result !== running && runtime < waitMs);
+  logDebug(`Waited ${runtime.toString()} for Factorio to ${running ? "start" : "exit"}\n`);
+}

+ 31 - 31
scripts/helpers/file.helper.ts → scripts/factorio-dump/helpers/file.helper.ts

@@ -1,31 +1,31 @@
-import fs from "fs";
-import path from "path";
-export function getJsonData(file: string): unknown {
-  const str = fs.readFileSync(file).toString();
-  return JSON.parse(str);
-}
-
-const appDataPath = process.env["AppData"] || `${process.env["HOME"] ?? ""}/Library/Application Support`;
-export const factorioPath = `${appDataPath}/Factorio`;
-export const scriptOutputPath = `${factorioPath}/script-output`;
-export const dataRawPath = `${scriptOutputPath}/data-raw-dump.json`;
-
-type Locale = Record<string, string>;
-
-export function getLocale(file: string): Locale {
-  const path = `${scriptOutputPath}/${file}`;
-  return getJsonData(path) as Locale;
-}
-
-export function checkIcons(icons: string[]) {
-  const missing: string[] = [];
-
-  for (const icon of icons) {
-    const fullPath = path.resolve(scriptOutputPath, icon);
-    if (!fs.existsSync(fullPath)) missing.push(fullPath);
-  }
-  if (missing.length) {
-    console.log(`❌ ${missing.length} icon(s) are missing:`);
-    console.log(missing.map((p) => `  • ${p}`).join("\n"));
-  }
-}
+import fs from "fs";
+import path from "path";
+export function getJsonData(file: string): unknown {
+  const str = fs.readFileSync(file).toString();
+  return JSON.parse(str);
+}
+
+const appDataPath = process.env["AppData"] || `${process.env["HOME"] ?? ""}/Library/Application Support`;
+export const factorioPath = `${appDataPath}/Factorio`;
+export const scriptOutputPath = `${factorioPath}/script-output`;
+export const dataRawPath = `${scriptOutputPath}/data-raw-dump.json`;
+
+type Locale = Record<string, string>;
+
+export function getLocale(file: string): Locale {
+  const path = `${scriptOutputPath}/${file}`;
+  return getJsonData(path) as Locale;
+}
+
+export function checkIcons(icons: string[]) {
+  const missing: string[] = [];
+
+  for (const icon of icons) {
+    const fullPath = path.resolve(scriptOutputPath, icon);
+    if (!fs.existsSync(fullPath)) missing.push(fullPath);
+  }
+  if (missing.length > 0) {
+    console.log(`❌ ${missing.length} icon(s) are missing:`);
+    console.log(missing.map((p) => `  • ${p}`).join("\n"));
+  }
+}

+ 164 - 161
scripts/helpers/groups.helper.ts → scripts/factorio-dump/helpers/groups.helper.ts

@@ -1,161 +1,164 @@
-import type { DataRawDump } from "../factorio-process-data.models.ts";
-
-/**
- * Compare two prototype objects by their `order` field.
- * If `order` is missing, it defaults to an empty string.
- *
- * @param a - First prototype object.
- * @param b - Second prototype object.
- * @returns Negative if `a.order < b.order`, positive if greater, zero otherwise.
- */
-function orderFactorioPrototype(a: { order?: string }, b: { order?: string }) {
-  return (a.order ?? "")?.localeCompare(b.order ?? "");
-}
-
-export type SubGroup<T> = {
-  name: string;
-  order?: string;
-  children: Array<T>;
-};
-export type Group<T> = {
-  name: string;
-  order?: string;
-  icon?: string;
-  subGroup: Array<SubGroup<T>>;
-};
-/** Type of an object that can be grouped by its `subgroup` property. */
-export interface GroupablaPrototype {
-  subgroup: string;
-  order?: string;
-}
-/**
- * Create a map of subgroup names to their corresponding `SubGroup` objects.
- *
- * @param prototypes - The prototypes to group.
- * @param subGroupsDefinitions - Definition objects for the available subgroups.
- * @returns Map where the key is the subgroup name and the value is a `SubGroup`.
- */
-function createSubGroupMap<T extends GroupablaPrototype>(
-  prototypes: Array<T>,
-  subGroupsDefinitions: Array<{ name: string; order?: string }>
-): Map<string, SubGroup<T>> {
-  const map = new Map<string, SubGroup<T>>();
-
-  for (const proto of prototypes) {
-    if (!map.has(proto.subgroup)) {
-      const subGroup = subGroupsDefinitions.find((sg) => sg.name === proto.subgroup);
-      if (subGroup) {
-        map.set(proto.subgroup, {
-          name: subGroup.name,
-          order: subGroup.order ?? "",
-          children: [],
-        });
-      }
-    }
-
-    const subgroup = map.get(proto.subgroup);
-    subgroup?.children.push(proto);
-  }
-
-  return map;
-}
-
-/**
- * Create a map of group names to their corresponding `Group` objects.
- *
- * @param subGroupMap - Map of subgroup names to `SubGroup` objects.
- * @param groupsDef - Definition objects for the available groups.
- * @param subGroupsDef - Definition objects for the available subgroups.
- * @returns Map where the key is the group name and the value is a `Group`.
- */
-function createGroupMap<T extends GroupablaPrototype>(
-  subGroupMap: Map<string, SubGroup<T>>,
-  groupsDef: Array<{ name: string; order?: string }>,
-  subGroupsDef: Array<{ name: string; group: string; order?: string }>
-): Map<string, Group<T>> {
-  const map = new Map<string, Group<T>>();
-
-  for (const subGroupDef of subGroupsDef) {
-    const subGroup = subGroupMap.get(subGroupDef.name);
-    if (!subGroup || subGroup.children.length === 0) continue;
-
-    if (!map.has(subGroupDef.group)) {
-      const groupDef = groupsDef.find((g) => g.name === subGroupDef.group);
-      if (groupDef) {
-        map.set(subGroupDef.group, {
-          name: groupDef.name,
-          icon: `item-group/${groupDef.name}.png`,
-          order: groupDef.order,
-          subGroup: [],
-        });
-      }
-    }
-
-    map.get(subGroupDef.group)?.subGroup.push(subGroup);
-  }
-
-  return map;
-}
-
-/**
- * Sort groups, subgroups, and their children by the `order` field.
- *
- * @param groups - Array of `Group` objects to be sorted.
- */
-function sortGroups<T extends GroupablaPrototype>(groups: Group<T>[]) {
-  groups.sort(orderFactorioPrototype);
-
-  for (const group of groups) {
-    group.subGroup.sort(orderFactorioPrototype);
-
-    for (const subGroup of group.subGroup) {
-      subGroup.children.sort(orderFactorioPrototype);
-    }
-  }
-}
-/**
- * Remove the `order` property from all groups, subgroups, and prototypes.
- *
- * @param groups - Array of `Group` objects from which to delete `order`.
- */
-function stripOrder<T extends GroupablaPrototype>(groups: Group<T>[]) {
-  for (const group of groups) {
-    delete group.order;
-    group.subGroup.sort(orderFactorioPrototype);
-
-    for (const subGroup of group.subGroup) {
-      delete subGroup.order;
-
-      for (const child of subGroup.children) {
-        delete child.order;
-      }
-    }
-  }
-}
-/**
- * Group the factorio prototypes according to there subgroups
- * @param arr the array of item to be grouped
- * @param data the rawData object extracted from Factorio
- * @param keepOrder flag to indicate if the order values must be kept in the final output
- * @returns
- */
-export function groupPrototypes<T extends GroupablaPrototype>(
-  arr: Array<T>,
-  data: DataRawDump,
-  keepOrder: boolean = true
-): Group<T>[] {
-  const itemGroups = Object.values(data["item-group"]).filter((o) => !o.hidden);
-  const itemSubGroups = Object.values(data["item-subgroup"]).filter((o) => !o.hidden);
-
-  const subGroupMap = createSubGroupMap(arr, itemSubGroups);
-  const groupMap = createGroupMap(subGroupMap, itemGroups, itemSubGroups);
-
-  const output = Array.from(groupMap.values());
-  // Sort the output
-  sortGroups(output);
-
-  if (!keepOrder) {
-    stripOrder(output);
-  }
-  return output;
-}
+import type { FactorioRawData } from "../lua-api/models.ts";
+
+/**
+ * Compare two prototype objects by their `order` field.
+ * If `order` is missing, it defaults to an empty string.
+ *
+ * @param a - First prototype object.
+ * @param b - Second prototype object.
+ * @returns Negative if `a.order < b.order`, positive if greater, zero otherwise.
+ */
+function orderFactorioPrototype(a: { order?: string }, b: { order?: string }) {
+  return (a.order ?? "")?.localeCompare(b.order ?? "");
+}
+
+export type SubGroup<T> = {
+  name: string;
+  order?: string;
+  children: Array<T>;
+};
+export type Group<T> = {
+  name: string;
+  order?: string;
+  icon?: string;
+  subGroup: Array<SubGroup<T>>;
+};
+/** Type of an object that can be grouped by its `subgroup` property. */
+export interface GroupablaPrototype {
+  subgroup: string;
+  order?: string;
+}
+/**
+ * Create a map of subgroup names to their corresponding `SubGroup` objects.
+ *
+ * @param prototypes - The prototypes to group.
+ * @param subGroupsDefinitions - Definition objects for the available subgroups.
+ * @returns Map where the key is the subgroup name and the value is a `SubGroup`.
+ */
+function createSubGroupMap<T extends GroupablaPrototype>(
+  prototypes: Array<T>,
+  subGroupsDefinitions: Array<{ name: string; order?: string }>
+): Map<string, SubGroup<T>> {
+  const map = new Map<string, SubGroup<T>>();
+
+  for (const proto of prototypes) {
+    const subgroupId = proto.subgroup;
+    if (!map.has(subgroupId)) {
+      const subGroup = subGroupsDefinitions.find((sg) => sg.name === subgroupId);
+      if (subGroup) {
+        map.set(subgroupId, {
+          name: subGroup.name,
+          order: subGroup.order ?? "",
+          children: [],
+        });
+      } else {
+        console.error(`Subgroup information not found "${proto.subgroup}"`);
+      }
+    }
+
+    const subgroup = map.get(subgroupId);
+    subgroup?.children.push(proto);
+  }
+
+  return map;
+}
+
+/**
+ * Create a map of group names to their corresponding `Group` objects.
+ *
+ * @param subGroupMap - Map of subgroup names to `SubGroup` objects.
+ * @param groupsDef - Definition objects for the available groups.
+ * @param subGroupsDef - Definition objects for the available subgroups.
+ * @returns Map where the key is the group name and the value is a `Group`.
+ */
+function createGroupMap<T extends GroupablaPrototype>(
+  subGroupMap: Map<string, SubGroup<T>>,
+  groupsDef: Array<{ name: string; order?: string }>,
+  subGroupsDef: Array<{ name: string; group: string; order?: string }>
+): Map<string, Group<T>> {
+  const map = new Map<string, Group<T>>();
+
+  for (const subGroupDef of subGroupsDef) {
+    const subGroup = subGroupMap.get(subGroupDef.name);
+    if (!subGroup || subGroup.children.length === 0) continue;
+
+    if (!map.has(subGroupDef.group)) {
+      const groupDef = groupsDef.find((g) => g.name === subGroupDef.group);
+      if (groupDef) {
+        map.set(subGroupDef.group, {
+          name: groupDef.name,
+          icon: `item-group/${groupDef.name}.png`,
+          order: groupDef.order,
+          subGroup: [],
+        });
+      }
+    }
+
+    map.get(subGroupDef.group)?.subGroup.push(subGroup);
+  }
+
+  return map;
+}
+
+/**
+ * Sort groups, subgroups, and their children by the `order` field.
+ *
+ * @param groups - Array of `Group` objects to be sorted.
+ */
+function sortGroups<T extends GroupablaPrototype>(groups: Group<T>[]) {
+  groups.sort(orderFactorioPrototype);
+
+  for (const group of groups) {
+    group.subGroup.sort(orderFactorioPrototype);
+
+    for (const subGroup of group.subGroup) {
+      subGroup.children.sort(orderFactorioPrototype);
+    }
+  }
+}
+/**
+ * Remove the `order` property from all groups, subgroups, and prototypes.
+ *
+ * @param groups - Array of `Group` objects from which to delete `order`.
+ */
+function stripOrder<T extends GroupablaPrototype>(groups: Group<T>[]) {
+  for (const group of groups) {
+    delete group.order;
+    group.subGroup.sort(orderFactorioPrototype);
+
+    for (const subGroup of group.subGroup) {
+      delete subGroup.order;
+
+      for (const child of subGroup.children) {
+        delete child.order;
+      }
+    }
+  }
+}
+/**
+ * Group the factorio prototypes according to there subgroups
+ * @param arr the array of item to be grouped
+ * @param data the rawData object extracted from Factorio
+ * @param keepOrder flag to indicate if the order values must be kept in the final output
+ * @returns
+ */
+export function groupPrototypes<T extends GroupablaPrototype>(
+  arr: Array<T>,
+  data: FactorioRawData,
+  keepOrder: boolean = true
+): Group<T>[] {
+  const itemGroups = Object.values(data["item-group"] ?? {}).filter((o) => !o.hidden);
+  const itemSubGroups = Object.values(data["item-subgroup"] ?? {}).filter((o) => !o.hidden);
+
+  const subGroupMap = createSubGroupMap(arr, itemSubGroups);
+  const groupMap = createGroupMap(subGroupMap, itemGroups, itemSubGroups);
+
+  const output = Array.from(groupMap.values());
+  // Sort the output
+  sortGroups(output);
+
+  if (!keepOrder) {
+    stripOrder(output);
+  }
+  return output;
+}

+ 166 - 165
scripts/helpers/icons.helper.ts → scripts/factorio-dump/helpers/icons.helper.ts

@@ -1,165 +1,166 @@
-import fs from "fs";
-import path from "path";
-import sharp from "sharp";
-
-/**
- * Recursively search an object for any property called `icon`
- * and push the string value to the supplied Set.
- *
- * @param obj  The object to search (may be nested)
- * @param set  The Set that collects unique icon strings
- */
-export function walkForIcons(obj: unknown, set: Set<string>) {
-  if (obj && typeof obj === "object") {
-    if (Array.isArray(obj)) {
-      for (const item of obj) walkForIcons(item, set);
-    } else {
-      for (const [key, value] of Object.entries(obj)) {
-        if (key === "icon" && typeof value === "string") {
-          set.add(value);
-        } else {
-          walkForIcons(value, set);
-        }
-      }
-    }
-  }
-}
-
-/**
- * Metadata for a single icon.
- * All icons are guaranteed to be square.
- */
-interface IconMeta {
-  name: string; // icon name (without extension)
-  size: number; // width = height
-  buffer: Buffer;
-  file: string; // absolute path to the PNG
-}
-/**
- * Position of an icon inside its texture.
- */
-export interface IconPosition {
-  name: string;
-  x: number;
-  y: number;
-  size: number;
-}
-async function validateIcons(icons: string[], scriptOutputPath: string) {
-  const validIcons: IconMeta[] = [];
-  for (const icon of icons) {
-    const absPath = path.resolve(scriptOutputPath, icon);
-
-    // Skip if file does not exist or is not a PNG
-    if (!fs.existsSync(absPath) || path.extname(absPath).toLowerCase() !== ".png") continue;
-
-    const stat = fs.statSync(absPath);
-    if (stat.size === 0) continue; // skip empty files
-
-    const buffer = fs.readFileSync(absPath);
-    const meta = await sharp(buffer).metadata();
-
-    if (!meta.width || !meta.height || meta.width !== meta.height) continue; // skip non‑square
-
-    validIcons.push({
-      name: icon,
-      file: absPath,
-      buffer,
-      size: meta.width,
-    });
-  }
-  return validIcons;
-}
-function groupBySize(metas: IconMeta[]): Record<string, IconMeta[]> {
-  const groups: Record<string, IconMeta[]> = {};
-  for (const m of metas) {
-    const key = String(m.size);
-    groups[key] ??= [];
-    groups[key].push(m);
-  }
-  return groups;
-}
-/**
- * Packs a set of square icons of the same size into a single texture image.
- *
- * The icons are laid out in a regular grid (rows × columns) where the
- * number of columns is the ceiling of the square root of the icon count,
- * and the number of rows is the smallest integer that can hold all icons.
- *
- * @param icons      Array of {@link IconMeta} objects that all share the same `size`.
- * @param iconSize   Width (and height) of each icon in pixels.
- * @returns An object containing:
- *   - `textureBuffer`:  Buffer with the resulting WEBP image that contains
- *     all icons arranged in the grid.
- *   - `iconMap`:        Mapping from icon name to its position (`x`, `y`,
- *     `width`, `height`) inside the texture.  `width` and `height` are equal
- *     to `iconSize`.
- *
- * @remarks
- *   * All icons are guaranteed to be square; `iconSize` is the common
- *     dimension of every icon in `icons`.
- *   * The texture background is fully transparent.
- */
-async function createTextureMap(
-  icons: IconMeta[],
-  iconSize: number
-): Promise<{ textureBuffer: Buffer; iconMap: Record<string, IconPosition> }> {
-  const n = icons.length;
-  const cols = Math.ceil(Math.sqrt(n));
-  const rows = Math.ceil(n / cols);
-  const canvasW = cols * iconSize;
-  const canvasH = rows * iconSize;
-
-  const composites: sharp.OverlayOptions[] = [];
-  const map: Record<string, IconPosition> = {};
-
-  icons.forEach((icon, idx) => {
-    const x = (idx % cols) * iconSize;
-    const y = Math.floor(idx / cols) * iconSize;
-    composites.push({ input: icon.file, left: x, top: y });
-    map[icon.name] = { name: icon.name, x, y, size: iconSize };
-  });
-
-  const buffer = await sharp({
-    create: {
-      width: canvasW,
-      height: canvasH,
-      channels: 4,
-      background: { r: 0, g: 0, b: 0, alpha: 0 },
-    },
-  })
-    .composite(composites)
-    .webp({ lossless: true })
-    .toBuffer();
-
-  return { textureBuffer: buffer, iconMap: map };
-}
-/**
- * Build a texture map from a list of PNG icons.
- *
- * @param icons            Array of icon file paths (relative to scriptOutputPath)
- * @param scriptOutputPath Base directory where the icons are located
- * @param outputDir        Destination directory for the generated files.
- *
- * The function will:
- *   • Filter out non‑PNG files, missing files or empty entries.
- *   • Ensure every PNG is square and that group icons by dimensions.
- *   • Pack the icons into a single WebP texture (grid layout).
- *   • Output `icon_{size}.webp` and `iconMap.json` into `outputDir`.
- */
-export async function buildIconTextureMap(icons: string[], scriptOutputPath: string, outputDir: string): Promise<void> {
-  const validIcons = await validateIcons(icons, scriptOutputPath);
-  if (validIcons.length === 0) {
-    throw new Error("No valid PNG icons found.");
-  }
-
-  const iconPositions: IconPosition[] = [];
-  for (let [size, group] of Object.entries(groupBySize(validIcons))) {
-    const outTexturePath = path.join(outputDir, `icon_${size}.webp`);
-    const { textureBuffer, iconMap } = await createTextureMap(group, parseInt(size));
-    fs.writeFileSync(outTexturePath, textureBuffer);
-    iconPositions.push(...Object.values(iconMap));
-  }
-
-  const outMapPath = path.join(outputDir, "iconMap.json");
-  fs.writeFileSync(outMapPath, JSON.stringify(iconPositions, null, 2), "utf-8");
-}
+import fs from "fs";
+import path from "path";
+import sharp from "sharp";
+import { DEFAULT_OUTPUT_FOLDER } from "../index.ts";
+
+/**
+ * Recursively search an object for any property called `icon`
+ * and push the string value to the supplied Set.
+ *
+ * @param obj  The object to search (may be nested)
+ * @param set  The Set that collects unique icon strings
+ */
+export function walkForIcons(obj: unknown, set: Set<string>) {
+  if (obj && typeof obj === "object") {
+    if (Array.isArray(obj)) {
+      for (const item of obj) walkForIcons(item, set);
+    } else {
+      for (const [key, value] of Object.entries(obj)) {
+        if (key === "icon" && typeof value === "string") {
+          set.add(value);
+        } else {
+          walkForIcons(value, set);
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Metadata for a single icon.
+ * All icons are guaranteed to be square.
+ */
+interface IconMeta {
+  name: string; // icon name (without extension)
+  size: number; // width = height
+  buffer: Buffer;
+  file: string; // absolute path to the PNG
+}
+/**
+ * Position of an icon inside its texture.
+ */
+export interface IconPosition {
+  name: string;
+  x: number;
+  y: number;
+  size: number;
+}
+async function validateIcons(icons: string[], scriptOutputPath: string) {
+  const validIcons: IconMeta[] = [];
+  for (const icon of icons) {
+    const absPath = path.resolve(scriptOutputPath, icon);
+
+    // Skip if file does not exist or is not a PNG
+    if (!fs.existsSync(absPath) || path.extname(absPath).toLowerCase() !== ".png") continue;
+
+    const stat = fs.statSync(absPath);
+    if (stat.size === 0) continue; // skip empty files
+
+    const buffer = fs.readFileSync(absPath);
+    const meta = await sharp(buffer).metadata();
+
+    if (!meta.width || !meta.height || meta.width !== meta.height) continue; // skip non‑square
+
+    validIcons.push({
+      name: icon,
+      file: absPath,
+      buffer,
+      size: meta.width,
+    });
+  }
+  return validIcons;
+}
+function groupBySize(metas: IconMeta[]): Record<string, IconMeta[]> {
+  const groups: Record<string, IconMeta[]> = {};
+  for (const m of metas) {
+    const key = String(m.size);
+    groups[key] ??= [];
+    groups[key].push(m);
+  }
+  return groups;
+}
+/**
+ * Packs a set of square icons of the same size into a single texture image.
+ *
+ * The icons are laid out in a regular grid (rows × columns) where the
+ * number of columns is the ceiling of the square root of the icon count,
+ * and the number of rows is the smallest integer that can hold all icons.
+ *
+ * @param icons      Array of {@link IconMeta} objects that all share the same `size`.
+ * @param iconSize   Width (and height) of each icon in pixels.
+ * @returns An object containing:
+ *   - `textureBuffer`:  Buffer with the resulting WEBP image that contains
+ *     all icons arranged in the grid.
+ *   - `iconMap`:        Mapping from icon name to its position (`x`, `y`,
+ *     `width`, `height`) inside the texture.  `width` and `height` are equal
+ *     to `iconSize`.
+ *
+ * @remarks
+ *   * All icons are guaranteed to be square; `iconSize` is the common
+ *     dimension of every icon in `icons`.
+ *   * The texture background is fully transparent.
+ */
+async function createTextureMap(
+  icons: IconMeta[],
+  iconSize: number
+): Promise<{ textureBuffer: Buffer; iconMap: Record<string, IconPosition> }> {
+  const n = icons.length;
+  const cols = Math.ceil(Math.sqrt(n));
+  const rows = Math.ceil(n / cols);
+  const canvasW = cols * iconSize;
+  const canvasH = rows * iconSize;
+
+  const composites: sharp.OverlayOptions[] = [];
+  const map: Record<string, IconPosition> = {};
+
+  icons.forEach((icon, idx) => {
+    const x = (idx % cols) * iconSize;
+    const y = Math.floor(idx / cols) * iconSize;
+    composites.push({ input: icon.file, left: x, top: y });
+    map[icon.name] = { name: icon.name, x, y, size: iconSize };
+  });
+
+  const buffer = await sharp({
+    create: {
+      width: canvasW,
+      height: canvasH,
+      channels: 4,
+      background: { r: 0, g: 0, b: 0, alpha: 0 },
+    },
+  })
+    .composite(composites)
+    .webp({ lossless: true })
+    .toBuffer();
+
+  return { textureBuffer: buffer, iconMap: map };
+}
+/**
+ * Build a texture map from a list of PNG icons.
+ *
+ * @param icons            Array of icon file paths (relative to scriptOutputPath)
+ * @param scriptOutputPath Base directory where the icons are located
+ * @param outputDir        Destination directory for the generated files.
+ *
+ * The function will:
+ *   • Filter out non‑PNG files, missing files or empty entries.
+ *   • Ensure every PNG is square and that group icons by dimensions.
+ *   • Pack the icons into a single WebP texture (grid layout).
+ *   • Output `icon_{size}.webp` and `iconMap.json` into `outputDir`.
+ */
+export async function buildIconTextureMap(icons: string[], scriptOutputPath: string, outputDir: string): Promise<void> {
+  const validIcons = await validateIcons(icons, scriptOutputPath);
+  if (validIcons.length === 0) {
+    throw new Error("No valid PNG icons found.");
+  }
+  const textureFolder = outputDir == DEFAULT_OUTPUT_FOLDER ? "./public/data/2.0" : outputDir;
+  const iconPositions: IconPosition[] = [];
+  for (let [size, group] of Object.entries(groupBySize(validIcons))) {
+    const outTexturePath = path.join(textureFolder, `icon_${size}.webp`);
+    const { textureBuffer, iconMap } = await createTextureMap(group, parseInt(size));
+    fs.writeFileSync(outTexturePath, textureBuffer);
+    iconPositions.push(...Object.values(iconMap));
+  }
+
+  const outMapPath = path.join(outputDir, "iconMap.json");
+  fs.writeFileSync(outMapPath, JSON.stringify(iconPositions, null, 2), "utf-8");
+}

+ 96 - 96
scripts/helpers/locale.helper.ts → scripts/factorio-dump/helpers/locale.helper.ts

@@ -1,96 +1,96 @@
-/**
- * i18n-aggregator.ts
- *
- * Aggregates translation files that match a list of prefixes.
- *
- */
-
-import { promises as fs } from "fs";
-import * as path from "path";
-import { factorioPath, scriptOutputPath } from "./file.helper.ts";
-import { dumpFactorioLocale } from "./command.helper.ts";
-
-type Translations = Record<string, Record<string, unknown>>;
-export const LOCALE_OF_INTEREST = ["fluid", "item", "recipe", "item-group", "quality", "virtual-signal"];
-export const LOCALE_TO_EXPORT = ["en", "ja", "zh-CH", "de", "fr"];
-
-/**
- * Aggregates i18n JSON files that match a list of prefixes and writes a single
- * JSON file containing all translations for the specified i18n code.
- *
- * @param prefixes     Array of file prefixes to look for.  Example: ['app', 'common']
- * @param inputFolder  The folder to search for the locale files.
- * @param outputFolder The folder where the aggregated JSON will be written.
- * @param i18nCode     The i18n code that will be used as the output file name.
- *
- * The function searches `inputFolder` for files that start with one of the
- * provided prefixes and end with `-locale.json`.  Each matching file is
- * parsed as JSON and stored in a `Record<string, any>` where the key is the
- * prefix and the value is the parsed JSON object.  The final aggregated
- * object is written to `outputFolder/i18n/<i18nCode>.json`.
- */
-export async function aggregateI18n(
-  prefixes: string[],
-  inputFolder: string,
-  outputFolder: string,
-  i18nCode: string
-): Promise<void> {
-  // Resolve absolute paths
-  const inputPath = path.resolve(inputFolder);
-  const outputPath = path.resolve(outputFolder, "i18n");
-
-  // Ensure the output directory exists
-  await fs.mkdir(outputPath, { recursive: true });
-
-  // Read all files in the input directory
-  const allFiles = await fs.readdir(inputPath);
-
-  // Map of prefix => filename
-  const prefixToFile: Record<string, string> = {};
-
-  // Find the first file that matches each prefix
-  for (const prefix of prefixes) {
-    const filename = prefix + "-locale.json";
-    const match = allFiles.find((f) => f == filename);
-    if (match) {
-      prefixToFile[prefix] = match;
-    }
-  }
-
-  const result: Translations = {};
-
-  // Read and parse each matched file
-  for (const [prefix, fileName] of Object.entries(prefixToFile)) {
-    const filePath = path.join(inputPath, fileName);
-    const data = await fs.readFile(filePath, "utf-8");
-    try {
-      result[prefix] = JSON.parse(data);
-    } catch {
-      result[prefix] = {};
-    }
-  }
-
-  // Write the aggregated JSON to the output folder
-  const outputFile = path.join(outputPath, `${i18nCode}.json`);
-  await fs.writeFile(outputFile, JSON.stringify(result, null, 2), "utf-8");
-}
-/**
- * Updates the first `locale=` line in a file to the supplied locale code.
- *
- * @param localCode  New locale code (e.g. "en-US", "de-DE")
- *
- */
-export async function setLocaleInIni(localCode: string): Promise<void> {
-  const resolvedPath = path.resolve(factorioPath, "config", "config.ini");
-  const content = await fs.readFile(resolvedPath, "utf8");
-  await fs.writeFile(resolvedPath, content.replace(/^locale=.*$/m, "locale=" + localCode), "utf8");
-}
-
-export async function extractLocale(outputFolder: string) {
-  for (let i18nCode of LOCALE_TO_EXPORT) {
-    console.log("Export i18n for " + i18nCode);
-    await setLocaleInIni(i18nCode);
-    await dumpFactorioLocale();
-    await aggregateI18n(LOCALE_OF_INTEREST, scriptOutputPath, outputFolder, i18nCode);
-  }
-}
+/**
+ * i18n-aggregator.ts
+ *
+ * Aggregates translation files that match a list of prefixes.
+ *
+ */
+
+import { promises as fs } from "fs";
+import * as path from "path";
+import { factorioPath, scriptOutputPath } from "./file.helper.ts";
+import { dumpFactorioLocale } from "./command.helper.ts";
+
+type Translations = Record<string, Record<string, unknown>>;
+export const LOCALE_OF_INTEREST = ["fluid", "item", "recipe", "item-group", "quality", "virtual-signal"];
+export const LOCALE_TO_EXPORT = ["en", "ja", "zh-CH", "de", "fr"];
+
+/**
+ * Aggregates i18n JSON files that match a list of prefixes and writes a single
+ * JSON file containing all translations for the specified i18n code.
+ *
+ * @param prefixes     Array of file prefixes to look for.  Example: ['app', 'common']
+ * @param inputFolder  The folder to search for the locale files.
+ * @param outputFolder The folder where the aggregated JSON will be written.
+ * @param i18nCode     The i18n code that will be used as the output file name.
+ *
+ * The function searches `inputFolder` for files that start with one of the
+ * provided prefixes and end with `-locale.json`.  Each matching file is
+ * parsed as JSON and stored in a `Record<string, any>` where the key is the
+ * prefix and the value is the parsed JSON object.  The final aggregated
+ * object is written to `outputFolder/i18n/<i18nCode>.json`.
+ */
+export async function aggregateI18n(
+  prefixes: string[],
+  inputFolder: string,
+  outputFolder: string,
+  i18nCode: string
+): Promise<void> {
+  // Resolve absolute paths
+  const inputPath = path.resolve(inputFolder);
+  const outputPath = path.resolve(outputFolder, "i18n");
+
+  // Ensure the output directory exists
+  await fs.mkdir(outputPath, { recursive: true });
+
+  // Read all files in the input directory
+  const allFiles = await fs.readdir(inputPath);
+
+  // Map of prefix => filename
+  const prefixToFile: Record<string, string> = {};
+
+  // Find the first file that matches each prefix
+  for (const prefix of prefixes) {
+    const filename = prefix + "-locale.json";
+    const match = allFiles.find((f) => f == filename);
+    if (match) {
+      prefixToFile[prefix] = match;
+    }
+  }
+
+  const result: Translations = {};
+
+  // Read and parse each matched file
+  for (const [prefix, fileName] of Object.entries(prefixToFile)) {
+    const filePath = path.join(inputPath, fileName);
+    const data = await fs.readFile(filePath, "utf-8");
+    try {
+      result[prefix] = JSON.parse(data);
+    } catch {
+      result[prefix] = {};
+    }
+  }
+
+  // Write the aggregated JSON to the output folder
+  const outputFile = path.join(outputPath, `${i18nCode}.json`);
+  await fs.writeFile(outputFile, JSON.stringify(result, null, 2), "utf-8");
+}
+/**
+ * Updates the first `locale=` line in a file to the supplied locale code.
+ *
+ * @param localCode  New locale code (e.g. "en-US", "de-DE")
+ *
+ */
+export async function setLocaleInIni(localCode: string): Promise<void> {
+  const resolvedPath = path.resolve(factorioPath, "config", "config.ini");
+  const content = await fs.readFile(resolvedPath, "utf8");
+  await fs.writeFile(resolvedPath, content.replace(/^locale=.*$/m, "locale=" + localCode), "utf8");
+}
+
+export async function extractLocale(outputFolder: string) {
+  for (let i18nCode of LOCALE_TO_EXPORT) {
+    console.log("Export i18n for " + i18nCode);
+    await setLocaleInIni(i18nCode);
+    await dumpFactorioLocale();
+    await aggregateI18n(LOCALE_OF_INTEREST, scriptOutputPath, outputFolder, i18nCode);
+  }
+}

+ 70 - 70
scripts/helpers/recipes.helper.ts → scripts/factorio-dump/helpers/recipes.helper.ts

@@ -1,70 +1,70 @@
-import type { FluidProductPrototype, ItemProductPrototype, RecipePrototype } from "../factorio-dump.models.ts";
-import type { Item } from "../factorio-process-data.models.ts";
-
-export type Ingredient = {
-  type: "fluid" | "item";
-  itemId: string;
-  amount: number;
-};
-
-export type Recipe = {
-  name: string;
-  icon?: string;
-  subgroup: string;
-  order?: string;
-  category?: string;
-  additional_categories?: string[];
-  allow_productivity?: boolean;
-  allow_quality?: boolean;
-  allow_speed?: boolean;
-  ingredients?: Ingredient[];
-  results?: Array<ItemProductPrototype | FluidProductPrototype>;
-};
-
-export function getRecipeIcon(recipe: RecipePrototype, mainProduct: Item | undefined): string {
-  if (recipe.icons || recipe.icon) return `recipe/${recipe.name}.png`;
-  if (mainProduct && mainProduct.icon) return mainProduct.icon;
-  console.warn(`No icon found for :${recipe.name}`);
-  return "";
-}
-export function getRecipeSubGroup(recipe: RecipePrototype, mainProduct: Item | undefined): string {
-  if (recipe.subgroup) return recipe.subgroup;
-  if (mainProduct) return mainProduct.subgroup;
-
-  return "other";
-}
-export function getMainProductName(recipe: RecipePrototype): string {
-  if (recipe.results?.length == 1) {
-    return recipe.results[0].name;
-  } else if (recipe.results && recipe.main_product) {
-    const mainProduct = recipe.main_product;
-    const result = recipe.results.find((r) => r.name === mainProduct);
-    if (result) {
-      return result.name;
-    }
-    throw new Error(`Main product '${mainProduct}' declared by recipe '${recipe.name}' not found in results`);
-  }
-  return "";
-}
-
-export function parseRecipe(recipe: RecipePrototype, itemsMap: Record<string, Item>): Recipe {
-  const rawIngredient = Array.isArray(recipe.ingredients) ? recipe.ingredients : [];
-  const ingredients = rawIngredient.map((i) => ({ itemId: i.name, amount: i.amount, type: i.type }));
-  const mainProduct = itemsMap[getMainProductName(recipe)];
-  const r: Recipe = {
-    name: recipe.name,
-    icon: getRecipeIcon(recipe, mainProduct),
-    subgroup: getRecipeSubGroup(recipe, mainProduct),
-    order: recipe.order,
-    category: recipe.category,
-    additional_categories: recipe.additional_categories,
-    ingredients,
-    results: recipe.results,
-  };
-  if (recipe.category) r.category = recipe.category;
-  if (recipe.additional_categories) r.additional_categories = recipe.additional_categories;
-  if (recipe.allow_productivity) r.allow_productivity = true;
-  if (recipe.allow_quality) r.allow_quality = true;
-  if (recipe.allow_speed) r.allow_speed = true;
-  return r;
-}
+import type { FluidProductPrototype, ItemProductPrototype, RecipePrototype } from "../lua-api/models.ts";
+import type { Item } from "../process-data.models.ts";
+
+export type Ingredient = {
+  type: "fluid" | "item";
+  itemId: string;
+  amount: number;
+};
+
+export type Recipe = {
+  name: string;
+  icon?: string;
+  subgroup: string;
+  order?: string;
+  category?: string;
+  additional_categories?: string[];
+  allow_productivity?: boolean;
+  allow_quality?: boolean;
+  allow_speed?: boolean;
+  ingredients?: Ingredient[];
+  results?: Array<ItemProductPrototype | FluidProductPrototype>;
+};
+
+export function getRecipeIcon(recipe: RecipePrototype, mainProduct: Item | undefined): string {
+  if (recipe.icons || recipe.icon) return `recipe/${recipe.name}.png`;
+  if (mainProduct && mainProduct.icon) return mainProduct.icon;
+  console.warn(`No icon found for :${recipe.name}`);
+  return "";
+}
+export function getRecipeSubGroup(recipe: RecipePrototype, mainProduct: Item | undefined): string {
+  if (recipe.subgroup) return recipe.subgroup;
+  if (mainProduct) return mainProduct.subgroup;
+
+  return "other";
+}
+export function getMainProductName(recipe: RecipePrototype): string {
+  if (recipe.results?.length == 1) {
+    return recipe.results[0].name;
+  } else if (recipe.results && recipe.main_product) {
+    const mainProduct = recipe.main_product;
+    const result = recipe.results.find((r) => r.name === mainProduct);
+    if (result) {
+      return result.name;
+    }
+    throw new Error(`Main product '${mainProduct}' declared by recipe '${recipe.name}' not found in results`);
+  }
+  return "";
+}
+
+export function parseRecipe(recipe: RecipePrototype, itemsMap: Record<string, Item>): Recipe {
+  const rawIngredient = Array.isArray(recipe.ingredients) ? recipe.ingredients : [];
+  const ingredients = rawIngredient.map((i) => ({ itemId: i.name, amount: i.amount, type: i.type }));
+  const mainProduct = itemsMap[getMainProductName(recipe)];
+  const r: Recipe = {
+    name: recipe.name,
+    icon: getRecipeIcon(recipe, mainProduct),
+    subgroup: getRecipeSubGroup(recipe, mainProduct),
+    order: recipe.order ?? mainProduct.order,
+    category: recipe.category ?? "crafting",
+    additional_categories: recipe.additional_categories,
+    ingredients,
+    results: recipe.results,
+  };
+  if (recipe.category) r.category = recipe.category;
+  if (recipe.additional_categories) r.additional_categories = recipe.additional_categories;
+  if (recipe.allow_productivity) r.allow_productivity = true;
+  if (recipe.allow_quality) r.allow_quality = true;
+  if (recipe.allow_speed) r.allow_speed = true;
+  return r;
+}

+ 46 - 0
scripts/factorio-dump/index.ts

@@ -0,0 +1,46 @@
+import { Command } from "@commander-js/extra-typings";
+import chalk from "chalk";
+
+import { processData } from "./process-data.ts";
+import { dumpFactorioData, dumpFactorioIcon } from "./helpers/command.helper.ts";
+import { extractLocale } from "./helpers/locale.helper.ts";
+import { step } from "../helpers/logger.ts";
+
+export const DEFAULT_OUTPUT_FOLDER = "./src/assets/data/2.0";
+const program = new Command()
+  .name("factorio-dump")
+  .option("--skip-dump", "Skip the dumping of icon and factorio data", false)
+  .option("--skip-locale", "Skip the dumping of locale datas", false)
+  .option("-o, --output <path>", "Path to put the extracted data", DEFAULT_OUTPUT_FOLDER)
+  .description("Builds Factorio data assets for the web app from local factorio installation")
+  .helpOption("-h, --help", "Display help")
+  .addHelpText(
+    "after",
+    `
+Examples
+  $ factorio-dump
+  $ factorio-dump --skip-dump
+  $ factorio-dump --skip-local
+`
+  );
+
+async function run() {
+  program.parse(process.argv);
+  const options = program.opts();
+  console.log(chalk.bold("\n🚀 Factorio Data Pipeline\n"));
+
+  if (!options.skipDump) {
+    await step("Dump factorio data", dumpFactorioData);
+    await step("Dump factorio icons", dumpFactorioIcon);
+  }
+  await processData(options.output);
+  //await step("Process factorio data and icons", processData, options.output);
+  if (!options.skipLocale) await step("Dump factorio locale", extractLocale, options.output);
+
+  console.log(chalk.green("\n✅ All steps completed successfully!"));
+}
+
+run().catch((err) => {
+  console.error(chalk.red("\n❌ Something went wrong:"), err);
+  process.exit(1);
+});

+ 178 - 0
scripts/factorio-dump/lua-api/const.ts

@@ -0,0 +1,178 @@
+/** Generated file, do not edit. See scripts/factorio-dump/lua-api/index.ts for generator. */
+import type { FactorioRawData } from "./models.ts"
+type ValidKeys = Array<keyof FactorioRawData> 
+
+/**
+ * All the keys that could contain item prototypes.
+ */
+export const ITEM_KEYS: ValidKeys = [
+  "ammo",
+  "armor",
+  "blueprint",
+  "blueprint-book",
+  "capsule",
+  "copy-paste-tool",
+  "deconstruction-item",
+  "fluid",
+  "gun",
+  "item",
+  "item-with-entity-data",
+  "item-with-inventory",
+  "item-with-label",
+  "item-with-tags",
+  "module",
+  "rail-planner",
+  "repair-tool",
+  "selection-tool",
+  "space-platform-starter-pack",
+  "spidertron-remote",
+  "tool",
+  "upgrade-item"
+]
+
+/**
+ * All the keys that could contain entity prototypes.
+ */
+export const ENTITY_KEYS: ValidKeys = [
+  "accumulator",
+  "agricultural-tower",
+  "ammo-turret",
+  "arithmetic-combinator",
+  "arrow",
+  "artillery-flare",
+  "artillery-projectile",
+  "artillery-turret",
+  "artillery-wagon",
+  "assembling-machine",
+  "asteroid",
+  "asteroid-collector",
+  "beacon",
+  "beam",
+  "boiler",
+  "burner-generator",
+  "capture-robot",
+  "car",
+  "cargo-bay",
+  "cargo-landing-pad",
+  "cargo-pod",
+  "cargo-wagon",
+  "character",
+  "character-corpse",
+  "cliff",
+  "combat-robot",
+  "constant-combinator",
+  "construction-robot",
+  "container",
+  "corpse",
+  "curved-rail-a",
+  "curved-rail-b",
+  "decider-combinator",
+  "deconstructible-tile-proxy",
+  "display-panel",
+  "electric-energy-interface",
+  "electric-pole",
+  "electric-turret",
+  "elevated-curved-rail-a",
+  "elevated-curved-rail-b",
+  "elevated-half-diagonal-rail",
+  "elevated-straight-rail",
+  "entity-ghost",
+  "explosion",
+  "fire",
+  "fish",
+  "fluid-turret",
+  "fluid-wagon",
+  "furnace",
+  "fusion-generator",
+  "fusion-reactor",
+  "gate",
+  "generator",
+  "half-diagonal-rail",
+  "heat-interface",
+  "heat-pipe",
+  "highlight-box",
+  "infinity-cargo-wagon",
+  "infinity-container",
+  "infinity-pipe",
+  "inserter",
+  "item-entity",
+  "item-request-proxy",
+  "lab",
+  "lamp",
+  "land-mine",
+  "lane-splitter",
+  "legacy-curved-rail",
+  "legacy-straight-rail",
+  "lightning",
+  "lightning-attractor",
+  "linked-belt",
+  "linked-container",
+  "loader",
+  "loader-1x1",
+  "locomotive",
+  "logistic-container",
+  "logistic-robot",
+  "market",
+  "mining-drill",
+  "offshore-pump",
+  "particle-source",
+  "pipe",
+  "pipe-to-ground",
+  "plant",
+  "player-port",
+  "power-switch",
+  "programmable-speaker",
+  "projectile",
+  "proxy-container",
+  "pump",
+  "radar",
+  "rail-chain-signal",
+  "rail-ramp",
+  "rail-remnants",
+  "rail-signal",
+  "rail-support",
+  "reactor",
+  "resource",
+  "roboport",
+  "rocket-silo",
+  "rocket-silo-rocket",
+  "rocket-silo-rocket-shadow",
+  "segment",
+  "segmented-unit",
+  "selector-combinator",
+  "simple-entity",
+  "simple-entity-with-force",
+  "simple-entity-with-owner",
+  "smoke-with-trigger",
+  "solar-panel",
+  "space-platform-hub",
+  "speech-bubble",
+  "spider-leg",
+  "spider-unit",
+  "spider-vehicle",
+  "splitter",
+  "sticker",
+  "storage-tank",
+  "straight-rail",
+  "stream",
+  "temporary-container",
+  "thruster",
+  "tile-ghost",
+  "train-stop",
+  "transport-belt",
+  "tree",
+  "turret",
+  "underground-belt",
+  "unit",
+  "unit-spawner",
+  "valve",
+  "wall"
+]
+
+/**
+ * All the keys that could contains space location prototypes.
+ */
+export const SPACE_LOCATION_KEYS: ValidKeys = [
+  "planet",
+  "space-location"
+]

+ 32 - 0
scripts/factorio-dump/lua-api/index.ts

@@ -0,0 +1,32 @@
+import type * as M from "./lua-api.models.js";
+import { generateDataModel } from "./parse-factorio-model.ts";
+import { extractEntityAndItemTypeName } from "./prototype-tree.ts";
+
+const API_PATH = "https://lua-api.factorio.com/latest/prototype-api.json";
+const OUTPUT_PATH = "scripts/factorio-dump/lua-api";
+
+/**
+ * Fetch the raw prototype‑API from Factorio.
+ * Uses the global `fetch` which is available in Node 18+.
+ *
+ * @returns the parsed {@link M.PrototypeApi} object.
+ */
+export async function getPrototypeApi(): Promise<M.PrototypeApi> {
+  const response = await fetch(API_PATH, { method: "GET" });
+
+  if (!response.ok) {
+    throw new Error(`Failed to fetch API: ${response.status} ${response.statusText}`);
+  }
+
+  return (await response.json()) as M.PrototypeApi;
+}
+
+const run = async function (): Promise<void> {
+  console.log(`Regenerating Factorio prototype models from ${API_PATH}...`);
+
+  const api = await getPrototypeApi();
+  await generateDataModel(OUTPUT_PATH, api);
+  await extractEntityAndItemTypeName(OUTPUT_PATH, api);
+};
+
+void run();

+ 118 - 118
scripts/factorio-api.models.d.ts → scripts/factorio-dump/lua-api/lua-api.models.d.ts

@@ -1,118 +1,118 @@
-export interface PrototypeApi {
-  application: string;
-  application_version: string;
-  api_version: number;
-  stage: string;
-  prototypes: Prototype[];
-  types: Concept[];
-}
-
-export interface Prototype {
-  name: string;
-  order: number;
-  description: string;
-  lists?: string[];
-  examples?: string[];
-  images?: Image[];
-  parent?: string;
-  abstract: boolean;
-  typename?: string;
-  instance_limit?: number;
-  deprecated: boolean;
-  properties: Property[];
-  custom_properties: CustomProperty[];
-}
-
-export interface Concept {
-  name: string;
-  order: number;
-  description: string;
-  lists?: string[];
-  examples?: string[];
-  images?: Image[];
-  parent?: string;
-  abstract: boolean;
-  inline: false;
-  type: DataType;
-  properties?: Property[];
-}
-
-export interface Property {
-  name: string;
-  order: number;
-  description: string;
-  lists?: string[];
-  examples?: string[];
-  images?: Image[];
-  alt_name?: string;
-  override: boolean;
-  type: DataType;
-  optional: boolean;
-  default?: [string, LiteralDataType];
-}
-
-export interface ComplexDataType {
-  complex_type: string;
-}
-
-export interface ArrayDataType extends ComplexDataType {
-  complex_type: "array";
-  value: DataType;
-}
-
-export interface DictionaryDataType extends ComplexDataType {
-  complex_type: "dictionary";
-  key: DataType;
-  value: DataType;
-}
-
-export interface TupleDataType extends ComplexDataType {
-  complex_type: "tuple";
-  values: DataType[];
-}
-
-export interface UnionDataType extends ComplexDataType {
-  complex_type: "union";
-  options: DataType[];
-  full_format: boolean;
-}
-
-export interface LiteralDataType extends ComplexDataType {
-  complex_type: "literal";
-  value: string | number | boolean;
-  description?: string;
-}
-
-export interface TypeDataType extends ComplexDataType {
-  complex_type: "type";
-  value: DataType;
-  description: string;
-}
-
-export interface StructDataType extends ComplexDataType {
-  complex_type: "struct";
-}
-
-export type DataType =
-  | string
-  | ArrayDataType
-  | DictionaryDataType
-  | TupleDataType
-  | UnionDataType
-  | LiteralDataType
-  | TypeDataType
-  | StructDataType;
-
-export interface Image {
-  filename: string;
-  caption?: string;
-}
-
-export interface CustomProperty {
-  description: string;
-  lists?: string[];
-  examples?: string[];
-  images?: Image[];
-  key_type: DataType;
-  value_type: DataType;
-}
+export interface PrototypeApi {
+  application: string;
+  application_version: string;
+  api_version: number;
+  stage: string;
+  prototypes: Prototype[];
+  types: Concept[];
+}
+
+export interface Prototype {
+  name: string;
+  order: number;
+  description: string;
+  lists?: string[];
+  examples?: string[];
+  images?: Image[];
+  parent?: string;
+  abstract: boolean;
+  typename?: string;
+  instance_limit?: number;
+  deprecated: boolean;
+  properties: Property[];
+  custom_properties: CustomProperty[];
+}
+
+export interface Concept {
+  name: string;
+  order: number;
+  description: string;
+  lists?: string[];
+  examples?: string[];
+  images?: Image[];
+  parent?: string;
+  abstract: boolean;
+  inline: false;
+  type: DataType;
+  properties?: Property[];
+}
+
+export interface Property {
+  name: string;
+  order: number;
+  description: string;
+  lists?: string[];
+  examples?: string[];
+  images?: Image[];
+  alt_name?: string;
+  override: boolean;
+  type: DataType;
+  optional: boolean;
+  default?: [string, LiteralDataType];
+}
+
+export interface ComplexDataType {
+  complex_type: string;
+}
+
+export interface ArrayDataType extends ComplexDataType {
+  complex_type: "array";
+  value: DataType;
+}
+
+export interface DictionaryDataType extends ComplexDataType {
+  complex_type: "dictionary";
+  key: DataType;
+  value: DataType;
+}
+
+export interface TupleDataType extends ComplexDataType {
+  complex_type: "tuple";
+  values: DataType[];
+}
+
+export interface UnionDataType extends ComplexDataType {
+  complex_type: "union";
+  options: DataType[];
+  full_format: boolean;
+}
+
+export interface LiteralDataType extends ComplexDataType {
+  complex_type: "literal";
+  value: string | number | boolean;
+  description?: string;
+}
+
+export interface TypeDataType extends ComplexDataType {
+  complex_type: "type";
+  value: DataType;
+  description: string;
+}
+
+export interface StructDataType extends ComplexDataType {
+  complex_type: "struct";
+}
+
+export type DataType =
+  | string
+  | ArrayDataType
+  | DictionaryDataType
+  | TupleDataType
+  | UnionDataType
+  | LiteralDataType
+  | TypeDataType
+  | StructDataType;
+
+export interface Image {
+  filename: string;
+  caption?: string;
+}
+
+export interface CustomProperty {
+  description: string;
+  lists?: string[];
+  examples?: string[];
+  images?: Image[];
+  key_type: DataType;
+  value_type: DataType;
+}

+ 246 - 1
scripts/factorio-dump.models.ts → scripts/factorio-dump/lua-api/models.ts

@@ -1,4 +1,4 @@
-/** Generated file, do not edit. See scripts/factorio-api.ts for generator. */
+/** Generated file, do not edit. See scripts/factorio-dump/lua-api/index.ts for generator. */
 
 /**
  * Application: factorio
@@ -19517,3 +19517,248 @@ export type Weight = number;
 export type WorkingSound = _WorkingSound | Sound;
 
 export type WorldAmbientSoundDefinition = _WorldAmbientSoundDefinition | Sound;
+
+type SR<T> = Record<string, T>;
+export type FactorioRawData = {
+  accumulator?: SR<AccumulatorPrototype>;
+  achievement?: SR<AchievementPrototype>;
+  "active-defense-equipment"?: SR<ActiveDefenseEquipmentPrototype>;
+  "agricultural-tower"?: SR<AgriculturalTowerPrototype>;
+  "airborne-pollutant"?: SR<AirbornePollutantPrototype>;
+  "ammo-category"?: SR<AmmoCategory>;
+  ammo?: SR<AmmoItemPrototype>;
+  "ammo-turret"?: SR<AmmoTurretPrototype>;
+  "arithmetic-combinator"?: SR<ArithmeticCombinatorPrototype>;
+  armor?: SR<ArmorPrototype>;
+  arrow?: SR<ArrowPrototype>;
+  "artillery-flare"?: SR<ArtilleryFlarePrototype>;
+  "artillery-projectile"?: SR<ArtilleryProjectilePrototype>;
+  "artillery-turret"?: SR<ArtilleryTurretPrototype>;
+  "artillery-wagon"?: SR<ArtilleryWagonPrototype>;
+  "assembling-machine"?: SR<AssemblingMachinePrototype>;
+  "asteroid-chunk"?: SR<AsteroidChunkPrototype>;
+  "asteroid-collector"?: SR<AsteroidCollectorPrototype>;
+  asteroid?: SR<AsteroidPrototype>;
+  "autoplace-control"?: SR<AutoplaceControl>;
+  "battery-equipment"?: SR<BatteryEquipmentPrototype>;
+  beacon?: SR<BeaconPrototype>;
+  beam?: SR<BeamPrototype>;
+  "belt-immunity-equipment"?: SR<BeltImmunityEquipmentPrototype>;
+  "blueprint-book"?: SR<BlueprintBookPrototype>;
+  blueprint?: SR<BlueprintItemPrototype>;
+  boiler?: SR<BoilerPrototype>;
+  "build-entity-achievement"?: SR<BuildEntityAchievementPrototype>;
+  "burner-generator"?: SR<BurnerGeneratorPrototype>;
+  "burner-usage"?: SR<BurnerUsagePrototype>;
+  capsule?: SR<CapsulePrototype>;
+  "capture-robot"?: SR<CaptureRobotPrototype>;
+  car?: SR<CarPrototype>;
+  "cargo-bay"?: SR<CargoBayPrototype>;
+  "cargo-landing-pad"?: SR<CargoLandingPadPrototype>;
+  "cargo-pod"?: SR<CargoPodPrototype>;
+  "cargo-wagon"?: SR<CargoWagonPrototype>;
+  "chain-active-trigger"?: SR<ChainActiveTriggerPrototype>;
+  "change-surface-achievement"?: SR<ChangedSurfaceAchievementPrototype>;
+  "character-corpse"?: SR<CharacterCorpsePrototype>;
+  character?: SR<CharacterPrototype>;
+  cliff?: SR<CliffPrototype>;
+  "collision-layer"?: SR<CollisionLayerPrototype>;
+  "combat-robot-count-achievement"?: SR<CombatRobotCountAchievementPrototype>;
+  "combat-robot"?: SR<CombatRobotPrototype>;
+  "complete-objective-achievement"?: SR<CompleteObjectiveAchievementPrototype>;
+  "constant-combinator"?: SR<ConstantCombinatorPrototype>;
+  "construct-with-robots-achievement"?: SR<ConstructWithRobotsAchievementPrototype>;
+  "construction-robot"?: SR<ConstructionRobotPrototype>;
+  container?: SR<ContainerPrototype>;
+  "copy-paste-tool"?: SR<CopyPasteToolPrototype>;
+  corpse?: SR<CorpsePrototype>;
+  "create-platform-achievement"?: SR<CreatePlatformAchievementPrototype>;
+  "curved-rail-a"?: SR<CurvedRailAPrototype>;
+  "curved-rail-b"?: SR<CurvedRailBPrototype>;
+  "custom-event"?: SR<CustomEventPrototype>;
+  "custom-input"?: SR<CustomInputPrototype>;
+  "damage-type"?: SR<DamageType>;
+  "decider-combinator"?: SR<DeciderCombinatorPrototype>;
+  "deconstruct-with-robots-achievement"?: SR<DeconstructWithRobotsAchievementPrototype>;
+  "deconstructible-tile-proxy"?: SR<DeconstructibleTileProxyPrototype>;
+  "deconstruction-item"?: SR<DeconstructionItemPrototype>;
+  "optimized-decorative"?: SR<DecorativePrototype>;
+  "delayed-active-trigger"?: SR<DelayedActiveTriggerPrototype>;
+  "deliver-by-robots-achievement"?: SR<DeliverByRobotsAchievementPrototype>;
+  "deplete-resource-achievement"?: SR<DepleteResourceAchievementPrototype>;
+  "destroy-cliff-achievement"?: SR<DestroyCliffAchievementPrototype>;
+  "display-panel"?: SR<DisplayPanelPrototype>;
+  "dont-build-entity-achievement"?: SR<DontBuildEntityAchievementPrototype>;
+  "dont-craft-manually-achievement"?: SR<DontCraftManuallyAchievementPrototype>;
+  "dont-kill-manually-achievement"?: SR<DontKillManuallyAchievementPrototype>;
+  "dont-research-before-researching-achievement"?: SR<DontResearchBeforeResearchingAchievementPrototype>;
+  "dont-use-entity-in-energy-production-achievement"?: SR<DontUseEntityInEnergyProductionAchievementPrototype>;
+  "electric-energy-interface"?: SR<ElectricEnergyInterfacePrototype>;
+  "electric-pole"?: SR<ElectricPolePrototype>;
+  "electric-turret"?: SR<ElectricTurretPrototype>;
+  "elevated-curved-rail-a"?: SR<ElevatedCurvedRailAPrototype>;
+  "elevated-curved-rail-b"?: SR<ElevatedCurvedRailBPrototype>;
+  "elevated-half-diagonal-rail"?: SR<ElevatedHalfDiagonalRailPrototype>;
+  "elevated-straight-rail"?: SR<ElevatedStraightRailPrototype>;
+  "unit-spawner"?: SR<EnemySpawnerPrototype>;
+  "energy-shield-equipment"?: SR<EnergyShieldEquipmentPrototype>;
+  "entity-ghost"?: SR<EntityGhostPrototype>;
+  "equip-armor-achievement"?: SR<EquipArmorAchievementPrototype>;
+  "equipment-category"?: SR<EquipmentCategory>;
+  "equipment-ghost"?: SR<EquipmentGhostPrototype>;
+  "equipment-grid"?: SR<EquipmentGridPrototype>;
+  explosion?: SR<ExplosionPrototype>;
+  fire?: SR<FireFlamePrototype>;
+  fish?: SR<FishPrototype>;
+  fluid?: SR<FluidPrototype>;
+  stream?: SR<FluidStreamPrototype>;
+  "fluid-turret"?: SR<FluidTurretPrototype>;
+  "fluid-wagon"?: SR<FluidWagonPrototype>;
+  "fuel-category"?: SR<FuelCategory>;
+  furnace?: SR<FurnacePrototype>;
+  "fusion-generator"?: SR<FusionGeneratorPrototype>;
+  "fusion-reactor"?: SR<FusionReactorPrototype>;
+  gate?: SR<GatePrototype>;
+  "generator-equipment"?: SR<GeneratorEquipmentPrototype>;
+  generator?: SR<GeneratorPrototype>;
+  "group-attack-achievement"?: SR<GroupAttackAchievementPrototype>;
+  "gui-style"?: SR<GuiStyle>;
+  gun?: SR<GunPrototype>;
+  "half-diagonal-rail"?: SR<HalfDiagonalRailPrototype>;
+  "heat-interface"?: SR<HeatInterfacePrototype>;
+  "heat-pipe"?: SR<HeatPipePrototype>;
+  "highlight-box"?: SR<HighlightBoxEntityPrototype>;
+  "infinity-cargo-wagon"?: SR<InfinityCargoWagonPrototype>;
+  "infinity-container"?: SR<InfinityContainerPrototype>;
+  "infinity-pipe"?: SR<InfinityPipePrototype>;
+  inserter?: SR<InserterPrototype>;
+  "inventory-bonus-equipment"?: SR<InventoryBonusEquipmentPrototype>;
+  "item-entity"?: SR<ItemEntityPrototype>;
+  "item-group"?: SR<ItemGroup>;
+  item?: SR<ItemPrototype>;
+  "item-request-proxy"?: SR<ItemRequestProxyPrototype>;
+  "item-subgroup"?: SR<ItemSubGroup>;
+  "item-with-entity-data"?: SR<ItemWithEntityDataPrototype>;
+  "item-with-inventory"?: SR<ItemWithInventoryPrototype>;
+  "item-with-label"?: SR<ItemWithLabelPrototype>;
+  "item-with-tags"?: SR<ItemWithTagsPrototype>;
+  "kill-achievement"?: SR<KillAchievementPrototype>;
+  lab?: SR<LabPrototype>;
+  lamp?: SR<LampPrototype>;
+  "land-mine"?: SR<LandMinePrototype>;
+  "lane-splitter"?: SR<LaneSplitterPrototype>;
+  "legacy-curved-rail"?: SR<LegacyCurvedRailPrototype>;
+  "legacy-straight-rail"?: SR<LegacyStraightRailPrototype>;
+  "lightning-attractor"?: SR<LightningAttractorPrototype>;
+  lightning?: SR<LightningPrototype>;
+  "linked-belt"?: SR<LinkedBeltPrototype>;
+  "linked-container"?: SR<LinkedContainerPrototype>;
+  "loader-1x1"?: SR<Loader1x1Prototype>;
+  loader?: SR<Loader1x2Prototype>;
+  locomotive?: SR<LocomotivePrototype>;
+  "logistic-container"?: SR<LogisticContainerPrototype>;
+  "logistic-robot"?: SR<LogisticRobotPrototype>;
+  market?: SR<MarketPrototype>;
+  "mining-drill"?: SR<MiningDrillPrototype>;
+  "mod-data"?: SR<ModData>;
+  "module-category"?: SR<ModuleCategory>;
+  module?: SR<ModulePrototype>;
+  "module-transfer-achievement"?: SR<ModuleTransferAchievementPrototype>;
+  "movement-bonus-equipment"?: SR<MovementBonusEquipmentPrototype>;
+  "noise-expression"?: SR<NamedNoiseExpression>;
+  "noise-function"?: SR<NamedNoiseFunction>;
+  "night-vision-equipment"?: SR<NightVisionEquipmentPrototype>;
+  "offshore-pump"?: SR<OffshorePumpPrototype>;
+  "optimized-particle"?: SR<ParticlePrototype>;
+  "particle-source"?: SR<ParticleSourcePrototype>;
+  pipe?: SR<PipePrototype>;
+  "pipe-to-ground"?: SR<PipeToGroundPrototype>;
+  "place-equipment-achievement"?: SR<PlaceEquipmentAchievementPrototype>;
+  planet?: SR<PlanetPrototype>;
+  plant?: SR<PlantPrototype>;
+  "player-damaged-achievement"?: SR<PlayerDamagedAchievementPrototype>;
+  "player-port"?: SR<PlayerPortPrototype>;
+  "power-switch"?: SR<PowerSwitchPrototype>;
+  "procession-layer-inheritance-group"?: SR<ProcessionLayerInheritanceGroup>;
+  procession?: SR<ProcessionPrototype>;
+  "produce-achievement"?: SR<ProduceAchievementPrototype>;
+  "produce-per-hour-achievement"?: SR<ProducePerHourAchievementPrototype>;
+  "programmable-speaker"?: SR<ProgrammableSpeakerPrototype>;
+  projectile?: SR<ProjectilePrototype>;
+  "proxy-container"?: SR<ProxyContainerPrototype>;
+  pump?: SR<PumpPrototype>;
+  quality?: SR<QualityPrototype>;
+  radar?: SR<RadarPrototype>;
+  "rail-chain-signal"?: SR<RailChainSignalPrototype>;
+  "rail-planner"?: SR<RailPlannerPrototype>;
+  "rail-ramp"?: SR<RailRampPrototype>;
+  "rail-remnants"?: SR<RailRemnantsPrototype>;
+  "rail-signal"?: SR<RailSignalPrototype>;
+  "rail-support"?: SR<RailSupportPrototype>;
+  reactor?: SR<ReactorPrototype>;
+  "recipe-category"?: SR<RecipeCategory>;
+  recipe?: SR<RecipePrototype>;
+  "repair-tool"?: SR<RepairToolPrototype>;
+  "research-achievement"?: SR<ResearchAchievementPrototype>;
+  "research-with-science-pack-achievement"?: SR<ResearchWithSciencePackAchievementPrototype>;
+  "resource-category"?: SR<ResourceCategory>;
+  resource?: SR<ResourceEntityPrototype>;
+  "roboport-equipment"?: SR<RoboportEquipmentPrototype>;
+  roboport?: SR<RoboportPrototype>;
+  "rocket-silo"?: SR<RocketSiloPrototype>;
+  "rocket-silo-rocket"?: SR<RocketSiloRocketPrototype>;
+  "rocket-silo-rocket-shadow"?: SR<RocketSiloRocketShadowPrototype>;
+  segment?: SR<SegmentPrototype>;
+  "segmented-unit"?: SR<SegmentedUnitPrototype>;
+  "selection-tool"?: SR<SelectionToolPrototype>;
+  "selector-combinator"?: SR<SelectorCombinatorPrototype>;
+  "shoot-achievement"?: SR<ShootAchievementPrototype>;
+  shortcut?: SR<ShortcutPrototype>;
+  "simple-entity"?: SR<SimpleEntityPrototype>;
+  "simple-entity-with-force"?: SR<SimpleEntityWithForcePrototype>;
+  "simple-entity-with-owner"?: SR<SimpleEntityWithOwnerPrototype>;
+  "smoke-with-trigger"?: SR<SmokeWithTriggerPrototype>;
+  "solar-panel-equipment"?: SR<SolarPanelEquipmentPrototype>;
+  "solar-panel"?: SR<SolarPanelPrototype>;
+  "space-connection-distance-traveled-achievement"?: SR<SpaceConnectionDistanceTraveledAchievementPrototype>;
+  "space-connection"?: SR<SpaceConnectionPrototype>;
+  "space-location"?: SR<SpaceLocationPrototype>;
+  "space-platform-hub"?: SR<SpacePlatformHubPrototype>;
+  "space-platform-starter-pack"?: SR<SpacePlatformStarterPackPrototype>;
+  "speech-bubble"?: SR<SpeechBubblePrototype>;
+  "spider-leg"?: SR<SpiderLegPrototype>;
+  "spider-unit"?: SR<SpiderUnitPrototype>;
+  "spider-vehicle"?: SR<SpiderVehiclePrototype>;
+  "spidertron-remote"?: SR<SpidertronRemotePrototype>;
+  splitter?: SR<SplitterPrototype>;
+  sticker?: SR<StickerPrototype>;
+  "storage-tank"?: SR<StorageTankPrototype>;
+  "straight-rail"?: SR<StraightRailPrototype>;
+  "surface-property"?: SR<SurfacePropertyPrototype>;
+  surface?: SR<SurfacePrototype>;
+  technology?: SR<TechnologyPrototype>;
+  "temporary-container"?: SR<TemporaryContainerPrototype>;
+  thruster?: SR<ThrusterPrototype>;
+  "tile-ghost"?: SR<TileGhostPrototype>;
+  tile?: SR<TilePrototype>;
+  "tips-and-tricks-item"?: SR<TipsAndTricksItem>;
+  tool?: SR<ToolPrototype>;
+  "train-path-achievement"?: SR<TrainPathAchievementPrototype>;
+  "train-stop"?: SR<TrainStopPrototype>;
+  "transport-belt"?: SR<TransportBeltPrototype>;
+  tree?: SR<TreePrototype>;
+  "trivial-smoke"?: SR<TrivialSmokePrototype>;
+  turret?: SR<TurretPrototype>;
+  tutorial?: SR<TutorialDefinition>;
+  "underground-belt"?: SR<UndergroundBeltPrototype>;
+  unit?: SR<UnitPrototype>;
+  "upgrade-item"?: SR<UpgradeItemPrototype>;
+  "use-entity-in-energy-production-achievement"?: SR<UseEntityInEnergyProductionAchievementPrototype>;
+  "use-item-achievement"?: SR<UseItemAchievementPrototype>;
+  "utility-constants"?: SR<UtilityConstants>;
+  "utility-sounds"?: SR<UtilitySounds>;
+  "utility-sprites"?: SR<UtilitySprites>;
+  valve?: SR<ValvePrototype>;
+  "virtual-signal"?: SR<VirtualSignalPrototype>;
+  wall?: SR<WallPrototype>;
+};

+ 205 - 284
scripts/factorio-api.ts → scripts/factorio-dump/lua-api/parse-factorio-model.ts

@@ -1,284 +1,205 @@
-import https from "node:https";
-
-import fs from "fs";
-import handlebars from "handlebars";
-
-import type * as M from "./factorio-api.models.d.ts";
-
-interface Data {
-  app: string;
-  appVersion: string;
-  apiVersion: number;
-  stage: string;
-  interfaces: Interface[];
-  types: DataType[];
-}
-
-interface Interface {
-  name: string;
-  description: string;
-  export: boolean;
-  extends?: string;
-  typeName?: string;
-  props: Property[];
-  isTypeFunction?: string;
-}
-
-interface Property {
-  name: string;
-  description: string;
-  optional: boolean;
-  type: string;
-}
-
-interface DataType {
-  name: string;
-  description: string;
-  value: string;
-}
-
-const API_PATH = "https://lua-api.factorio.com/latest/prototype-api.json";
-
-function getPrototypeApi(): Promise<M.PrototypeApi> {
-  return new Promise((resolve, reject) => {
-    https
-      .get(API_PATH, (res) => {
-        let data = "";
-
-        res.on("data", (chunk: string) => {
-          data += chunk;
-        });
-
-        res.on("end", () => {
-          const api = JSON.parse(data) as M.PrototypeApi;
-          resolve(api);
-        });
-      })
-      .on("error", (err) => {
-        reject(err);
-      });
-  });
-}
-
-function parseType(type: M.DataType, structName?: string): string {
-  if (typeof type === "string") {
-    // Handle known built-in types
-    switch (type) {
-      case "bool":
-        return "boolean";
-      case "double":
-      case "float":
-      case "int8":
-      case "int16":
-      case "int32":
-      case "uint8":
-      case "uint16":
-      case "uint32":
-      case "uint64":
-        return "number";
-      case "string":
-        return "string";
-      case "DataExtendMethod":
-        return "() => void";
-      default:
-        return type;
-    }
-  }
-
-  switch (type.complex_type) {
-    case "array":
-      return `${parseType(type.value, structName)}[]`;
-    case "dictionary":
-      return `Record<${parseType(type.key, structName)}, ${parseType(type.value, structName)}>`;
-    case "literal": {
-      if (typeof type.value === "string") {
-        return `'${type.value}'`;
-      } else {
-        return type.value.toString();
-      }
-    }
-    case "struct":
-      if (structName == null)
-        throw new Error(
-          "Unexpected struct: use properties on parent or pass a struct name to use"
-        );
-
-      return structName;
-    case "tuple":
-      return `[${type.values.map((v) => parseType(v, structName)).join(", ")}]`;
-    case "type":
-      return parseType(type.value, structName);
-    case "union":
-      return `(${type.options.map((o) => parseType(o, structName)).join(" | ")})`;
-  }
-}
-
-function parseProperty(prop: M.Property): Property {
-  return {
-    name: prop.name,
-    description: prop.description,
-    optional: prop.optional,
-    type: parseType(prop.type),
-  };
-}
-
-function getTypePropertyValue(props: M.Property[]): string | undefined {
-  const typeProp = props.find((p) => p.name === "type");
-  if (
-    typeProp &&
-    typeof typeProp.type !== "string" &&
-    typeProp.type.complex_type === "literal" &&
-    typeof typeProp.type.value === "string"
-  ) {
-    return typeProp.type.value;
-  }
-
-  return undefined;
-}
-
-function parsePrototypeApi(api: M.PrototypeApi): Data {
-  const data: Data = {
-    app: api.application,
-    appVersion: api.application_version,
-    apiVersion: api.api_version,
-    stage: api.stage,
-    interfaces: [],
-    types: [],
-  };
-
-  api.prototypes.forEach((p) => {
-    const int: Interface = {
-      name: p.name,
-      description: p.description,
-      export: p.parent == null,
-      extends: p.parent,
-      props: p.properties.map((p) => parseProperty(p)),
-    };
-
-    const typePropValue = getTypePropertyValue(p.properties);
-    if (typePropValue != null) {
-      int.isTypeFunction = typePropValue;
-    } else if (p.typename) {
-      int.typeName = p.typename;
-      int.isTypeFunction = p.typename;
-    }
-
-    data.interfaces.push(int);
-  });
-
-  api.types.forEach((t) => {
-    if (typeof t.type === "string") {
-      if (t.type !== "builtin") {
-        const dataType: DataType = {
-          name: t.name,
-          description: t.description,
-          value: parseType(t.type),
-        };
-
-        data.types.push(dataType);
-      }
-    } else if (t.type.complex_type === "struct") {
-      const int: Interface = {
-        name: t.name,
-        description: t.description,
-        export: t.parent == null,
-        extends: t.parent,
-        props: [],
-      };
-
-      if (t.properties) {
-        int.props = t.properties.map((p) => parseProperty(p));
-
-        int.isTypeFunction = getTypePropertyValue(t.properties);
-      }
-
-      data.interfaces.push(int);
-    } else {
-      let structName: string | undefined;
-
-      if (t.properties) {
-        structName = `_${t.name}`;
-        const int: Interface = {
-          name: structName,
-          description: t.description,
-          export: false,
-          extends: t.parent,
-          props: t.properties.map((p) => parseProperty(p)),
-        };
-
-        data.interfaces.push(int);
-      }
-
-      const dataType: DataType = {
-        name: t.name,
-        description: t.description,
-        value: parseType(t.type, structName),
-      };
-
-      data.types.push(dataType);
-    }
-  });
-
-  return data;
-}
-
-const generate = async function (): Promise<void> {
-  console.log(`Regenerating Factorio prototype models from ${API_PATH}...`);
-
-  const api = await getPrototypeApi();
-  const data = parsePrototypeApi(api);
-  const modelsSource = `/** Generated file, do not edit. See scripts/factorio-api.ts for generator. */
-
-/**
- * Application: {{app}}
- * Version: {{appVersion}}
- * API Version: {{apiVersion}}
- */
-
-{{#interfaces}}
-{{#if description}}
-/** {{{description}}} */
-{{/if}}
-{{#unless props.length}}
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-{{/unless}}
-{{#if export}}export {{/if}}interface {{#if extends}}_{{/if}}{{name}} {
-  {{#if typeName}}
-  type: '{{typeName}}';
-  {{/if}}
-  {{#props}}
-  {{#if description}}
-  /** {{{description}}} */
-  {{/if}}
-  {{name}}{{#if optional}}?{{/if}}: {{{type}}};
-  {{/props}}
-}
-{{#if extends}}
-
-export type {{name}} = _{{name}} & Omit<{{extends}}, keyof _{{name}}>;
-{{/if}}
-{{#if isTypeFunction}}
-
-export function is{{name}}(value: unknown): value is {{name}} {
-  return (value as { type: string }).type === '{{isTypeFunction}}';
-}
-
-{{/if}}
-{{/interfaces}}
-{{#types}}
-
-{{#if description}}
-/** {{{description}}} */
-{{/if}}
-export type {{name}} = {{{value}}};
-
-{{/types}}`;
-
-  const modelsTemplate = handlebars.compile(modelsSource);
-  const result = modelsTemplate(data);
-
-  fs.writeFileSync("scripts/factorio-dump.models.ts", result);
-  console.log(`Generated ${api.prototypes.length.toString()} models`);
-};
-
-void generate();
+import fs from "fs";
+import Handlebars from "handlebars";
+import type * as M from "./lua-api.models.js";
+import { FULL_API_TEMPLATE } from "./template.ts";
+import path from "path";
+
+interface Data {
+  app: string;
+  appVersion: string;
+  apiVersion: number;
+  stage: string;
+  interfaces: Interface[];
+  types: DataType[];
+}
+
+interface Interface {
+  name: string;
+  description: string;
+  export: boolean;
+  extends?: string;
+  typeName?: string;
+  props: Property[];
+  isTypeFunction?: string;
+}
+
+interface Property {
+  name: string;
+  description: string;
+  optional: boolean;
+  type: string;
+}
+
+interface DataType {
+  name: string;
+  description: string;
+  value: string;
+}
+
+function parseType(type: M.DataType, structName?: string): string {
+  if (typeof type === "string") {
+    // Handle known built-in types
+    switch (type) {
+      case "bool":
+        return "boolean";
+      case "double":
+      case "float":
+      case "int8":
+      case "int16":
+      case "int32":
+      case "uint8":
+      case "uint16":
+      case "uint32":
+      case "uint64":
+        return "number";
+      case "string":
+        return "string";
+      case "DataExtendMethod":
+        return "() => void";
+      default:
+        return type;
+    }
+  }
+
+  switch (type.complex_type) {
+    case "array":
+      return `${parseType(type.value, structName)}[]`;
+    case "dictionary":
+      return `Record<${parseType(type.key, structName)}, ${parseType(type.value, structName)}>`;
+    case "literal": {
+      if (typeof type.value === "string") {
+        return `'${type.value}'`;
+      } else {
+        return type.value.toString();
+      }
+    }
+    case "struct":
+      if (structName == null)
+        throw new Error("Unexpected struct: use properties on parent or pass a struct name to use");
+
+      return structName;
+    case "tuple":
+      return `[${type.values.map((v) => parseType(v, structName)).join(", ")}]`;
+    case "type":
+      return parseType(type.value, structName);
+    case "union":
+      return `(${type.options.map((o) => parseType(o, structName)).join(" | ")})`;
+  }
+}
+
+function parseProperty(prop: M.Property): Property {
+  return {
+    name: prop.name,
+    description: prop.description,
+    optional: prop.optional,
+    type: parseType(prop.type),
+  };
+}
+
+function getTypePropertyValue(props: M.Property[]): string | undefined {
+  const typeProp = props.find((p) => p.name === "type");
+  if (
+    typeProp &&
+    typeof typeProp.type !== "string" &&
+    typeProp.type.complex_type === "literal" &&
+    typeof typeProp.type.value === "string"
+  ) {
+    return typeProp.type.value;
+  }
+
+  return undefined;
+}
+
+export function parsePrototypeApi(api: M.PrototypeApi): Data {
+  const data: Data = {
+    app: api.application,
+    appVersion: api.application_version,
+    apiVersion: api.api_version,
+    stage: api.stage,
+    interfaces: [],
+    types: [],
+  };
+
+  api.prototypes.forEach((p) => {
+    const int: Interface = {
+      name: p.name,
+      description: p.description,
+      export: p.parent == null,
+      extends: p.parent,
+      props: p.properties.map((p) => parseProperty(p)),
+    };
+
+    const typePropValue = getTypePropertyValue(p.properties);
+    if (typePropValue != null) {
+      int.isTypeFunction = typePropValue;
+    } else if (p.typename) {
+      int.typeName = p.typename;
+      int.isTypeFunction = p.typename;
+    }
+
+    data.interfaces.push(int);
+  });
+
+  api.types.forEach((t) => {
+    if (typeof t.type === "string") {
+      if (t.type !== "builtin") {
+        const dataType: DataType = {
+          name: t.name,
+          description: t.description,
+          value: parseType(t.type),
+        };
+
+        data.types.push(dataType);
+      }
+    } else if (t.type.complex_type === "struct") {
+      const int: Interface = {
+        name: t.name,
+        description: t.description,
+        export: t.parent == null,
+        extends: t.parent,
+        props: [],
+      };
+
+      if (t.properties) {
+        int.props = t.properties.map((p) => parseProperty(p));
+
+        int.isTypeFunction = getTypePropertyValue(t.properties);
+      }
+
+      data.interfaces.push(int);
+    } else {
+      let structName: string | undefined;
+
+      if (t.properties) {
+        structName = `_${t.name}`;
+        const int: Interface = {
+          name: structName,
+          description: t.description,
+          export: false,
+          extends: t.parent,
+          props: t.properties.map((p) => parseProperty(p)),
+        };
+
+        data.interfaces.push(int);
+      }
+
+      const dataType: DataType = {
+        name: t.name,
+        description: t.description,
+        value: parseType(t.type, structName),
+      };
+
+      data.types.push(dataType);
+    }
+  });
+
+  return data;
+}
+
+export const generateDataModel = async function (outputFolder: string, api: M.PrototypeApi): Promise<void> {
+  const data = parsePrototypeApi(api);
+  const modelsTemplate = Handlebars.compile(FULL_API_TEMPLATE);
+  const result = modelsTemplate(data);
+  fs.writeFileSync(path.join(outputFolder, "models.ts"), result);
+  console.log(`Generated ${api.prototypes.length.toString()} models`);
+};

+ 73 - 0
scripts/factorio-dump/lua-api/prototype-tree.ts

@@ -0,0 +1,73 @@
+import fs from "fs";
+import * as path from "path";
+
+import type * as M from "./lua-api.models.js";
+import { TEMPLATE_HEADER } from "./template.ts";
+
+type PrototypeTreeNode = M.Prototype & { children: PrototypeTreeNode[] };
+
+/**
+ * Build a full prototype tree – each node keeps a list of its children.
+ *
+ * @returns the top‑level prototypes (the array you normally walk over)
+ */
+function computePrototypeTree(api: M.PrototypeApi): PrototypeTreeNode[] {
+  const prototypeMap: Record<string, PrototypeTreeNode> = {};
+  for (let p of api.prototypes) {
+    prototypeMap[p.name] = { ...p, children: [] };
+  }
+  for (let p of Object.values(prototypeMap)) {
+    if (p.parent && p.parent in prototypeMap) prototypeMap[p.parent].children.push(p);
+  }
+  return Object.values(prototypeMap).filter((p) => !p.parent);
+}
+/**
+ * Recursive helper that returns of all *typename* keys that correspond to a particular Prototype.
+ */
+function listChildrenTypename(nodes: PrototypeTreeNode[], parentName: string): string[] {
+  const output: string[] = [];
+  for (let node of nodes) {
+    if (!parentName || node.name == parentName) {
+      output.push(...listChildrenTypename(node.children, ""));
+      if (node.typename) output.push(node.typename);
+    } else {
+      output.push(...listChildrenTypename(node.children, parentName));
+    }
+  }
+  return output;
+}
+
+/**
+ * Write out the three `const` arrays that correspond to the 3 main types of Prototype in Factorio
+ */
+export function extractEntityAndItemTypeName(outputFolder: string, api: M.PrototypeApi) {
+  const treeRoot = computePrototypeTree(api);
+
+  const entityTypeNames = listChildrenTypename(treeRoot, "EntityPrototype").sort();
+  const itemTypeNames = listChildrenTypename(treeRoot, "ItemPrototype");
+  itemTypeNames.push(...listChildrenTypename(treeRoot, "FluidPrototype"));
+  itemTypeNames.sort();
+
+  const SpaceLocationTypeNames = listChildrenTypename(treeRoot, "SpaceLocationPrototype").sort();
+
+  const result = `${TEMPLATE_HEADER}
+import type { FactorioRawData } from "./models.ts"
+type ValidKeys = Array<keyof FactorioRawData> 
+
+/**
+ * All the keys that could contain item prototypes.
+ */
+export const ITEM_KEYS: ValidKeys = ${JSON.stringify(itemTypeNames, null, 2)}
+
+/**
+ * All the keys that could contain entity prototypes.
+ */
+export const ENTITY_KEYS: ValidKeys = ${JSON.stringify(entityTypeNames, null, 2)}
+
+/**
+ * All the keys that could contains space location prototypes.
+ */
+export const SPACE_LOCATION_KEYS: ValidKeys = ${JSON.stringify(SpaceLocationTypeNames, null, 2)}
+`;
+  fs.writeFileSync(path.join(outputFolder, "const.ts"), result);
+}

+ 57 - 0
scripts/factorio-dump/lua-api/template.ts

@@ -0,0 +1,57 @@
+export const TEMPLATE_HEADER =
+  "/** Generated file, do not edit. See scripts/factorio-dump/lua-api/index.ts for generator. */";
+export const FULL_API_TEMPLATE = `${TEMPLATE_HEADER}
+
+/**
+ * Application: {{app}}
+ * Version: {{appVersion}}
+ * API Version: {{apiVersion}}
+ */
+
+{{#interfaces}}
+{{#if description}}
+/** {{{description}}} */
+{{/if}}
+{{#unless props.length}}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+{{/unless}}
+{{#if export}}export {{/if}}interface {{#if extends}}_{{/if}}{{name}} {
+  {{#if typeName}}
+  type: '{{typeName}}';
+  {{/if}}
+  {{#props}}
+  {{#if description}}
+  /** {{{description}}} */
+  {{/if}}
+  {{name}}{{#if optional}}?{{/if}}: {{{type}}};
+  {{/props}}
+}
+{{#if extends}}
+
+export type {{name}} = _{{name}} & Omit<{{extends}}, keyof _{{name}}>;
+{{/if}}
+{{#if isTypeFunction}}
+
+export function is{{name}}(value: unknown): value is {{name}} {
+  return (value as { type: string }).type === '{{isTypeFunction}}';
+}
+
+{{/if}}
+{{/interfaces}}
+{{#types}}
+
+{{#if description}}
+/** {{{description}}} */
+{{/if}}
+export type {{name}} = {{{value}}};
+
+{{/types}}
+type SR<T> = Record<string, T>;
+export type FactorioRawData = {
+  {{#interfaces}}
+    {{#if typeName}}
+     "{{typeName}}"?:SR<{{name}}>;
+    {{/if}}
+  {{/interfaces}}
+}  
+`;

+ 85 - 0
scripts/factorio-dump/process-data.models.ts

@@ -0,0 +1,85 @@
+import * as M from "./lua-api/models.ts";
+import type { Group } from "./helpers/groups.helper.ts";
+import type { Recipe } from "./helpers/recipes.helper.ts";
+
+export interface ModList {
+  mods: { name: string; enabled: boolean }[];
+}
+
+export type EffectType = "speed" | "productivity" | "consumption" | "pollution" | "quality";
+
+export const allEffects: EffectType[] = ["consumption", "speed", "productivity", "pollution", "quality"];
+
+export interface Locale {
+  names: Record<string, string>;
+}
+export type Item = {
+  name: string;
+  icon?: string;
+  hidden: boolean;
+  subgroup: string;
+  order?: string;
+};
+export type Machine = {
+  name: string;
+  icon?: string;
+  allowed_effects?: string | Array<string>;
+  crafting_categories: string[];
+  crafting_speed: number;
+  selection_box?: M.BoundingBox;
+  module_slots?: number;
+  effect_receiver?: M.EffectReceiver;
+};
+export type Module = {
+  name: string;
+  icon?: string;
+  subgroup?: string;
+  category: string;
+  tier: number;
+  order?: string;
+  effect?: M.Effect;
+};
+export type Beacon = {
+  name: string;
+  icon?: string;
+  subgroup?: string;
+  module_slots: number;
+  /** The types of [modules](prototype:ModulePrototype) that a player can place inside of the beacon. */
+  allowed_effects?: M.EffectTypeLimitation;
+  /** Extra multiplier applied to the effects received from beacon by the effect receiver based on amount of beacons that are in range of that effect receiver.
+
+  If there are more beacons that reach the effect receiver than there are entries in this array, then the last entry in the array is used for the multiplier.
+  
+  If this is not defined, then an implicit profile of `{1}` will be used. */
+  profile?: number[];
+  /** The multiplier of the module's effects, when shared between neighbors. */
+
+  distribution_effectivity: number;
+  /** Must be 0 or positive. */
+  distribution_effectivity_bonus_per_quality_level?: number;
+};
+
+export type RecipeCategory = {
+  name: string;
+  subgroup?: string;
+  order?: string;
+};
+export type Quality = M.QualityPrototype & {
+  icon: string;
+};
+export type Signal = {
+  name: string;
+  type: string;
+  icon?: string;
+  subgroup: string;
+  order?: string;
+};
+export type ProcessedData = {
+  machines: Machine[];
+  beacons: Beacon[];
+  qualityLevels: Quality[];
+  modules: Module[];
+  recipeCategories: RecipeCategory[];
+  recipeGroup: Group<Recipe>[];
+  signalGroup: Group<Signal>[];
+};

+ 187 - 0
scripts/factorio-dump/process-data.ts

@@ -0,0 +1,187 @@
+import fs from "fs";
+import { checkIcons, dataRawPath, getJsonData, scriptOutputPath } from "./helpers/file.helper.ts";
+import type { Beacon, Item, Machine, Module, Quality, RecipeCategory, Signal } from "./process-data.models.ts";
+
+import { groupPrototypes, type Group } from "./helpers/groups.helper.ts";
+import { parseRecipe, type Recipe } from "./helpers/recipes.helper.ts";
+import type { ProcessedData } from "./process-data.models.ts";
+import { buildIconTextureMap, walkForIcons } from "./helpers/icons.helper.ts";
+import path from "path";
+import { ENTITY_KEYS, ITEM_KEYS, SPACE_LOCATION_KEYS } from "./lua-api/const.ts";
+import type { FactorioRawData } from "./lua-api/models.ts";
+
+const isNotHidden = (o: unknown) => !(o as { hidden?: boolean }).hidden;
+
+/**
+ * Extract machines.
+ */
+function extractMachines(data: FactorioRawData): Machine[] {
+  return Object.values(data["assembling-machine"] ?? {})
+    .filter(isNotHidden)
+    .map((machine) => ({
+      name: machine.name,
+      icon: `entity/${machine.name}.png`,
+      crafting_categories: machine.crafting_categories,
+      crafting_speed: machine.crafting_speed,
+      allowed_effects: machine.allowed_effects,
+      module_slots: machine.module_slots,
+      selection_box: machine.selection_box,
+      effect_receiver: machine.effect_receiver,
+    }));
+}
+/**
+ * Extract modules.
+ */
+function extractModules(data: FactorioRawData): Module[] {
+  return Object.values(data.module ?? {})
+    .filter(isNotHidden)
+    .map((module) => ({
+      name: module.name,
+      icon: `item/${module.name}.png`,
+      subgroup: module.subgroup,
+      category: module.category,
+      tier: module.tier,
+      order: module.order,
+      effect: module.effect,
+    }));
+}
+/**
+ * Extract beacon.
+ */
+function extractBeacon(data: FactorioRawData): Beacon[] {
+  return Object.values(data.beacon ?? {})
+    .filter(isNotHidden)
+    .map((beacon) => ({
+      name: beacon.name,
+      icon: `item/${beacon.name}.png`,
+      subgroup: beacon.subgroup,
+      distribution_effectivity: beacon.distribution_effectivity,
+      distribution_effectivity_bonus_per_quality_level: beacon.distribution_effectivity_bonus_per_quality_level,
+      module_slots: beacon.module_slots,
+      allowed_effects: beacon.allowed_effects,
+      profile: beacon.profile,
+    }));
+}
+/**
+ * Extract all items (and item‑like things) from the raw dump.
+ */
+function extractItems(dataRaw: FactorioRawData): Record<string, Item> {
+  return ITEM_KEYS.reduce((acc: Record<string, Item>, key) => {
+    const data = dataRaw[key];
+    Object.values(data ?? {}).forEach((item) => {
+      const isFluid = item.type === "fluid";
+      acc[item.name] = {
+        name: item.name,
+        hidden: !isNotHidden(item),
+        icon: `${isFluid ? "fluid" : "item"}/${item.name}.png`,
+        subgroup: item.subgroup ?? (isFluid ? "fluid" : "other"),
+        order: item.order,
+      };
+    });
+    return acc;
+  }, {});
+}
+
+export async function processData(outputFolder: string) {
+  const data = getJsonData(dataRawPath) as FactorioRawData;
+  const items = extractItems(data);
+  const machines = extractMachines(data);
+  const modules = extractModules(data);
+
+  const recipeCategories: RecipeCategory[] = Object.values(data["recipe-category"] ?? {})
+    .filter(isNotHidden)
+    .map((c) => ({ name: c.name, subgroup: c.subgroup, order: c.order }));
+
+  const recipes = Object.values(data["recipe"] ?? {})
+    .filter(isNotHidden)
+    .map((o) => parseRecipe(o, items));
+  const qualityLevels: Quality[] = Object.values(data.quality ?? {})
+    .filter(isNotHidden)
+    .map((o) => ({
+      ...o,
+      icon: `quality/${o.name}.png`,
+    }));
+
+  const signals: Record<string, Signal> = {};
+  const EXCLUDED_ENTITY: Array<keyof FactorioRawData> = [
+    "corpse",
+    "rail-remnants",
+    "sticker",
+    "elevated-straight-rail",
+    "straight-rail",
+  ];
+  const signalGroups: { signalType: string; entityTypes: Array<keyof FactorioRawData> }[] = [
+    { signalType: "virtual-signal", entityTypes: ["virtual-signal"] },
+    { signalType: "quality", entityTypes: ["quality"] },
+    { signalType: "entity", entityTypes: ENTITY_KEYS },
+    { signalType: "space-location", entityTypes: SPACE_LOCATION_KEYS },
+  ];
+  for (let { signalType, entityTypes } of signalGroups)
+    for (let entityTypeName of entityTypes) {
+      let entries = data[entityTypeName];
+      console.log(entityTypeName);
+      if (entries !== undefined)
+        for (let entry of Object.values(entries)) {
+          if (isNotHidden(entry) && !EXCLUDED_ENTITY.includes(entry.type) && !entry.deconstruction_alternative)
+            signals[entry.name] = {
+              type: signalType,
+              name: entry.name,
+              subgroup: entry.subgroup ?? "other",
+              icon: `${signalType}/${entry.name}.png`,
+              order: entry.order,
+            };
+        }
+    }
+  for (let item of Object.values(items)) {
+    if (!item.hidden) {
+      const signal = {
+        name: item.name,
+        type: "item",
+        subgroup: item.subgroup ?? "",
+        icon: item.icon,
+        order: item.order,
+      };
+      let k = item.name;
+      if (k in signals) {
+        if (signal.subgroup && signals[k].subgroup != "other" && signals[k].subgroup != signal.subgroup) {
+          k = "item." + k;
+        } else {
+          console.log(`Existing entry for ${k} in the same group`);
+        }
+      }
+      signals[k] = signal;
+    }
+  }
+  for (let recipe of recipes) {
+    if (!(recipe.name in signals))
+      signals[recipe.name] = {
+        type: "recipe",
+        name: recipe.name,
+        subgroup: recipe.subgroup ?? "",
+        icon: recipe.icon,
+        order: recipe.order,
+      };
+  }
+
+  const recipeGroup: Array<Group<Recipe>> = groupPrototypes(recipes, data, true);
+  const signalGroup: Array<Group<Signal>> = groupPrototypes(Object.values(signals), data, true);
+  const beacons = extractBeacon(data);
+  const output: ProcessedData = {
+    qualityLevels,
+    machines,
+    beacons,
+    modules,
+    recipeCategories,
+    recipeGroup,
+    signalGroup,
+  };
+  fs.mkdirSync(outputFolder, { recursive: true });
+
+  const icons = new Set<string>();
+  walkForIcons(output, icons);
+  const iconsList = Array.from(icons).sort();
+  checkIcons(iconsList);
+
+  fs.writeFileSync(path.resolve(outputFolder, "data.json"), JSON.stringify(output, null, 2), "utf-8");
+  await buildIconTextureMap(iconsList, scriptOutputPath, outputFolder);
+}

+ 0 - 114
scripts/factorio-process-data.models.ts

@@ -1,114 +0,0 @@
-import * as M from "./factorio-dump.models.ts";
-import type { Group } from "./helpers/groups.helper.ts";
-import type { Recipe } from "./helpers/recipes.helper.ts";
-
-export interface ModList {
-  mods: { name: string; enabled: boolean }[];
-}
-
-export type EffectType = "speed" | "productivity" | "consumption" | "pollution" | "quality";
-
-export const allEffects: EffectType[] = ["consumption", "speed", "productivity", "pollution", "quality"];
-type Entities<T = string> = Record<string, T>;
-export interface DataRawDump {
-  "agricultural-tower"?: Entities<M.AgriculturalTowerPrototype>;
-  ammo: Entities<M.AmmoItemPrototype>;
-  armor: Entities<M.ArmorPrototype>;
-  "assembling-machine": Entities<M.AssemblingMachinePrototype>;
-  asteroid?: Entities<M.AsteroidPrototype>;
-  "asteroid-chunk": Entities<M.AsteroidChunkPrototype>;
-  "asteroid-collector"?: Entities<M.AsteroidCollectorPrototype>;
-  beacon: Entities<M.BeaconPrototype>;
-  boiler: Entities<M.BoilerPrototype>;
-  capsule: Entities<M.CapsulePrototype>;
-  "cargo-wagon": Entities<M.CargoWagonPrototype>;
-  fluid: Entities<M.FluidPrototype>;
-  "fluid-wagon": Entities<M.FluidWagonPrototype>;
-  furnace: Entities<M.FurnacePrototype>;
-  gun: Entities<M.GunPrototype>;
-  item: Entities<M.ItemPrototype>;
-  "item-group": Entities<M.ItemGroup>;
-  "item-subgroup": Entities<M.ItemSubGroup>;
-  "item-with-entity-data": Entities<M.ItemWithEntityDataPrototype>;
-  lab: Entities<M.LabPrototype>;
-  "mining-drill": Entities<M.MiningDrillPrototype>;
-  module: Entities<M.ModulePrototype>;
-  "offshore-pump": Entities<M.OffshorePumpPrototype>;
-  planet: Entities<M.PlanetPrototype>;
-  plant: Entities<M.PlantPrototype>;
-  pump: Entities<M.PumpPrototype>;
-  quality: Entities<M.QualityPrototype>;
-  "rail-planner": Entities<M.RailPlannerPrototype>;
-  reactor: Entities<M.ReactorPrototype>;
-  recipe: Entities<M.RecipePrototype>;
-  "recipe-category": Entities<M.RecipeCategory>;
-  "repair-tool": Entities<M.RepairToolPrototype>;
-  resource: Entities<M.ResourceEntityPrototype>;
-  "rocket-silo": Entities<M.RocketSiloPrototype>;
-  "rocket-silo-rocket": Entities<M.RocketSiloRocketPrototype>;
-  "selection-tool": Entities<M.SelectionToolPrototype>;
-  "space-connection"?: Entities<M.SpaceConnectionPrototype>;
-  "space-location": Entities<M.SpaceLocationPrototype>;
-  "space-platform-starter-pack"?: Entities<M.SpacePlatformStarterPackPrototype>;
-  "spidertron-remote": Entities<M.SpidertronRemotePrototype>;
-  surface: Entities<M.SurfacePrototype>;
-  "surface-property": Entities<M.SurfacePropertyPrototype>;
-  technology: Entities<M.TechnologyPrototype>;
-  tile: Entities<M.TilePrototype>;
-  tool: Entities<M.ToolPrototype>;
-  "transport-belt": Entities<M.TransportBeltPrototype>;
-  "utility-constants": { default: M.UtilityConstants };
-  "virtual-signal": Entities<M.VirtualSignalPrototype>;
-}
-
-export interface Locale {
-  names: Entities;
-}
-export type Item = {
-  name: string;
-  icon?: string;
-  subgroup: string;
-  order?: string;
-};
-export type Machine = {
-  name: string;
-  icon?: string;
-  allowed_effects?: string | Array<string>;
-  crafting_categories: string[];
-  crafting_speed: number;
-  selection_box?: M.BoundingBox;
-  module_slots?: number;
-  effect_receiver?: M.EffectReceiver;
-};
-export type Module = {
-  name: string;
-  icon?: string;
-  subgroup?: string;
-  category: string;
-  tier: number;
-  order?: string;
-  effect?: M.Effect;
-};
-
-export type RecipeCategory = {
-  name: string;
-  subgroup?: string;
-  order?: string;
-};
-export type Quality = M.QualityPrototype & {
-  icon: string;
-};
-export type Signal = {
-  name: string;
-  icon?: string;
-  subgroup: string;
-  order?: string;
-};
-export type ProcessedData = {
-  machines: Machine[];
-  qualityLevels: Quality[];
-  modules: Module[];
-  recipeCategories: RecipeCategory[];
-  recipeGroup: Group<Recipe>[];
-  signalGroup: Group<Signal>[];
-};

+ 0 - 172
scripts/factorio-process-data.ts

@@ -1,172 +0,0 @@
-import fs from "fs";
-import { checkIcons, dataRawPath, getJsonData, scriptOutputPath } from "./helpers/file.helper.ts";
-import type {
-  DataRawDump,
-  Item,
-  Machine,
-  Module,
-  Quality,
-  RecipeCategory,
-  Signal,
-} from "./factorio-process-data.models.js";
-
-import { groupPrototypes, type Group } from "./helpers/groups.helper.ts";
-import { parseRecipe, type Recipe } from "./helpers/recipes.helper.ts";
-import type { ProcessedData } from "./factorio-process-data.models.ts";
-import { buildIconTextureMap, walkForIcons } from "./helpers/icons.helper.ts";
-import path from "path";
-
-//const modsPath = `${factorioPath}/mods`;
-
-// const tempPath = "./scripts/temp";
-// const tempIconsPath = `${tempPath}/icons`;
-const isHidden = (o: unknown) => !(o as { hidden?: boolean }).hidden;
-
-/**
- * Extract machines.
- */
-function extractMachines(data: DataRawDump): Machine[] {
-  return Object.values(data["assembling-machine"])
-    .filter(isHidden)
-    .map((machine) => ({
-      name: machine.name,
-      icon: `entity/${machine.name}.png`,
-      crafting_categories: machine.crafting_categories,
-      crafting_speed: machine.crafting_speed,
-      allowed_effects: machine.allowed_effects,
-      module_slots: machine.module_slots,
-      selection_box: machine.selection_box,
-      effect_receiver: machine.effect_receiver,
-    }));
-}
-/**
- * Extract modules.
- */
-function extractModules(data: DataRawDump): Module[] {
-  return Object.values(data.module)
-    .filter(isHidden)
-    .map((module) => ({
-      name: module.name,
-      icon: `item/${module.name}.png`,
-      subgroup: module.subgroup,
-      category: module.category,
-      tier: module.tier,
-      order: module.order,
-      effect: module.effect,
-    }));
-}
-// TODO : Add quality Modules and quality Machines
-
-/**
- * All the keys that contain item‑like entities.
- */
-const ITEM_KEYS = [
-  "item",
-  "ammo",
-  "armor",
-  "capsule",
-  "gun",
-  "item-with-entity-data",
-  "module",
-  "rail-planner",
-  "repair-tool",
-  "selection-tool",
-  "spidertron-remote",
-  "space-platform-starter-pack",
-  "tool",
-  "fluid",
-] as const;
-/**
- * Extract all items (and item‑like things) from the raw dump.
- */
-function extractItems(dataRaw: DataRawDump): Record<string, Item> {
-  return ITEM_KEYS.reduce((acc: Record<string, Item>, key) => {
-    const data = dataRaw[key];
-    if (data)
-      Object.values(data)
-        .filter(isHidden)
-        .forEach((item) => {
-          const isFluid = item.type === "fluid";
-          acc[item.name] = {
-            name: item.name,
-            icon: `${isFluid ? "fluid" : "item"}/${item.name}.png`,
-            subgroup: item.subgroup ?? (isFluid ? "fluid" : "other"),
-            order: item.order,
-          };
-        });
-    return acc;
-  }, {});
-}
-
-export async function processData(outputFolder: string) {
-  const data = getJsonData(dataRawPath) as DataRawDump;
-  const items = extractItems(data);
-  const machines = extractMachines(data);
-  const modules = extractModules(data);
-
-  const recipeCategories: RecipeCategory[] = Object.values(data["recipe-category"])
-    .filter(isHidden)
-    .map((c) => ({ name: c.name, subgroup: c.subgroup, order: c.order }));
-
-  const recipes = Object.values(data["recipe"])
-    .filter(isHidden)
-    .map((o) => parseRecipe(o, items));
-  const qualityLevels: Quality[] = Object.values(data.quality)
-    .filter(isHidden)
-    .map((o) => ({
-      ...o,
-      icon: `quality/${o.name}.png`,
-    }));
-  const recipeGroup: Array<Group<Recipe>> = groupPrototypes(recipes, data, false);
-
-  const signals: Array<Signal> = [];
-  for (let item of Object.values(items)) {
-    signals.push({
-      name: item.name,
-      subgroup: item.subgroup ?? "",
-      icon: item.icon,
-      order: item.order,
-    });
-  }
-
-  const virtualSignals = Object.values(data["virtual-signal"]).filter(isHidden);
-  for (let vs of virtualSignals) {
-    signals.push({
-      name: vs.name,
-      subgroup: vs.subgroup ?? "",
-      icon: `virtual-signal/${vs.name}.png`,
-      order: vs.order,
-    });
-  }
-  for (let quality of qualityLevels) {
-    signals.push({
-      name: quality.name,
-      subgroup: quality.subgroup ?? "",
-      icon: quality.icon,
-      order: quality.order,
-    });
-  }
-  const signalGroup: Array<Group<Signal>> = groupPrototypes(signals, data, false);
-
-  const output: ProcessedData = {
-    qualityLevels,
-    machines,
-    modules,
-    recipeCategories,
-    recipeGroup,
-    signalGroup,
-  };
-  fs.mkdirSync(outputFolder, { recursive: true });
-
-  fs.writeFileSync(
-    path.resolve(outputFolder, "data.json"),
-    JSON.stringify(output, null, 2),
-    "utf-8"
-  );
-  const icons = new Set<string>();
-  walkForIcons(output, icons);
-  const iconsList = Array.from(icons).sort();
-  checkIcons(iconsList);
-  await buildIconTextureMap(iconsList, scriptOutputPath, outputFolder);
-  console.log(`✅ Done - data written to ${outputFolder}`);
-}

+ 27 - 0
scripts/helpers/logger.ts

@@ -0,0 +1,27 @@
+// logger.ts
+import ora from "ora";
+
+export async function step<T extends (...args: any[]) => Promise<any>>(
+  name: string,
+  fn: T,
+  ...args: any[]
+): Promise<any> {
+  console.time(name);
+  const spinner = ora({ text: `${name}…`, spinner: "dots" }).start();
+  const consoleLog = console.log;
+  console.log = (text) => {
+    spinner.text += "\n" + text;
+  };
+  try {
+    const result = await fn(...args);
+    spinner.succeed(`${name} finished`);
+    console.timeEnd(name);
+    return result;
+  } catch (err) {
+    spinner.fail(`${name} failed`);
+    console.timeEnd(name);
+    throw err;
+  } finally {
+    console.log = consoleLog;
+  }
+}

+ 0 - 16
scripts/index.ts

@@ -1,16 +0,0 @@
-import { processData } from "./factorio-process-data.ts";
-import { dumpFactorioData, dumpFactorioIcon } from "./helpers/command.helper.ts";
-import { extractLocale } from "./helpers/locale.helper.ts";
-
-const outputFolder = "./src/assets/data/2.0";
-
-console.log("Dump factorio data");
-await dumpFactorioData();
-
-console.log("Dump factorio icons");
-await dumpFactorioIcon();
-console.log("Process factorio data and icons");
-await processData(outputFolder);
-
-console.log("Dump factorio locale");
-await extractLocale(outputFolder);

+ 9 - 16
scripts/tsconfig.json

@@ -1,16 +1,9 @@
-{
-  "extends": "../tsconfig.node.json",
-  "compilerOptions": {
-    "module": "NodeNext",
-    "moduleResolution": "NodeNext",
-    "esModuleInterop": true
-  },
-  "files": [
-    "helpers/file.helper.ts",
-    "factorio-api.models.d.ts",
-    "factorio-api.ts",
-    "factorio-process-data.models.ts",
-    "factorio-process-data.ts"
-  ],
-  "include": ["**/*.ts"]
-}
+{
+  "extends": "../tsconfig.node.json",
+  "compilerOptions": {
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "esModuleInterop": true
+  },
+  "include": ["**/*.ts"]
+}

+ 0 - 615
src/App.css

@@ -1,615 +0,0 @@
-#root {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-  text-align: center;
-}
-.panel-button {
-  border: none;
-  cursor: pointer;
-  position: relative;
-  background-color: #313031;
-  height: 24px;
-  aspect-ratio: 1;
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -8px 4px -8px #000,
-    inset 0px 8px 4px -8px #fff,
-    inset 0px -8px 2px -8px #432400,
-    0px 0px 4px 0px #000;
-}
-.panel-button:active,
-.panel-button.active {
-  box-shadow:
-    inset 0px 9px 2px -8px #000,
-    inset 8px 0px 4px -8px #563a10,
-    inset 8px 0px 4px -8px #563a10,
-    inset -8px 0px 4px -8px #563a10,
-    inset -8px 0px 4px -8px #563a10,
-    inset 0px -9px 2px -8px #fff,
-    0px 0px 4px 0px #000;
-  background-color: #f1be64;
-  filter: none;
-  outline: 0;
-}
-
-.panel-button.hover,
-.panel-button:focus,
-.panel-button:hover {
-  color: #000;
-  text-decoration: none;
-  outline: 0;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 9px 2px -8px #fff,
-    inset 0px -8px 4px -8px hsl(36, 94%, 20%),
-    0px 0px 4px 0px #000,
-    inset 0px 0px 4px 2px #f9b44b;
-  background-color: #e39827;
-  filter: drop-shadow(0 0 2px #f9b44b);
-}
-input:focus {
-  border: none;
-  filter: drop-shadow(0 0 2px #f9b44b);
-}
-.button {
-  background-color: #8e8e8e;
-  padding: 10px 12px 10px 12px;
-  font-size: 100%;
-  text-align: left;
-  color: #000;
-  font-weight: 600;
-  display: inline-block;
-  vertical-align: baseline;
-  min-width: 128px;
-  border: none;
-  line-height: inherit;
-  white-space: nowrap;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 10px 2px -8px #e3e3e3,
-    inset 0px 10px 2px -8px #282828,
-    inset 0px -9px 2px -8px #000,
-    0px 0px 4px 0px #000;
-  position: relative;
-  margin-right: 14px;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  height: 36px;
-  text-align: left;
-}
-.button.icon {
-  height: fit-content;
-  min-width: auto;
-  padding: 0;
-  margin: 0;
-}
-.flex-column .button {
-  margin-right: 0;
-}
-.button i {
-  margin-right: 0px;
-}
-.button:hover,
-.button.hover {
-  color: #000;
-  text-decoration: none;
-  outline: 0;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 9px 2px -8px #fff,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -8px 4px -8px #000,
-    inset 0px -9px 2px -8px #432400,
-    0px 0px 4px 0px #000,
-    inset 0px 0px 4px 2px #f9b44b;
-  background-color: #e39827;
-  filter: drop-shadow(0 0 2px #f9b44b);
-}
-.button:active,
-.button.active {
-  position: relative;
-  padding-top: 12px;
-  padding-bottom: 8px;
-  vertical-align: -2px;
-  box-shadow:
-    inset 0px 10px 2px -8px #000,
-    inset 0px 9px 2px -8px #000,
-    inset 8px 0px 4px -8px #563a10,
-    inset 8px 0px 4px -8px #563a10,
-    inset -8px 0px 4px -8px #563a10,
-    inset -8px 0px 4px -8px #563a10,
-    inset 0px 9px 2px -8px #563a10,
-    inset 0px -9px 2px -8px #563a10,
-    inset 0px -8.5px 0px -8px #563a10,
-    0px 0px 4px 0px #000;
-  background-color: #f1be64;
-  filter: none;
-  outline: 0;
-}
-.button.disabled,
-.button.disabled:hover,
-.button.disabled:active {
-  padding-top: 10px;
-  padding-bottom: 10px;
-  cursor: default;
-  vertical-align: 0;
-  background-color: #3d3d3d;
-  color: #818181;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -6px 4px -8px #818181,
-    inset 0px -8px 4px -8px #000,
-    0px 0px 4px 0px #000;
-  filter: none;
-}
-.button-green {
-  background-color: #5eb663;
-  padding: 10px 12px 10px 12px;
-  font-size: 100%;
-  text-align: left;
-  color: #000;
-  font-weight: 600;
-  display: inline-block;
-  vertical-align: baseline;
-  min-width: 128px;
-  border: none;
-  line-height: inherit;
-  white-space: nowrap;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 10px 2px -8px #95df99,
-    inset 0px 10px 2px -8px #163218,
-    inset 0px -9px 2px -8px #000,
-    0px 0px 4px 0px #000;
-  position: relative;
-  margin-right: 14px;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  height: 36px;
-  text-align: left;
-}
-.flex-column .button-green {
-  margin-right: 0;
-}
-.button-green i {
-  margin-right: 0px;
-}
-.button-green:hover,
-.button-green:focus,
-.button-green.hover {
-  color: #000;
-  text-decoration: none;
-  outline: 0;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 9px 2px -8px #cdf1cf,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -8px 4px -8px #000,
-    inset 0px -9px 2px -8px #432400,
-    0px 0px 4px 0px #000,
-    inset 0px 0px 4px 2px #34be3c;
-  background-color: #92e897;
-  filter: drop-shadow(0 0 2px #34be3c);
-}
-.button-green:active,
-.button-green.active {
-  position: relative;
-  padding-top: 12px;
-  padding-bottom: 8px;
-  vertical-align: -2px;
-  box-shadow:
-    inset 0px 10px 2px -8px #000,
-    inset 0px 9px 2px -8px #000,
-    inset 8px 0px 4px -8px #3f5024,
-    inset 8px 0px 4px -8px #3f5024,
-    inset -8px 0px 4px -8px #3f5024,
-    inset -8px 0px 4px -8px #3f5024,
-    inset 0px 9px 2px -8px #3f5024,
-    inset 0px -9px 2px -8px #3f5024,
-    inset 0px -8.5px 0px -8px #3f5024,
-    0px 0px 4px 0px #000;
-  background-color: #cfdf93;
-  filter: none;
-  outline: 0;
-}
-.button-green.disabled,
-.button-green.disabled:hover,
-.button-green.disabled:active {
-  padding-top: 10px;
-  padding-bottom: 10px;
-  cursor: default;
-  vertical-align: 0;
-  background-color: #002b02;
-  color: #376d3b;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -6px 4px -8px #376d3b,
-    inset 0px -8px 4px -8px #000,
-    0px 0px 4px 0px #000;
-  filter: none;
-}
-.button-green-right {
-  background-color: #5eb663;
-  padding: 10px 12px 10px 12px;
-  font-size: 100%;
-  text-align: left;
-  color: #000;
-  font-weight: 600;
-  display: inline-block;
-  vertical-align: baseline;
-  min-width: 128px;
-  border: none;
-  line-height: inherit;
-  white-space: nowrap;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset 0px 10px 2px -8px #95df99,
-    inset 0px 10px 2px -8px #163218,
-    inset 0px -9px 2px -8px #000,
-    0px 0px 4px 0px #000;
-  position: relative;
-  margin-right: 14px;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  height: 36px;
-  text-align: right;
-}
-.button-green-right::after {
-  content: "";
-  position: absolute;
-  height: 100%;
-  width: 24px;
-  right: -10px;
-  top: 0;
-  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAABACAMAAADCiszgAAABDlBMVEUAAAASMBURLxJJgUkAKxNKiU1Ih01msGpMjVFNjlFLik9ToFdLik9Lik9Lik5Yq1wIHgoOKhGAyoVNjVFipmY3cDpQkVVrsHB2vHlXmFyK1Y5/yoRvuHNcs2E7eT8bQB1doGFAe0IJIQxNjFJDgkhjrGgGHgxetmNcs2FasF9RllVVpllXqlxRnlVQm1RYrV0cQh5SoVcUNRdQk1VLllA8e0AXOhpQkFM0bDcqXC4jTiVRmFVFiUkIHgpTpFhNmVJEh0hNj1EtYTCI1I16w35utnJXn1tNk1FJkk1HjUs/fkI3czsyajYpWSsZPhxrsW9ZnV1XmVo6dz41bzgmUygkUCYiTSUTMRUPKxEOKRFdNbmJAAAAJ3RSTlMAVisODG41Bvb75Zfc1dGXlwb98/7y8fDS0JeXl5eXl25sVjU1MSucyRWzAAABMElEQVQ4y23M51LCUBiE4QWRKFFB7L37EUJTithI6FXs7f5vxDgok3Oy+/OdZxbGdrOZ3DyDOmMn2WptHM5odbfdflzdOtGq2ek8Pa/Pqtg0nVK3eLUUVWrcvsil08UVFaeszG/WsKQsT+tYJJW3s7caFpl8FOf8WKZ40YeFYqFYKBaKhWLx44V/LBQLxaLg+T8sFAvFouFYdFIZFoqFYqFYArjuYQngeuwUEsB19wii4lqu2x+s6dXO9QavDa1atuMOG2+Bh1658VFQq5XJutX3cQjaQal8XbgMQ6N31ZtKKAJKQSkoBaWgFJSCUlAKSkEpKAWloBSUglJQCkpBKSgFpdiz8rUARTxec9yRRmGYTv/lQaMw9ocjL6oUywfjyufXdxhqTdx7O46QmjhX4w9Fq5Q/daB+bwAAAABJRU5ErkJggg==");
-  background-repeat: no-repeat;
-  background-position: 100% 0%;
-  background-size: contain;
-}
-.button-green-right i {
-  margin-right: 0px;
-}
-.button-green-right:hover,
-.button-green-right:focus,
-.button-green-right.hover {
-  color: #000;
-  text-decoration: none;
-  outline: 0;
-  filter: brightness(1.3) drop-shadow(0 0 2px #34be3c);
-}
-.button-green-right:active,
-.button-green-right.active {
-  position: relative;
-  padding-top: 12px;
-  padding-bottom: 8px;
-  vertical-align: -2px;
-  box-shadow:
-    inset 0px 10px 2px -8px #000,
-    inset 0px 9px 2px -8px #000,
-    inset 8px 0px 4px -8px #3f5024,
-    inset 8px 0px 4px -8px #3f5024,
-    inset 0px 9px 2px -8px #3f5024,
-    inset 0px -9px 2px -8px #3f5024,
-    inset 0px -8.5px 0px -8px #3f5024,
-    0px 0px 4px 0px #000;
-  background-color: #cfdf93;
-  filter: none;
-  outline: 0;
-}
-.button-green-right:active::after,
-.button-green-right.active::after {
-  content: "";
-  position: absolute;
-  height: 100%;
-  width: 24px;
-  right: -10px;
-  top: 0;
-  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAABACAMAAADCiszgAAACBFBMVEUAAACWq1clPgwnPRChwmOhv18iOQ4iOhEdLwwhNBAkOA8jOQ4iNBAkPBSJolSUs1t1ij4kNA4MHwAhLwwhMw0hMQ+Fn0+Jp16Xt14aLA0kOxQlPBR5jkAnOBMqOhYVJQYOIgMcLgsLHQIoPRgZKQoiOBEkOxF4jj6SsFkLGwMlORdOYCRleS4/UB9ohU8xRR3P35PD5ovO3pHM3ZDK243A1IXG2Iq4zX7N4JLM4ZHL4pDJ4o+ev2nH44/F5I6guGoxRxitw3WBmlNxiUZOZS294YW12H0+VCKqzXOcvGWSqmBedjmevmSRqV9geDoQIQIJGgLJ346v0nquxHUmMSMEEQTF4Y3B44mtzom+3IegwoSVt3+etWlwj2KKpld+llJ/mVFuhUQ6TiLH2oq704nA24e21Ya73oW73oOz1X210n2oy3Klxm6lvW6Lq22YuWWaumSUs2KYt2CQpl6Rr1yLqFuNplqBnFF0jEN2kEJeci9MYi1LYCtBVCU9UyItQBguQxccKBLK3Y3C2onD1YfC2IWrzYS22YC6z32bvny1ynu10HigwnaoyHOpx22brmmjv2WWtWGMq2GPr196lV2AnFprh1iSrlZ4k1ZuilaLp1F4kEpyikhmfUF1jEBdcD5acjdUZzdtgjZacDNNYi5HWy0zQyg4TCYoNx4pOxsmMRogMA46/ea2AAAAMHRSTlMADhT7YTLolBhR/O2XTTk1Ev779vKcbWxlVk5MPv76+Ovpp6CclpRzZ15aVU5JRxpOaUmtAAAB4klEQVQ4y23Q5VuTcRTG8VtRBKQEAxS7++BgM6YOwYBJbOo6AQnp7rK7u5v6J3keHsbO2fW7X36vz4tzHVRryzmIhNVoy9mVArkb2mp25ENuZm5qqnr7ngT849e/6dn5namyvugJf/trS0tfKWqxpa0/OjqZnCFq0d3HgbB9PGmfwCeKiksqS8tsyVt4JZPZcv6C/XLSGo5JiUmJKY4zWFViUmJSYlrCFRpeu5lXA1cxTEpMAu9dwiRxarzGcezPtIyvVXwts603MAk8npaeYlSGS0cmDUwcR+y16zbomDiujI5O5ObrleNA+KeBieOSnujIRO4hrTJcZwlEqsa27j6uV3HGlz+1246CeK572NY/NDx2WNbb9568/Px9+Iio5Lh56/mHod8Fop7UamtocH+2qKcc3mbrwGABwOlpl9fZFdmYDXB6xtX4rG8gEwCnZ90PrCGdgtNzbmdXSKfgtLCptffACr1yWtjc0ZsFfZxebHmVt0jB6SVrt0HB6ZXOVQaFoOWrYYzT8hgFp1djFEoKJYWSQkmhpFBSKCmUFCaTw+VuaukUFGbzHW+js6NbUNTXNzQ4rW/yBIXH47n/9HVfFsR8Pt+j9nebJIXf729/+zETcsFg8P2nRIrr2v4fg9wCPnjQTyTwM1IAAAAASUVORK5CYII=");
-  background-repeat: no-repeat;
-  background-position: 100% 0%;
-  background-size: contain;
-}
-.button-green-right.disabled,
-.button-green-right.disabled:hover,
-.button-green-right.disabled:active {
-  padding-top: 10px;
-  padding-bottom: 10px;
-  cursor: default;
-  vertical-align: 0;
-  background-color: #002b02;
-  color: #376d3b;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -6px 4px -8px #376d3b,
-    inset 0px -8px 4px -8px #000,
-    0px 0px 4px 0px #000;
-  filter: none;
-}
-.button-red {
-  background-color: #fe5a5a;
-  padding: 10px 12px 10px 12px;
-  font-size: 100%;
-  text-align: left;
-  color: #000;
-  font-weight: 600;
-  display: inline-block;
-  vertical-align: baseline;
-  min-width: 128px;
-  border: none;
-  line-height: inherit;
-  white-space: nowrap;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 10px 2px -8px #fda1a1,
-    inset 0px 10px 2px -8px #8b0101,
-    inset 0px -9px 2px -8px #000,
-    0px 0px 4px 0px #000;
-  position: relative;
-  margin-right: 14px;
-  cursor: pointer;
-  -webkit-user-select: none;
-  -moz-user-select: none;
-  user-select: none;
-  height: 36px;
-  text-align: left;
-}
-.flex-column .button-red {
-  margin-right: 0;
-}
-.button-red i {
-  margin-right: 0px;
-}
-.button-red:hover,
-.button-red:focus,
-.button-red.hover {
-  color: #000;
-  text-decoration: none;
-  outline: 0;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 9px 2px -8px #f8eaea,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -8px 4px -8px #000,
-    inset 0px -9px 2px -8px #432400,
-    0px 0px 4px 0px #000,
-    inset 0px 0px 4px 2px #c35353;
-  background-color: #ff9b9b;
-  filter: drop-shadow(0 0 2px #c35353);
-}
-.button-red:active,
-.button-red.active {
-  position: relative;
-  padding-top: 12px;
-  padding-bottom: 8px;
-  vertical-align: -2px;
-  box-shadow:
-    inset 0px 10px 2px -8px #000,
-    inset 0px 9px 2px -8px #000,
-    inset 8px 0px 4px -8px #642323,
-    inset 8px 0px 4px -8px #642323,
-    inset -8px 0px 4px -8px #642323,
-    inset -8px 0px 4px -8px #642323,
-    inset 0px 9px 2px -8px #642323,
-    inset 0px -9px 2px -8px #642323,
-    inset 0px -8.5px 0px -8px #642323,
-    0px 0px 4px 0px #000;
-  background-color: #fca8a8;
-  filter: none;
-  outline: 0;
-}
-.button-red.disabled,
-.button-red.disabled:hover,
-.button-red.disabled:active {
-  padding-top: 10px;
-  padding-bottom: 10px;
-  cursor: default;
-  vertical-align: 0;
-  background-color: #571f1f;
-  color: #8c4e4e;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -6px 4px -8px #8c4e4e,
-    inset 0px -8px 4px -8px #000,
-    0px 0px 4px 0px #000;
-  filter: none;
-}
-input[type="text"],
-input[type="password"],
-input[type="email"],
-input[type="url"],
-input[type="search"],
-textarea {
-  vertical-align: baseline;
-  font-family: inherit;
-  line-height: 1.2;
-  font-size: 105%;
-  height: 36px;
-  max-width: 100%;
-  background: #8e8e8e;
-  border-radius: 4px;
-  padding: 6px;
-  border: none;
-  box-shadow:
-    inset 0px 4px 1px -2px #000,
-    inset 0px -4px 1px -2px #c5c5c5,
-    inset 2px 0px 1px 0px #5f5f5f,
-    inset -2px 0px 1px 0px #5f5f5f,
-    inset 0px -2px 2px 0px #5f5f5f,
-    0px 0px 4px 1px #2e2521;
-}
-input[type="text"]:focus,
-input[type="password"]:focus,
-input[type="email"]:focus,
-input[type="url"]:focus,
-input[type="search"]:focus,
-textarea:focus {
-  outline: none;
-  background: #f0dab4;
-  box-shadow:
-    inset 0px 4px 2px -2px #000,
-    inset 0px -1px 1px 0px #74624b,
-    inset 0px -4px 2px -2px #e0e0e0,
-    inset 2px 0px 2px 0px #a6885c,
-    inset -2px 0px 2px 0px #a6885c,
-    0px 0px 4px 1px #2e2521;
-}
-textarea {
-  width: 100%;
-  height: 8.1em;
-}
-
-.checkbox-label {
-  margin-top: 8px;
-  margin-bottom: 8px;
-  display: flex;
-  align-content: center;
-  position: relative;
-  cursor: pointer;
-  line-height: 1.2;
-  break-inside: avoid;
-}
-.checkbox-label > div {
-  width: 100%;
-  padding-left: 8px;
-}
-.checkbox-label input {
-  position: relative;
-  top: -1px;
-  left: 0px;
-  width: 16px;
-  height: 15px;
-  padding: 6px;
-  position: absolute;
-  opacity: 0;
-  cursor: pointer;
-}
-.checkbox-label .checkbox {
-  margin: auto;
-  position: relative;
-  top: -1px;
-  left: 0px;
-  width: 16px;
-  height: 15px;
-  padding: 6px;
-  background: #8e8e8e;
-  box-shadow:
-    inset 0px 4px 2.5px -2.5px #111,
-    inset 2px 0px 2px 0px #323232,
-    inset -2px 0px 2px 0px #323232,
-    inset 0px -1px 1px 0px #fff,
-    inset 0px -4px 2px -2px #8e8e8e,
-    0px 0px 4px 1px #2e2521;
-}
-.checkbox-label .checkbox:hover,
-.checkbox-label .checkbox.hover,
-.checkbox-label .checkbox:active,
-.checkbox-label .checkbox.active,
-.checkbox-label input:focus ~ .checkbox {
-  background: #e39827;
-  box-shadow:
-    inset 0px 4px 2px -2px #412a07,
-    inset 2px 0px 2px 0px #5a3c10,
-    inset -2px 0px 2px 0px #5a3c10,
-    inset 0px -2px 2px 0px #d0ae79,
-    inset 0px -2px 4px 0px #c78627,
-    0px 0px 4px 1px #786b4f;
-}
-.checkbox-label .checkbox:active,
-.checkbox-label .checkbox.active {
-  background: #f3c98e;
-  box-shadow:
-    inset 0px 4px 2px -2px #412a07,
-    inset 2px 0px 2px 0px #5a3c10,
-    inset -2px 0px 2px 0px #5a3c10,
-    inset 0px -2px 2px 0px #d0ae79,
-    inset 0px -2px 4px 0px #c78627,
-    inset 0px 1px 2.5px 4.5px #e39827,
-    0px 0px 4px 1px #786b4f;
-}
-.checkbox-label .checkbox:disabled,
-.checkbox-label .checkbox.disabled {
-  background: #313031;
-  box-shadow:
-    inset 0px 4px 2.5px -2.5px #111,
-    inset 2px 0px 2px 0px #323232,
-    inset -2px 0px 2px 0px #323232,
-    inset 0px -1px 1px 0px #666,
-    inset 0px -4px 2px -2px #8e8e8e,
-    0px 0px 4px 1px #2e2521;
-}
-.checkbox-label input:checked ~ .checkbox {
-  background: #e39827;
-}
-.checkbox-label input:checked ~ .checkbox:active,
-.checkbox-label input:checked ~ .checkbox.active {
-  background: #f3c98e;
-}
-.checkbox-label .checkbox::after {
-  position: absolute;
-  content: "";
-}
-.checkbox-label input:checked ~ .checkbox::after {
-  -webkit-transform: rotate(45deg) scale(1);
-  -ms-transform: rotate(45deg) scale(1);
-  transform: rotate(45deg) scale(1);
-  opacity: 1;
-  left: 6px;
-  top: 3px;
-  width: 2px;
-  height: 6px;
-  border: solid #282728;
-  border-width: 0 2.5px 2.5px 0;
-  background-color: transparent;
-  border-radius: 0;
-}
-.disable-checkbox-label input[type="checkbox"] {
-  position: relative;
-  opacity: 1;
-  -webkit-appearance: none;
-  appearance: none;
-  margin: 0;
-  margin-top: 2px;
-  width: 1.2em;
-  height: 1.2em;
-  cursor: pointer;
-  background-color: transparent;
-  background-repeat: no-repeat;
-  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNhNmE2YTZ9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
-}
-.disable-checkbox-label input[type="checkbox"]:hover {
-  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNmZmI1MzN9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
-}
-.disable-checkbox-label input[type="checkbox"]:checked {
-  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNmZmEyMDB9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
-}
-
-.simplebar-scrollbar::before {
-  background-color: #e39827;
-}

+ 7 - 39
src/App.tsx

@@ -1,39 +1,7 @@
-import { FormControl, InputLabel, MenuItem, Select, type SelectChangeEvent } from "@mui/material";
-import "simplebar-react/dist/simplebar.min.css";
-import "./App.css";
-
-import data from "./assets/data/2.0/data.json";
-import SelectMenu from "./assets/SelectRecipeMenu";
-import { useMemo, useState } from "react";
-
-function App() {
-  const [group, setGroup] = useState("Signals");
-  const handleChange = (event: SelectChangeEvent) => {
-    setGroup(event.target.value);
-  };
-  const categories = useMemo(() => (group == "Signals" ? data.signalGroup : data.recipeGroup), [group]);
-  return (
-    <>
-      <h1>Vite + React</h1>
-      <div className="card">
-        <FormControl fullWidth>
-          <InputLabel id="demo-simple-select-label">Groups</InputLabel>
-          <Select
-            labelId="demo-simple-select-label"
-            id="demo-simple-select"
-            value={group}
-            label="Age"
-            onChange={handleChange}
-          >
-            <MenuItem value={"Signals"}>Signals</MenuItem>
-            <MenuItem value={"Recipes"}>Recipes</MenuItem>
-          </Select>
-        </FormControl>
-        <SelectMenu categories={categories} title={`Select ${group.toLowerCase()}`}></SelectMenu>
-      </div>
-      <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
-    </>
-  );
-}
-
-export default App;
+import "./App.css";
+
+function App() {
+  return;
+}
+
+export default App;

+ 109 - 0
src/ClockBuilder.module.css

@@ -0,0 +1,109 @@
+.wrap {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  background-color: #313031;
+  border: 1px solid #646464;
+  padding: 12px;
+  color: #ffe6c0;
+  font-size: 13px;
+}
+
+.header {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 14px;
+}
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.field label {
+  font-size: 11px;
+  color: #999;
+}
+.field input,
+.field select {
+  background-color: #242324;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 5px 8px;
+  font-family: ui-monospace, monospace;
+  font-size: 12.5px;
+  min-width: 120px;
+}
+.field input:focus,
+.field select:focus {
+  outline: 2px solid #f1be64;
+  outline-offset: 1px;
+}
+
+.footer {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.actions {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+.generateBtn {
+  background-color: #f1be64;
+  color: #1a1300;
+  border: none;
+  border-radius: 5px;
+  padding: 8px 16px;
+  font-weight: 700;
+  font-size: 12.5px;
+  cursor: pointer;
+}
+.generateBtn:hover {
+  background-color: #e39827;
+}
+.copyBtn {
+  background: none;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 5px;
+  padding: 8px 14px;
+  font-size: 12.5px;
+  cursor: pointer;
+}
+.copyBtn:hover:not(:disabled) {
+  border-color: #f1be64;
+  color: #f1be64;
+}
+.copyBtn:disabled {
+  opacity: 0.4;
+  cursor: not-allowed;
+}
+.count {
+  font-family: ui-monospace, monospace;
+  font-size: 11.5px;
+  color: #999;
+}
+.statusOk {
+  font-size: 11.5px;
+  color: #7fc98a;
+}
+.statusErr {
+  font-size: 11.5px;
+  color: #e07a63;
+}
+
+.output {
+  width: 100%;
+  min-height: 80px;
+  background-color: #242324;
+  border: 1px solid #646464;
+  color: #7fc98a;
+  font-family: ui-monospace, monospace;
+  font-size: 11.5px;
+  border-radius: 4px;
+  padding: 8px;
+  resize: vertical;
+}

+ 211 - 0
src/ClockBuilder.tsx

@@ -0,0 +1,211 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import ClockTimeline, { defaultActivationSignal } from "./assets/ClockTimeline";
+import { buildBlueprint } from "./blueprint/Blueprintbuilder";
+import { encodeBlueprintFileBrowser } from "./blueprint/parser";
+import type { ClockBlock, ClockRow } from "./assets/types";
+import styles from "./ClockBuilder.module.css";
+import SelectedBlockPanel from "./assets/components/SelectedBlockPanel";
+import SelectSignal, { type Signal } from "./assets/SelectSignal";
+import ExpressionInput from "./assets/components/ExpressionInpux";
+
+let uid = 0;
+const nextId = (prefix: string) => `${prefix}-${++uid}-${Date.now()}`;
+
+const defaultClockSignal: Signal = {
+  type: "virtual-signal",
+  name: "signal-clock",
+  subgroup: "pictographs",
+  icon: "virtual-signal/signal-clock.png",
+  order: "p[clock]",
+};
+export default function ClockBuilder() {
+  const [duration, setDuration] = useState(256);
+  const [clockSignal, setClockSignal] = useState<Signal | null>(defaultClockSignal);
+
+  const [rows, setRows] = useState<ClockRow[]>([
+    { id: nextId("row"), name: "Row 1", signals: [defaultActivationSignal], stackSize: 16, inserterCount: 1 },
+  ]);
+  const [displayUnit, setDisplayUnit] = useState<"s" | "m">("s");
+  const [beltReference, setBeltReference] = useState({ name: "turbo-belt", itemsPerSecond: 240 });
+  const [blocks, setBlocks] = useState<ClockBlock[]>([]);
+  const [selectedBlockIds, setSelectedBlockIds] = useState<Set<string>>(new Set());
+  const selectedBlocks = blocks.filter((b) => selectedBlockIds.has(b.id));
+
+  useEffect(() => {
+    const isEditable = (el: Element | null) =>
+      !!el &&
+      (el.tagName === "INPUT" ||
+        el.tagName === "TEXTAREA" ||
+        el.tagName === "SELECT" ||
+        (el as HTMLElement).isContentEditable);
+
+    const handler = (e: KeyboardEvent) => {
+      if (isEditable(document.activeElement)) return;
+
+      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "d") {
+        e.preventDefault();
+        if (selectedBlockIds.size === 0) return;
+        const clones: ClockBlock[] = [];
+        blocks.forEach((b) => {
+          if (selectedBlockIds.has(b.id)) {
+            clones.push({
+              ...b,
+              id: `block-${Date.now()}-${Math.random().toString(36).slice(2)}`,
+              start: b.start + b.duration,
+            });
+          }
+        });
+        setBlocks((prev) => [...prev, ...clones]);
+        setSelectedBlockIds(new Set(clones.map((c) => c.id)));
+      } else if (e.key === "Delete") {
+        if (selectedBlockIds.size === 0) return;
+        e.preventDefault();
+        setBlocks((prev) => prev.filter((b) => !selectedBlockIds.has(b.id)));
+        setSelectedBlockIds(new Set());
+      }
+    };
+    window.addEventListener("keydown", handler);
+    return () => window.removeEventListener("keydown", handler);
+  }, [blocks, selectedBlockIds]);
+
+  const handleChangeOne = (blockId: string, patch: Partial<ClockBlock>) => {
+    setBlocks((prev) => prev.map((b) => (b.id === blockId ? { ...b, ...patch } : b)));
+  };
+  const handleRemoveSelected = () => {
+    setBlocks((prev) => prev.filter((b) => !selectedBlockIds.has(b.id)));
+    setSelectedBlockIds(new Set());
+  };
+  const [output, setOutput] = useState("");
+  const [status, setStatus] = useState<{ text: string; ok: boolean } | null>(null);
+
+  const totalCombinators = useMemo(() => rows.length + 1, [rows]);
+
+  const handleSaveFile = () => {
+    const config = { duration, clockSignal, rows, blocks };
+    const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" });
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement("a");
+    a.href = url;
+    a.download = `clock-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.json`;
+    a.click();
+    URL.revokeObjectURL(url);
+  };
+
+  const fileInputRef = useRef<HTMLInputElement>(null);
+  const handleLoadFile = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file) return;
+    const reader = new FileReader();
+    reader.onload = () => {
+      try {
+        const parsed = JSON.parse(reader.result as string);
+        setDuration(parsed.duration);
+        setClockSignal(parsed.clockSignal);
+        setRows(parsed.rows);
+        setBlocks(parsed.blocks);
+        setStatus({ text: "Clock loaded.", ok: true });
+      } catch {
+        setStatus({ text: "That file isn't a valid clock export.", ok: false });
+      }
+    };
+    reader.readAsText(file);
+    e.target.value = "";
+  };
+  const handleGenerate = () => {
+    if (rows.length === 0 || blocks.length === 0) {
+      setStatus({ text: "Add at least one row and one block first.", ok: false });
+      return;
+    }
+    try {
+      const bp = buildBlueprint({ duration, clockSignal: clockSignal ?? defaultClockSignal, rows, blocks });
+      const str = encodeBlueprintFileBrowser(bp);
+      setOutput(str);
+      setStatus({ text: `Generated — ${bp.blueprint.entities.length} combinators.`, ok: true });
+    } catch (err) {
+      setStatus({ text: `Error: ${(err as Error).message}`, ok: false });
+    }
+  };
+
+  const handleCopy = async () => {
+    if (!output) return;
+    try {
+      await navigator.clipboard.writeText(output);
+      setStatus({ text: "Copied to clipboard.", ok: true });
+    } catch {
+      setStatus({ text: "Copy failed — select the text manually.", ok: false });
+    }
+  };
+
+  return (
+    <div className={styles.wrap}>
+      <div className={styles.header}>
+        <div className={styles.field}>
+          <label>Cycle length (ticks)</label>
+          <ExpressionInput value={duration} min={2} onCommit={setDuration} />
+        </div>
+
+        <div className={styles.field}>
+          <label>Clock signal icon</label>
+          <SelectSignal value={clockSignal?.name} onSelectSignal={(_, sig) => setClockSignal(sig)} />
+        </div>
+        <div className={styles.field}>
+          <label>Throughput unit</label>
+          <select value={displayUnit} onChange={(e) => setDisplayUnit(e.target.value as "s" | "m")}>
+            <option value="s">items/s</option>
+            <option value="m">items/min</option>
+          </select>
+        </div>
+        <div className={styles.field}>
+          <label>Belt reference</label>
+          <input
+            type="text"
+            value={beltReference.name}
+            onChange={(e) => setBeltReference({ ...beltReference, name: e.target.value })}
+          />
+          <input
+            type="number"
+            value={beltReference.itemsPerSecond}
+            onChange={(e) => setBeltReference({ ...beltReference, itemsPerSecond: Number(e.target.value) || 0 })}
+          />
+        </div>
+      </div>
+
+      <ClockTimeline
+        duration={duration}
+        rows={rows}
+        blocks={blocks}
+        onRowsChange={setRows}
+        onBlocksChange={setBlocks}
+        selectedBlockIds={selectedBlockIds}
+        onSelectBlocks={setSelectedBlockIds}
+      />
+      <SelectedBlockPanel
+        selectedBlocks={selectedBlocks}
+        onChangeOne={handleChangeOne}
+        onRemoveSelected={handleRemoveSelected}
+      />
+      <div className={styles.footer}>
+        <div className={styles.actions}>
+          <button className={styles.generateBtn} onClick={handleGenerate}>
+            Generate blueprint
+          </button>
+          <button className={styles.copyBtn} onClick={handleCopy} disabled={!output}>
+            Copy string
+          </button>
+          <span className={styles.count}>
+            {totalCombinators} combinator{totalCombinators === 1 ? "" : "s"}
+          </span>
+          <button className={styles.copyBtn} onClick={handleSaveFile}>
+            Save clock
+          </button>
+          <input ref={fileInputRef} type="file" accept="application/json" hidden onChange={handleLoadFile} />
+          <button className={styles.copyBtn} onClick={() => fileInputRef.current?.click()}>
+            Load clock
+          </button>
+          {status && <span className={status.ok ? styles.statusOk : styles.statusErr}>{status.text}</span>}
+        </div>
+        <textarea className={styles.output} readOnly value={output} placeholder="Blueprint string will appear here…" />
+      </div>
+    </div>
+  );
+}

+ 65 - 0
src/ComponentTests.tsx

@@ -0,0 +1,65 @@
+import { FormControl, InputLabel, MenuItem, Select, TextField, type SelectChangeEvent } from "@mui/material";
+import "simplebar-react/dist/simplebar.min.css";
+
+import data from "./assets/data/2.0/data.json";
+import SelectMenu from "./assets/SelectFactorioMenu";
+import { useMemo, useState } from "react";
+import { decodeBlueprintFileBrowser } from "./blueprint/parser";
+import MachineSelector from "./assets/MachineSelector";
+import Icon from "./assets/icon";
+import SelectSignal from "./assets/SelectSignal";
+
+function ComponentTests() {
+  const [blueprint, setBlueprint] = useState("");
+  const [group, setGroup] = useState("Signals");
+  const handleChange = (event: SelectChangeEvent) => {
+    setGroup(event.target.value);
+  };
+  const categories = useMemo(() => (group == "Signals" ? data.signalGroup : data.recipeGroup), [group]);
+  return (
+    <>
+      <div className="card">
+        <FormControl fullWidth>
+          <InputLabel id="demo-simple-select-label">Groups</InputLabel>
+          <Select
+            labelId="demo-simple-select-label"
+            id="demo-simple-select"
+            value={group}
+            label="Age"
+            onChange={handleChange}
+          >
+            <MenuItem value={"Signals"}>Signals</MenuItem>
+            <MenuItem value={"Recipes"}>Recipes</MenuItem>
+          </Select>
+        </FormControl>
+        <SelectMenu categories={categories} title={`Select ${group.toLowerCase()}`}></SelectMenu>
+      </div>
+
+      <div>
+        <TextField
+          multiline
+          fullWidth
+          label="Blueprintstring"
+          id="blueprintstring"
+          onChange={(evt) => setBlueprint(JSON.stringify(decodeBlueprintFileBrowser(evt.target.value), null, 2))}
+        />
+        <TextField multiline fullWidth label="Blueprint Content" id="decodedblueprint" value={blueprint} />
+      </div>
+      <div>
+        <MachineSelector></MachineSelector>
+      </div>
+      <div>
+        <SelectSignal></SelectSignal>
+      </div>
+      <div>
+        <Icon iconName="item/transport-belt.png" qualityLevel={0}></Icon>
+        <Icon iconName="item/transport-belt.png" qualityLevel={1}></Icon>
+        <Icon iconName="item/transport-belt.png" qualityLevel={2}></Icon>
+        <Icon iconName="item/transport-belt.png" qualityLevel={3}></Icon>
+        <Icon iconName="item/transport-belt.png" qualityLevel={5}></Icon>
+      </div>
+    </>
+  );
+}
+
+export default ComponentTests;

+ 31 - 0
src/Layout.tsx

@@ -0,0 +1,31 @@
+import { AppBar, Box, Tab, Tabs, Toolbar, Typography } from "@mui/material";
+import { Outlet, useLocation, useNavigate } from "react-router";
+
+const NAV_ITEMS = [
+  { label: "Clock Builder", path: "/" },
+  { label: "Component Tests", path: "/tests" },
+];
+
+export default function Layout() {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const current = NAV_ITEMS.find((i) => i.path === location.pathname)?.path ?? "/";
+
+  return (
+    <Box sx={{ flexGrow: 1, minHeight: "100vh", bgcolor: "background.default" }}>
+      <AppBar position="static">
+        <Toolbar variant="dense">
+          <Typography variant="subtitle1" sx={{ mr: 4, fontWeight: 700, color: "primary.main" }}>
+            Combinator Clock Builder
+          </Typography>
+          <Tabs value={current} onChange={(_, value) => navigate(value)}>
+            {NAV_ITEMS.map((item) => (
+              <Tab key={item.path} value={item.path} label={item.label} />
+            ))}
+          </Tabs>
+        </Toolbar>
+      </AppBar>
+      <Outlet />
+    </Box>
+  );
+}

+ 252 - 0
src/assets/ClockTimeline.module.css

@@ -0,0 +1,252 @@
+.wrap {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  background-color: #242324;
+  border: 1px solid #646464;
+  padding: 10px;
+}
+
+.palette {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  align-items: center;
+}
+.hint {
+  color: #999;
+  font-size: 11.5px;
+  margin-left: 4px;
+}
+.chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  background-color: #373737;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 4px 8px;
+  font-size: 12px;
+  cursor: grab;
+}
+.chip:active {
+  cursor: grabbing;
+}
+.chip:hover {
+  background-color: #e39827;
+  color: #1a1300;
+}
+.chipTicks {
+  font-family: ui-monospace, monospace;
+  color: #999;
+  font-size: 10.5px;
+}
+.chip:hover .chipTicks {
+  color: #1a1300;
+}
+
+.ruler {
+  position: relative;
+  height: 20px;
+  border-bottom: 1px solid #646464;
+}
+.rulerTick {
+  position: absolute;
+  top: 0;
+  transform: translateX(-50%);
+  font-family: ui-monospace, monospace;
+  font-size: 10px;
+  color: #999;
+}
+.rulerReset {
+  position: absolute;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  width: 2px;
+  background: #f1be64;
+}
+
+.rows {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+.rowBlockWrap {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.rowHeader {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.rowNameInput {
+  background: #373737;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 3px 8px;
+  font-size: 12px;
+  width: 110px;
+}
+.rowNameInput:focus {
+  outline: 2px solid #f1be64;
+  outline-offset: 1px;
+}
+.removeBtn {
+  margin-left: auto;
+  background: none;
+  border: none;
+  color: #999;
+  cursor: pointer;
+}
+.removeBtn:hover {
+  color: #d9614f;
+}
+
+.lane {
+  position: relative;
+  background-color: #1a1a1a;
+  border: 1px solid #3a3a3a;
+  border-radius: 4px;
+  touch-action: none;
+  transition: height 0.12s ease;
+}
+
+.block {
+  position: absolute;
+  min-width: 22px;
+  background-color: #373737;
+  border: 1px solid #f9b44b;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  user-select: none;
+  overflow: hidden;
+}
+.block:active {
+  cursor: grabbing;
+}
+.blockSelected {
+  border-color: #f1be64;
+  box-shadow:
+    0 0 0 2px #f1be64,
+    0 0 6px 1px #f9b44b;
+}
+.blockIcons {
+  display: flex;
+  align-items: center;
+  gap: 1px;
+  pointer-events: none;
+}
+.resizeHandle {
+  position: absolute;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  width: 6px;
+  cursor: ew-resize;
+  background: rgba(249, 180, 75, 0.35);
+}
+
+.addRowBtn {
+  align-self: flex-start;
+  background: none;
+  border: 1px dashed #646464;
+  color: #999;
+  border-radius: 4px;
+  padding: 6px 12px;
+  font-size: 12px;
+  cursor: pointer;
+}
+.addRowBtn:hover {
+  border-color: #f1be64;
+  color: #f1be64;
+}
+.rowThroughput {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 11px;
+  color: #999;
+}
+.rowThroughput input {
+  width: 44px;
+  background: #373737;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 2px 4px;
+  font-size: 11px;
+}
+.throughputValue {
+  margin-left: auto;
+  font-family: ui-monospace, monospace;
+  color: #f1be64;
+}
+.rows {
+  position: relative; /* anchors the alignment guide across all rows */
+}
+.alignGuide {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 2px;
+  background: #34be3c;
+  box-shadow: 0 0 4px #34be3c;
+  z-index: 5;
+  pointer-events: none;
+}
+.blockAligned {
+  border-color: #34be3c;
+  box-shadow: 0 0 0 2px #34be3c;
+}
+
+.signalList {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.signalChip {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+}
+.signalRemove {
+  background: none;
+  border: none;
+  color: #999;
+  font-size: 10px;
+  cursor: pointer;
+}
+.signalRemove:hover {
+  color: #d9614f;
+}
+
+.autoFillPreset {
+  background-color: #373737;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 3px 6px;
+  font-size: 11px;
+}
+.autoFillBtn {
+  background-color: #5eb663;
+  border: none;
+  color: #000;
+  font-weight: 600;
+  border-radius: 4px;
+  padding: 4px 8px;
+  font-size: 11px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+.autoFillBtn:hover {
+  background-color: #92e897;
+}

+ 380 - 0
src/assets/ClockTimeline.tsx

@@ -0,0 +1,380 @@
+import { useCallback, useMemo, useRef, useState } from "react";
+import Icon from "./icon";
+import SelectSignal, { type Signal } from "./SelectSignal";
+import styles from "./ClockTimeline.module.css";
+import { ACTION_PRESETS, expandBlockInstances, getPreset, type ClockBlock, type ClockRow } from "./types";
+import ExpressionInput from "./components/ExpressionInpux";
+
+type Props = {
+  duration: number;
+  rows: ClockRow[];
+  blocks: ClockBlock[];
+  selectedBlockIds: Set<string>;
+  onRowsChange: (rows: ClockRow[]) => void;
+  onBlocksChange: (blocks: ClockBlock[]) => void;
+  onSelectBlocks: (ids: Set<string>) => void;
+};
+
+const LANE_HEIGHT = 30;
+const LANE_GAP = 4;
+export const defaultActivationSignal = {
+  type: "virtual-signal",
+  name: "signal-check",
+  subgroup: "virtual-signal",
+  icon: "virtual-signal/signal-check.png",
+  order: "a[checked]",
+};
+let uid = 0;
+const nextId = (prefix: string) => `${prefix}-${++uid}-${Date.now()}`;
+type PackedInstance = { id: string; blockId: string; start: number; duration: number; lane: number };
+
+function packLanes(instances: { id: string; blockId: string; start: number; duration: number }[]) {
+  const sorted = [...instances].sort((a, b) => a.start - b.start);
+  const laneEnds: number[] = [];
+  const packed: PackedInstance[] = [];
+  for (const inst of sorted) {
+    let lane = laneEnds.findIndex((end) => end <= inst.start);
+    if (lane === -1) {
+      lane = laneEnds.length;
+      laneEnds.push(inst.start + inst.duration);
+    } else {
+      laneEnds[lane] = inst.start + inst.duration;
+    }
+    packed.push({ ...inst, lane });
+  }
+  return { packed, laneCount: Math.max(1, laneEnds.length) };
+}
+
+export default function ClockTimeline({
+  duration,
+  rows,
+  blocks,
+  selectedBlockIds,
+  onRowsChange,
+  onBlocksChange,
+  onSelectBlocks,
+}: Props) {
+  const laneRefs = useRef<Record<string, HTMLDivElement | null>>({});
+  const [alignmentTick, setAlignmentTick] = useState<number | null>(null);
+  const dragState = useRef<{
+    blockId: string;
+    mode: "move" | "resize";
+    startX: number;
+    laneWidth: number;
+    shiftKey: boolean;
+    wasSelected: boolean;
+    groupOrigStarts: Map<string, number>; // blockId -> original start, for every block moving together
+    origDuration: number;
+  } | null>(null);
+
+  const pxToTick = useCallback((laneWidth: number, px: number) => Math.round((px / laneWidth) * duration), [duration]);
+
+  const addRow = () => {
+    const row: ClockRow = {
+      id: nextId("row"),
+      name: `Row ${rows.length + 1}`,
+      signals: [defaultActivationSignal],
+      stackSize: 16,
+      inserterCount: 1,
+    };
+    onRowsChange([...rows, row]);
+  };
+
+  const removeRow = (rowId: string) => {
+    onRowsChange(rows.filter((r) => r.id !== rowId));
+    const removedIds = new Set(blocks.filter((b) => b.rowId === rowId).map((b) => b.id));
+    onBlocksChange(blocks.filter((b) => b.rowId !== rowId));
+    if ([...selectedBlockIds].some((id) => removedIds.has(id))) {
+      onSelectBlocks(new Set([...selectedBlockIds].filter((id) => !removedIds.has(id))));
+    }
+  };
+
+  const updateRow = (rowId: string, patch: Partial<ClockRow>) => {
+    onRowsChange(rows.map((r) => (r.id === rowId ? { ...r, ...patch } : r)));
+  };
+
+  const updateRowSignal = (rowId: string, index: number, signal: Signal | null) => {
+    if (!signal) return;
+    const row = rows.find((r) => r.id === rowId);
+    if (!row) return;
+    const next = [...row.signals];
+    next[index] = signal;
+    updateRow(rowId, { signals: next });
+  };
+  const addRowSignal = (rowId: string, signal: Signal | null) => {
+    if (!signal) return;
+    const row = rows.find((r) => r.id === rowId);
+    if (!row) return;
+    updateRow(rowId, { signals: [...row.signals, signal] });
+  };
+  const removeRowSignal = (rowId: string, index: number) => {
+    const row = rows.find((r) => r.id === rowId);
+    if (!row || row.signals.length <= 1) return; // keep at least one
+    updateRow(rowId, { signals: row.signals.filter((_, i) => i !== index) });
+  };
+
+  const autoFillRow = (rowId: string, presetId: string) => {
+    const row = rows.find((r) => r.id === rowId);
+    if (!row) return;
+    const preset = getPreset(presetId);
+    const count = Math.max(1, row.inserterCount);
+    const spacing = duration / count;
+    const generated: ClockBlock[] = Array.from({ length: count }, (_, i) => ({
+      id: nextId("block"),
+      rowId,
+      presetId,
+      start: Math.round(i * spacing),
+      duration: preset.ticks + 1,
+      count: 1,
+    }));
+    onBlocksChange([...blocks.filter((b) => b.rowId !== rowId), ...generated]);
+  };
+
+  const onPaletteDragStart = (e: React.DragEvent, presetId: string) => {
+    e.dataTransfer.setData("text/plain", presetId);
+    e.dataTransfer.effectAllowed = "copy";
+  };
+
+  const onLaneDrop = (e: React.DragEvent, rowId: string) => {
+    e.preventDefault();
+    const presetId = e.dataTransfer.getData("text/plain");
+    const preset = ACTION_PRESETS.find((p) => p.id === presetId);
+    const lane = laneRefs.current[rowId];
+    if (!preset || !lane) return;
+    const rect = lane.getBoundingClientRect();
+    const tick = Math.max(0, Math.min(duration - 1, pxToTick(rect.width, e.clientX - rect.left)));
+    const block: ClockBlock = {
+      id: nextId("block"),
+      rowId,
+      presetId: preset.id,
+      start: tick,
+      duration: preset.ticks + 1,
+      count: 1,
+    };
+    onBlocksChange([...blocks, block]);
+    onSelectBlocks(new Set([block.id]));
+  };
+  const onPointerDownBlock = (e: React.PointerEvent, block: ClockBlock, mode: "move" | "resize") => {
+    e.stopPropagation();
+    const lane = laneRefs.current[block.rowId];
+    if (!lane) return;
+    (e.target as HTMLElement).setPointerCapture(e.pointerId);
+
+    const wasSelected = selectedBlockIds.has(block.id);
+    const groupIds = mode === "move" && wasSelected ? selectedBlockIds : new Set([block.id]);
+
+    dragState.current = {
+      blockId: block.id,
+      mode,
+      startX: e.clientX,
+      laneWidth: lane.clientWidth,
+      shiftKey: e.shiftKey,
+      wasSelected,
+      groupOrigStarts: new Map(blocks.filter((b) => groupIds.has(b.id)).map((b) => [b.id, b.start])),
+      origDuration: block.duration,
+    };
+  };
+
+  const onPointerMove = (e: React.PointerEvent) => {
+    const drag = dragState.current;
+    if (!drag) return;
+    const deltaTicks = pxToTick(drag.laneWidth, e.clientX - drag.startX);
+
+    if (drag.mode === "resize") {
+      updateBlockDuration(drag.blockId, Math.max(1, drag.origDuration + deltaTicks));
+      setAlignmentTick(null);
+      return;
+    }
+
+    // Group move: apply the same delta to every block's original start,
+    // clamped as a group so none of them fall off either edge.
+    const patched = new Map<string, number>();
+    let clampDelta = deltaTicks;
+    drag.groupOrigStarts.forEach((origStart) => {
+      const proposed = origStart + clampDelta;
+      if (proposed < 0) clampDelta = Math.max(clampDelta, -origStart);
+      if (proposed > duration - 1) clampDelta = Math.min(clampDelta, duration - 1 - origStart);
+    });
+    drag.groupOrigStarts.forEach((origStart, blockId) => patched.set(blockId, origStart + clampDelta));
+
+    onBlocksChange(blocks.map((b) => (patched.has(b.id) ? { ...b, start: patched.get(b.id)! } : b)));
+
+    // Alignment guide: does the primary dragged block now share a start tick
+    // with any block outside the moving group?
+    const draggedNewStart = patched.get(drag.blockId);
+    const match = blocks.some((b) => !drag.groupOrigStarts.has(b.id) && b.start === draggedNewStart);
+    setAlignmentTick(match && draggedNewStart !== undefined ? draggedNewStart : null);
+  };
+
+  const updateBlockDuration = (blockId: string, newDuration: number) => {
+    onBlocksChange(blocks.map((b) => (b.id === blockId ? { ...b, duration: newDuration } : b)));
+  };
+
+  const onPointerUp = (e: React.PointerEvent) => {
+    const drag = dragState.current;
+    dragState.current = null;
+    setAlignmentTick(null);
+    if (!drag) return;
+
+    const moved = Math.abs(e.clientX - drag.startX) >= 3;
+
+    if (drag.mode === "move" && !moved) {
+      // Tap — resolve as a selection change.
+      if (drag.shiftKey) {
+        const next = new Set(selectedBlockIds);
+        next.has(drag.blockId) ? next.delete(drag.blockId) : next.add(drag.blockId);
+        onSelectBlocks(next);
+      } else {
+        onSelectBlocks(new Set([drag.blockId]));
+      }
+    } else if (drag.mode === "move" && moved && !drag.wasSelected) {
+      // Actually dragged a block that wasn't part of the prior selection — it becomes the selection.
+      onSelectBlocks(new Set([drag.blockId]));
+    }
+  };
+
+  const majorTicks = useMemo(() => {
+    const step = Math.max(1, Math.round(duration / 10));
+    const out: number[] = [];
+    for (let t = 0; t <= duration; t += step) out.push(t);
+    return out;
+  }, [duration]);
+
+  return (
+    <div className={styles.wrap}>
+      <div className={styles.palette}>
+        {ACTION_PRESETS.map((p) => (
+          <div key={p.id} className={styles.chip} draggable onDragStart={(e) => onPaletteDragStart(e, p.id)}>
+            {p.fromItem && <Icon iconName={`item/${p.fromItem}.png`} size={20} />}
+            <span>{p.label}</span>
+            <span className={styles.chipTicks}>{p.ticks}t</span>
+          </div>
+        ))}
+        <span className={styles.hint}>Drag onto a row · Shift-click to multi-select · Ctrl+D to duplicate</span>
+      </div>
+
+      <div className={styles.ruler}>
+        {majorTicks.map((t) => (
+          <div key={t} className={styles.rulerTick} style={{ left: `${(t / duration) * 100}%` }}>
+            <span>{t}</span>
+          </div>
+        ))}
+        <div className={styles.rulerReset} />
+      </div>
+
+      <div className={styles.rows}>
+        {alignmentTick !== null && (
+          <div className={styles.alignGuide} style={{ left: `${(alignmentTick / duration) * 100}%` }} />
+        )}
+
+        {rows.map((row) => {
+          const rowBlocks = blocks.filter((b) => b.rowId === row.id);
+          const allInstances = rowBlocks.flatMap((b) => expandBlockInstances(b));
+          const { packed, laneCount } = packLanes(allInstances);
+          const laneAreaHeight = laneCount * LANE_HEIGHT + (laneCount - 1) * LANE_GAP + 8;
+
+          return (
+            <div key={row.id} className={styles.rowBlockWrap}>
+              <div className={styles.rowHeader}>
+                <input
+                  className={styles.rowNameInput}
+                  value={row.name}
+                  onChange={(e) => updateRow(row.id, { name: e.target.value })}
+                />
+                <div className={styles.signalList}>
+                  {row.signals.map((s, i) => (
+                    <div key={`${s.name}-${i}`} className={styles.signalChip}>
+                      <SelectSignal value={s.name} onSelectSignal={(_, sig) => updateRowSignal(row.id, i, sig)} />
+                      {row.signals.length > 1 && (
+                        <button className={styles.signalRemove} onClick={() => removeRowSignal(row.id, i)}>
+                          ✕
+                        </button>
+                      )}
+                    </div>
+                  ))}
+                  <SelectSignal key={row.signals.length} onSelectSignal={(_, sig) => addRowSignal(row.id, sig)} />
+                </div>
+                <div className={styles.rowStats}>
+                  <label>Stack</label>
+                  <ExpressionInput
+                    value={row.stackSize}
+                    min={1}
+                    onCommit={(v) => updateRow(row.id, { stackSize: v })}
+                  />
+                  <label>Inserters</label>
+                  <ExpressionInput
+                    value={row.inserterCount}
+                    min={1}
+                    onCommit={(v) => updateRow(row.id, { inserterCount: v })}
+                  />
+                </div>
+                <select className={styles.autoFillPreset} defaultValue="chest_to_belt" id={`autofill-preset-${row.id}`}>
+                  {ACTION_PRESETS.filter((p) => p.id !== "custom").map((p) => (
+                    <option key={p.id} value={p.id}>
+                      {p.label}
+                    </option>
+                  ))}
+                </select>
+                <button
+                  className={styles.autoFillBtn}
+                  onClick={() => {
+                    const select = document.getElementById(`autofill-preset-${row.id}`) as HTMLSelectElement | null;
+                    autoFillRow(row.id, select?.value ?? "chest_to_belt");
+                  }}
+                >
+                  Auto-fill ({row.inserterCount})
+                </button>
+                <button className={styles.removeBtn} onClick={() => removeRow(row.id)}>
+                  ✕
+                </button>
+              </div>
+              <div
+                className={styles.lane}
+                style={{ height: laneAreaHeight }}
+                ref={(el) => (laneRefs.current[row.id] = el)}
+                onDragOver={(e) => e.preventDefault()}
+                onDrop={(e) => onLaneDrop(e, row.id)}
+                onPointerMove={onPointerMove}
+                onPointerUp={onPointerUp}
+              >
+                {packed.map((inst) => {
+                  const block = rowBlocks.find((b) => b.id === inst.blockId)!;
+                  const preset = getPreset(block.presetId);
+                  const left = (inst.start / duration) * 100;
+                  const width = (inst.duration / duration) * 100;
+                  const selected = selectedBlockIds.has(block.id);
+                  const aligned = alignmentTick !== null && inst.start === alignmentTick;
+                  return (
+                    <div
+                      key={inst.id}
+                      className={`${styles.block} ${selected ? styles.blockSelected : ""} ${aligned ? styles.blockAligned : ""}`}
+                      style={{
+                        left: `${left}%`,
+                        width: `${width}%`,
+                        top: inst.lane * (LANE_HEIGHT + LANE_GAP) + 4,
+                        height: LANE_HEIGHT,
+                      }}
+                      onPointerDown={(e) => onPointerDownBlock(e, block, "move")}
+                    >
+                      <div className={styles.blockIcons}>
+                        {preset.fromItem && <Icon iconName={`item/${preset.fromItem}.png`} size={18} />}
+                        {preset.toItem && <Icon iconName={`item/${preset.toItem}.png`} size={18} />}
+                      </div>
+                      <div
+                        className={styles.resizeHandle}
+                        onPointerDown={(e) => onPointerDownBlock(e, block, "resize")}
+                      />
+                    </div>
+                  );
+                })}
+              </div>
+            </div>
+          );
+        })}
+        <button className={styles.addRowBtn} onClick={addRow}>
+          + Add row
+        </button>
+      </div>
+    </div>
+  );
+}

+ 9 - 0
src/assets/Icon.module.css

@@ -0,0 +1,9 @@
+.factorio-icon {
+  display: inline-block;
+  position: relative;
+}
+.factorio-icon .factorio-icon {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+}

+ 4 - 0
src/assets/MachineSelector.module.css

@@ -0,0 +1,4 @@
+.machine-spec-recipe{
+    border: 1px solid black;
+    color: #000;
+}

+ 93 - 0
src/assets/MachineSelector.tsx

@@ -0,0 +1,93 @@
+import React, { useCallback, useEffect, useMemo, useState, type CSSProperties } from "react";
+import data from "../assets/data/2.0/data.json";
+import styles from "./MachineSelector.module.css";
+import Icon from "./icon";
+import { Autocomplete, Box, Popper, TextField } from "@mui/material";
+import SelectMenu from "./SelectFactorioMenu";
+import type { Recipe } from "../../scripts/factorio-dump/helpers/recipes.helper";
+import Tooltip from "./Tooltip";
+import type { Machine } from "../../scripts/factorio-dump/process-data.models";
+
+const recipeList = data.recipeGroup.flatMap((r) => r.subGroup.flatMap((s) => (s.children ?? []) as Recipe[]));
+const machines = data.machines as Machine[];
+type MachineSelectorProps = {
+  className?: string;
+  style?: CSSProperties;
+  onSelectMachine?: (itemName: string) => void;
+};
+function MachineSelector({ style, className, onSelectMachine }: MachineSelectorProps) {
+  const [machine, setMachine] = React.useState<Machine | null>(null);
+  const [recipe, setRecipe] = useState<null | Recipe>(null);
+  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
+  const [showMenuRecipe, setShowMenuRecipe] = useState(false);
+
+  const machineOptions = useMemo(() => {
+    if (recipe == null) return [];
+    const categories = [recipe.category];
+    if (recipe.additional_categories) categories.push(...recipe.additional_categories);
+    return machines.filter((m) => m.crafting_categories.some((c) => categories.includes(c)));
+  }, [recipe]);
+  useEffect(() => {
+    if (machineOptions.length == 1) setMachine(machineOptions[0]);
+    if (machine && !machineOptions.includes(machine)) setMachine(null);
+  }, [machineOptions]);
+
+  const onOpenRecipe = useCallback(
+    (event: React.MouseEvent<HTMLElement>) => {
+      setAnchorEl(anchorEl ? null : event.currentTarget);
+      setShowMenuRecipe(!showMenuRecipe);
+    },
+    [setAnchorEl, setShowMenuRecipe],
+  );
+  const onSelectRecipe = useCallback(
+    (name: string) => {
+      setRecipe(recipeList.find((r) => r.name == name) ?? null);
+      setAnchorEl(null);
+      setShowMenuRecipe(false);
+    },
+    [setShowMenuRecipe, setRecipe],
+  );
+
+  return (
+    <div className={className} style={style}>
+      <div onClick={onOpenRecipe} className={styles.machineSpecRecipe}>
+        {recipe == null ? (
+          <>Select recipe</>
+        ) : (
+          <Tooltip title={recipe.name}>
+            <Icon iconName={recipe.icon ?? ""} size={30} />
+          </Tooltip>
+        )}
+      </div>
+      <Autocomplete
+        value={machine}
+        onChange={(_: any, newValue: Machine | null) => {
+          setMachine(newValue);
+        }}
+        disablePortal
+        options={machineOptions}
+        sx={{ width: 300 }}
+        getOptionLabel={(option) => option.name}
+        renderInput={(params: any) => <TextField {...params} label="Machine" />}
+        renderOption={(props: any, option: Machine) => {
+          const { key, ...optionProps } = props;
+          return (
+            <Box key={key} component="li" {...optionProps}>
+              <Icon iconName={option.icon ?? ""} size={30} />
+              {option.name}
+            </Box>
+          );
+        }}
+      />
+      <Popper open={showMenuRecipe} anchorEl={anchorEl} placement="right">
+        <SelectMenu
+          title="Select recipe"
+          categories={data.recipeGroup}
+          onSelectItem={onSelectRecipe}
+          onClose={() => setShowMenuRecipe(false)}
+        ></SelectMenu>
+      </Popper>
+    </div>
+  );
+}
+export default MachineSelector;

+ 0 - 0
src/assets/ModuleSelector.module.css


+ 16 - 0
src/assets/ModuleSelector.tsx

@@ -0,0 +1,16 @@
+import type { CSSProperties } from "@mui/material";
+import { useState } from "react";
+
+type ModuleSelectorProps = {
+  className?: string;
+  style?: CSSProperties;
+  moduleCount: number;
+  allowedEffects: string[];
+  onSelectModule?: (itemName: string) => void;
+};
+function ModuleSelector({ style, className, onSelectModule }: ModuleSelectorProps) {
+  const [selectedModules, setSelectedModule] = useState<string[]>([]);
+
+  return;
+  <div className={className} style={style}></div>;
+}

+ 169 - 156
src/assets/SelectRecipeMenu.module.css → src/assets/SelectFactorioMenu.module.css

@@ -1,156 +1,169 @@
-.select-menu {
-  width: 382px;
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-  padding: 8px;
-  background: #646464;
-  background-color: #313031;
-}
-
-.select-menu-header {
-  display: flex;
-  flex-direction: row;
-}
-.select-menu-header-title {
-  font-weight: 700;
-  font-stretch: 100%;
-  font-size: 19.2px;
-  color: #ffe6c0;
-}
-.select-menu-header-spacer {
-  flex-grow: 1;
-  cursor: grab;
-  margin: 2px 6px;
-  background: repeating-linear-gradient(
-    90deg,
-    #2b2b2b,
-    #2b2b2b 3px,
-    rgb(56, 55, 56) 3px,
-    rgb(56, 55, 56) 6px
-  );
-  box-shadow: inset 0 -4px 5px -2px #313031;
-}
-.select-menu-header-action {
-  display: inline-flex;
-  gap: 4px;
-  font-size: 12px;
-}
-.select-menu-header-action input[type="search"] {
-  font-size: 12px;
-  line-height: 1.1rem;
-  height: 1.5rem;
-}
-.select-menu-header-action img {
-  height: 18px;
-  aspect-ratio: 1;
-}
-.select-menu-group {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-}
-.select-menu-group-icon {
-  cursor: pointer;
-  background-color: rgb(140, 140, 140);
-  border: 1px solid #646464;
-}
-.select-menu-group-icon.active {
-  background-color: #f1be64;
-}
-.select-menu-group-icon:hover {
-  background-color: #e39827;
-  filter: drop-shadow(0 0 2px #f9b44b);
-}
-.select-menu-content {
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-  background-color: #242324;
-  padding: 2px;
-  max-height: 300px;
-  overflow-y: auto;
-}
-.select-menu-content-row {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  gap: 2px;
-}
-.select-menu-slot {
-  position: relative;
-  height: 36px;
-  width: 36px;
-  display: inline-flex;
-}
-.select-menu-icon {
-  cursor: pointer;
-  background-color: #373737;
-  border-radius: 4px;
-  height: 36px;
-  width: 36px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  box-shadow:
-    inset 0px -4px 0.5px -2px #121212,
-    inset 5px 0px 2px -2.5px #1f1f1f,
-    inset -3px 0px 1px -1.5px #292929,
-    inset 0px 4px 1px -2px #515050;
-}
-.select-menu-icon:hover,
-.select-menu-icon.hover {
-  color: #000;
-  outline: 0;
-  box-shadow:
-    inset 8px 0px 4px -8px #000,
-    inset -8px 0px 4px -8px #000,
-    inset 0px 9px 2px -8px #fff,
-    inset 0px 8px 4px -8px #000,
-    inset 0px -8px 4px -8px #000,
-    inset 0px -9px 2px -8px #432400,
-    0px 0px 4px 0px #000,
-    inset 0px 0px 4px 2px #f9b44b;
-  background-color: #e39827;
-  filter: drop-shadow(0 0 2px #f9b44b);
-}
-.select-menu-icon.active {
-  box-shadow:
-    inset 0px 10px 2px -8px #000,
-    inset 0px 9px 2px -8px #000,
-    inset 8px 0px 4px -8px #563a10,
-    inset 8px 0px 4px -8px #563a10,
-    inset -8px 0px 4px -8px #563a10,
-    inset -8px 0px 4px -8px #563a10,
-    inset 0px 9px 2px -8px #563a10,
-    inset 0px -9px 2px -8px #563a10,
-    inset 0px -8.5px 0px -8px #563a10,
-    0px 0px 4px 0px #000;
-  background-color: #f1be64;
-}
-
-.select-menu-slot-empty {
-  position: relative;
-  z-index: 0;
-  width: 100%;
-  margin: 8px;
-  background-color: #242324;
-  box-shadow: 0px 0px 2px #131112;
-}
-.select-menu-slot-empty::before {
-  position: relative;
-  content: "";
-  display: block;
-  width: 100%;
-  height: 0px;
-  box-shadow: 0px -0.5px 1.5px 1px #555250;
-}
-.select-menu-slot-empty::after {
-  position: relative;
-  content: "";
-  display: block;
-  width: 100%;
-  height: 0px;
-  margin-top: 100%;
-  box-shadow: 0px 0.5px 1.5px 1px #161414;
-}
+.select-menu {
+  width: 400px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 8px;
+  background: #646464;
+  background-color: #313031;
+}
+
+.select-menu-header {
+  display: flex;
+  flex-direction: row;
+}
+.select-menu-header-title {
+  font-weight: 700;
+  font-stretch: 100%;
+  font-size: 19.2px;
+  color: #ffe6c0;
+}
+.select-menu-header-spacer {
+  flex-grow: 1;
+  cursor: grab;
+  margin: 2px 6px;
+  background: repeating-linear-gradient(90deg, #2b2b2b, #2b2b2b 3px, rgb(56, 55, 56) 3px, rgb(56, 55, 56) 6px);
+  box-shadow: inset 0 -4px 5px -2px #313031;
+}
+.select-menu-header-action {
+  display: inline-flex;
+  gap: 4px;
+  font-size: 12px;
+}
+.select-menu-header-action input[type="search"] {
+  font-size: 12px;
+  line-height: 1.1rem;
+  height: 1.5rem;
+}
+.select-menu-header-action img {
+  height: 18px;
+  aspect-ratio: 1;
+}
+.select-menu-group {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+.select-menu-group-icon {
+  cursor: pointer;
+  background-color: rgb(140, 140, 140);
+  border: 1px solid #646464;
+}
+.select-menu-group-icon.active {
+  background-color: #f1be64;
+}
+.select-menu-group-icon:hover {
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.select-menu-content {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  background-color: #242324;
+  padding: 2px;
+  max-height: 300px;
+}
+.select-menu-content-row {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 2px;
+}
+.select-menu-slot {
+  position: relative;
+  height: 36px;
+  width: 36px;
+  display: inline-flex;
+}
+
+.quality-menu-icon,
+.select-menu-icon {
+  cursor: pointer;
+  background-color: #373737;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow:
+    inset 0px -4px 0.5px -2px #121212,
+    inset 5px 0px 2px -2.5px #1f1f1f,
+    inset -3px 0px 1px -1.5px #292929,
+    inset 0px 4px 1px -2px #515050;
+}
+.select-menu-icon {
+  height: 36px;
+  width: 36px;
+}
+.quality-menu-icon {
+  height: 30px;
+  width: 30px;
+}
+.quality-menu-icon:hover,
+.select-menu-icon:hover,
+.select-menu-icon.hover {
+  color: #000;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.quality-menu-icon.active,
+.select-menu-icon.active {
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px 9px 2px -8px #563a10,
+    inset 0px -9px 2px -8px #563a10,
+    inset 0px -8.5px 0px -8px #563a10,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+}
+
+.select-menu-slot-empty {
+  position: relative;
+  z-index: 0;
+  width: 100%;
+  margin: 8px;
+  background-color: #242324;
+  box-shadow: 0px 0px 2px #131112;
+}
+.select-menu-slot-empty::before {
+  position: relative;
+  content: "";
+  display: block;
+  width: 100%;
+  height: 0px;
+  box-shadow: 0px -0.5px 1.5px 1px #555250;
+}
+.select-menu-slot-empty::after {
+  position: relative;
+  content: "";
+  display: block;
+  width: 100%;
+  height: 0px;
+  margin-top: 100%;
+  box-shadow: 0px 0.5px 1.5px 1px #161414;
+}
+.quality-menu {
+  display: flex;
+  gap: 2px;
+  flex-direction: row;
+}
+.quality-menu-icon {
+  display: flex;
+  gap: 2px;
+  flex-direction: row;
+}

+ 188 - 174
src/assets/SelectRecipeMenu.tsx → src/assets/SelectFactorioMenu.tsx

@@ -1,174 +1,188 @@
-import { useState, type CSSProperties, useMemo } from "react";
-import Icon from "./icon";
-import styles from "./SelectRecipeMenu.module.css";
-import Tooltip from "./Tooltip";
-import SimpleBar from "simplebar-react";
-
-// Utility type definitions
-export type Category<T> = {
-  name: string;
-  icon: string;
-  subGroup: SubGroup<T>[];
-};
-export type SubGroup<T> = {
-  name: string;
-  order?: string;
-  children: Array<T>;
-};
-/**
- * Represents a single selectable item.
- * @property {string} name – Display name of the item.
- * @property {string} icon – Identifier of the icon to render.
- */
-export type MenuItem = { name: string; icon: string };
-
-interface SubGroupProps {
-  subgroup: SubGroup<MenuItem>;
-  selectedItem: string;
-  onSelectItem: (name: string) => void;
-}
-
-// Constants
-const ITEM_PER_ROW = 10;
-
-/**
- * Filters a {@link Category} based on a search string.
- * Only sub‑groups and items that contain *all* search terms are
- * retained. The function returns a **new** category object; the
- * original data is never mutated.
- *
- * @param {Category<MenuItem>} category - Original category to filter.
- * @param {string} search - User’s search query (space separated).
- * @returns {Category<MenuItem>} - A filtered copy of the category.
- */
-function filterCategory(category: Category<MenuItem>, search: string): Category<MenuItem> {
-  const searchKeys = search.toLowerCase().split(" ");
-
-  // Filter the children inside a single sub‑group
-  function filterSubgroupChildren(subgroup: SubGroup<MenuItem>): SubGroup<MenuItem> {
-    const children = subgroup.children.filter((item) => searchKeys.every((k) => item.name.includes(k)));
-    return {
-      ...subgroup,
-      children,
-    };
-  }
-  return {
-    ...category,
-    subGroup: category.subGroup.map(filterSubgroupChildren).filter((s) => s.children.length > 0),
-  };
-}
-
-/**
- * Renders a single row of icons representing a sub‑group’s items.
- *
- * Empty slots are rendered to keep the grid layout intact when the
- * number of items is not a multiple of {@link ITEM_PER_ROW}.
- *
- * @component
- * @param {SubGroupProps} props
- */
-function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
-  const emptySlotsCounts = useMemo(() => {
-    const lastRowSlots = subgroup.children.length % ITEM_PER_ROW;
-    return lastRowSlots > 0 ? ITEM_PER_ROW - lastRowSlots : 0;
-  }, [subgroup]);
-  return (
-    <div className={styles.selectMenuContentRow}>
-      {subgroup.children.map((o) => (
-        <Tooltip title={o.name} key={o.name}>
-          <div
-            className={`${styles.selectMenuIcon} ${o.name === selectedItem ? styles.active : ""}`}
-            onClick={() => onSelectItem(o.name)}
-          >
-            <Icon iconName={o.icon} size={30} />
-          </div>
-        </Tooltip>
-      ))}
-      {Array(emptySlotsCounts)
-        .fill(0)
-        .map((_, i) => (
-          <div className={styles.selectMenuSlot} key={i}>
-            <div className={styles.selectMenuSlotEmpty} />
-          </div>
-        ))}
-    </div>
-  );
-}
-
-/**
- * The main dropdown menu component. It renders:
- *   - A header with optional title and search bar
- *   - Category icons for switching between categories
- *   - A scrollable list of {@link SubGroupRow}s for the current
- *     category
- *
- * The component accepts an optional {@link onSelectItem} callback;
- * if omitted it simply updates internal state and returns the name
- * of the selected item via the `selectItem` state hook.
- *
- * @component
- * @param {SelectMenuProps} props
- */
-type SelectMenuProps = {
-  categories: Category<MenuItem>[];
-  className?: string;
-  title?: string;
-  style?: CSSProperties;
-  onClose?: () => void;
-  onSelectItem?: (itemName: string) => void;
-};
-function SelectMenu({ style, className, title, categories, onClose, onSelectItem }: SelectMenuProps) {
-  const [category, setCategory] = useState(categories[0].name);
-  const [item, selectItem] = useState("");
-  const [showSearch, setShowSearch] = useState(false);
-  const [search, setSearch] = useState("");
-
-  const activeCategory = useMemo(() => categories.find((r) => r.name == category), [category, categories]);
-  const activeOnSelect = useMemo(() => (onSelectItem ? onSelectItem : selectItem), [onSelectItem, selectItem]);
-  // Apply filtering only if a search is active
-  const filteredContent = useMemo(
-    () => (search && activeCategory && showSearch ? filterCategory(activeCategory, search) : activeCategory),
-    [activeCategory, search, showSearch]
-  );
-  const rootClassName = `${styles.selectMenu} ${className ?? ""}`;
-  console.log(styles);
-  return (
-    <div style={style} className={rootClassName}>
-      {/* Header */}
-      <div className={styles.selectMenuHeader}>
-        <div className={styles.selectMenuHeaderTitle}>{title ?? "Title"}</div>
-        <div className={styles.selectMenuHeaderSpacer} />
-        <div className={styles.selectMenuHeaderAction}>
-          {showSearch && <input type="search" onChange={(evt) => setSearch(evt.target.value)} value={search} />}
-          <button className={`panel-button ${showSearch ? "active" : ""}`} onClick={() => setShowSearch(!showSearch)}>
-            <img src="/assets/search.png" alt="Search" />
-          </button>
-          <button className="panel-button" onClick={onClose}>
-            <img src="/assets/close.png" alt="Close" />
-          </button>
-        </div>
-      </div>
-      {/* Category icons */}
-      <div className={styles.selectMenuGroup}>
-        {categories.map((g) => (
-          <div
-            key={g.name}
-            className={`${styles.selectMenuGroupIcon} ${g.name === category ? styles.active : ""}`}
-            onClick={() => setCategory(g.name)}
-          >
-            <Icon iconName={g.icon} size={62} />
-          </div>
-        ))}
-      </div>
-      {/* Scrollable content */}
-      <div className={styles.selectMenuContent}>
-        <SimpleBar style={{ maxHeight: 300 }}>
-          {filteredContent?.subGroup.map((subgroup) => (
-            <SubGroupRow key={subgroup.name} subgroup={subgroup} selectedItem={item} onSelectItem={activeOnSelect} />
-          ))}
-        </SimpleBar>
-      </div>
-    </div>
-  );
-}
-export default SelectMenu;
+import { useState, type CSSProperties, useMemo } from "react";
+import Icon from "./icon";
+import styles from "./SelectFactorioMenu.module.css";
+import Tooltip from "./Tooltip";
+import SimpleBar from "simplebar-react";
+
+import data from "../assets/data/2.0/data.json";
+
+const qualityLevels = data.qualityLevels;
+// Utility type definitions
+export type Category<T> = {
+  name: string;
+  icon: string;
+  subGroup: SubGroup<T>[];
+};
+export type SubGroup<T> = {
+  name: string;
+  order?: string;
+  children: Array<T>;
+};
+/**
+ * Represents a single selectable item.
+ * @property {string} name – Display name of the item.
+ * @property {string} icon – Identifier of the icon to render.
+ */
+export type MenuItem = { name: string; icon: string };
+
+interface SubGroupProps {
+  subgroup: SubGroup<MenuItem>;
+  selectedItem: string;
+  onSelectItem: (name: string) => void;
+}
+
+// Constants
+const ITEM_PER_ROW = 10;
+
+/**
+ * Filters a {@link Category} based on a search string.
+ * Only sub‑groups and items that contain *all* search terms are
+ * retained. The function returns a **new** category object; the
+ * original data is never mutated.
+ *
+ * @param {Category<MenuItem>} category - Original category to filter.
+ * @param {string} search - User’s search query (space separated).
+ * @returns {Category<MenuItem>} - A filtered copy of the category.
+ */
+function filterCategory(category: Category<MenuItem>, search: string): Category<MenuItem> {
+  const searchKeys = search.toLowerCase().split(" ");
+
+  // Filter the children inside a single sub‑group
+  function filterSubgroupChildren(subgroup: SubGroup<MenuItem>): SubGroup<MenuItem> {
+    const children = subgroup.children.filter((item) => searchKeys.every((k) => item.name.includes(k)));
+    return {
+      ...subgroup,
+      children,
+    };
+  }
+  return {
+    ...category,
+    subGroup: category.subGroup.map(filterSubgroupChildren).filter((s) => s.children.length > 0),
+  };
+}
+
+/**
+ * Renders a single row of icons representing a sub‑group’s items.
+ *
+ * Empty slots are rendered to keep the grid layout intact when the
+ * number of items is not a multiple of {@link ITEM_PER_ROW}.
+ *
+ * @component
+ * @param {SubGroupProps} props
+ */
+function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
+  const emptySlotsCounts = useMemo(() => {
+    const lastRowSlots = subgroup.children.length % ITEM_PER_ROW;
+    return lastRowSlots > 0 ? ITEM_PER_ROW - lastRowSlots : 0;
+  }, [subgroup]);
+  return (
+    <div className={styles.selectMenuContentRow}>
+      {subgroup.children.map((o) => (
+        <Tooltip title={o.name} key={o.name} enterDelay={800}>
+          <div
+            className={`${styles.selectMenuIcon} ${o.name === selectedItem ? styles.active : ""}`}
+            onClick={() => onSelectItem(o.name)}
+          >
+            <Icon iconName={o.icon} size={30} />
+          </div>
+        </Tooltip>
+      ))}
+      {Array(emptySlotsCounts)
+        .fill(0)
+        .map((_, i) => (
+          <div className={styles.selectMenuSlot} key={i}>
+            <div className={styles.selectMenuSlotEmpty} />
+          </div>
+        ))}
+    </div>
+  );
+}
+
+type SelectMenuProps = {
+  className?: string;
+  style?: CSSProperties;
+  categories: Category<MenuItem>[];
+  title?: string;
+  onClose?: () => void;
+  onSelectItem?: (itemName: string) => void;
+};
+/**
+ * The main dropdown menu component. It renders:
+ *   - A header with optional title and search bar
+ *   - Category icons for switching between categories
+ *   - A scrollable list of {@link SubGroupRow}s for the current
+ *     category
+ *
+ * The component accepts an optional {@link onSelectItem} callback;
+ * if omitted it simply updates internal state and returns the name
+ * of the selected item via the `selectItem` state hook.
+ *
+ * @component
+ * @param {SelectMenuProps} props
+ */
+function SelectMenu({ style, className, title, categories, onClose, onSelectItem }: SelectMenuProps) {
+  const [category, setCategory] = useState(categories[0].name);
+  const [item, selectItem] = useState("");
+  const [showSearch, setShowSearch] = useState(false);
+  const [search, setSearch] = useState("");
+  const [quality, setQuality] = useState("normal");
+
+  const activeCategory = useMemo(() => categories.find((r) => r.name == category), [category, categories]);
+  const activeOnSelect = useMemo(() => (onSelectItem ? onSelectItem : selectItem), [onSelectItem, selectItem]);
+  // Apply filtering only if a search is active
+  const filteredContent = useMemo(
+    () => (search && activeCategory && showSearch ? filterCategory(activeCategory, search) : activeCategory),
+    [activeCategory, search, showSearch],
+  );
+  const rootClassName = `${styles.selectMenu} ${className ?? ""}`;
+  return (
+    <div style={style} className={rootClassName}>
+      {/* Header */}
+      <div className={styles.selectMenuHeader}>
+        <div className={styles.selectMenuHeaderTitle}>{title ?? "Title"}</div>
+        <div className={styles.selectMenuHeaderSpacer} />
+        <div className={styles.selectMenuHeaderAction}>
+          {showSearch && <input type="search" onChange={(evt) => setSearch(evt.target.value)} value={search} />}
+          <button className={`panel-button ${showSearch ? "active" : ""}`} onClick={() => setShowSearch(!showSearch)}>
+            <img src="/assets/search.png" alt="Search" />
+          </button>
+          <button className="panel-button" onClick={onClose}>
+            <img src="/assets/close.png" alt="Close" />
+          </button>
+        </div>
+      </div>
+      {/* Category icons */}
+      <div className={styles.selectMenuGroup}>
+        {categories.map((g) => (
+          <div
+            key={g.name}
+            className={`${styles.selectMenuGroupIcon} ${g.name === category ? styles.active : ""}`}
+            onClick={() => setCategory(g.name)}
+          >
+            <Icon iconName={g.icon} size={62} />
+          </div>
+        ))}
+      </div>
+      {/* Scrollable content */}
+      <div className={styles.selectMenuContent}>
+        <SimpleBar style={{ maxHeight: 300 }}>
+          {filteredContent?.subGroup.map((subgroup) => (
+            <SubGroupRow key={subgroup.name} subgroup={subgroup} selectedItem={item} onSelectItem={activeOnSelect} />
+          ))}
+        </SimpleBar>
+      </div>
+      {/* Quality toolbar*/}
+      <div className={styles.qualityMenu}>
+        {qualityLevels.map((q) => (
+          <div
+            className={`${styles.qualityMenuIcon} ${q.name === quality ? styles.active : ""}`}
+            onClick={() => setQuality(q.name)}
+          >
+            <Icon iconName={q.icon} size={20} />
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
+export default SelectMenu;

+ 50 - 0
src/assets/SelectSignal.module.css

@@ -0,0 +1,50 @@
+.trigger {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  height: 32px;
+  min-width: 32px;
+  padding: 0 0px;
+  background-color: #8e8e8e;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  color: #000;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #e3e3e3,
+    inset 0px 10px 2px -8px #282828,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+}
+.trigger:hover {
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+}
+.trigger.open {
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px 9px 2px -8px #563a10,
+    inset 0px -9px 2px -8px #563a10,
+    inset 0px -8.5px 0px -8px #563a10,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+}
+.placeholder {
+  color: #2a2a2a;
+  white-space: nowrap;
+}

+ 74 - 0
src/assets/SelectSignal.tsx

@@ -0,0 +1,74 @@
+import React, { useCallback, useState, type CSSProperties } from "react";
+import data from "../assets/data/2.0/data.json";
+import styles from "./SelectSignal.module.css";
+import Icon from "./icon";
+import { Popper } from "@mui/material";
+import SelectMenu from "./SelectFactorioMenu";
+import Tooltip from "./Tooltip";
+
+const signals = data.signalGroup.flatMap((r) => r.subGroup.flatMap((s) => (s.children ?? []) as Signal[]));
+
+export type Signal = {
+  name: string;
+  type: string;
+  subgroup: string;
+  icon: string;
+  order: string;
+  quality?: string;
+};
+
+type SelectSignalProps = {
+  className?: string;
+  style?: CSSProperties;
+  value?: string;
+  onSelectSignal?: (itemName: string, signal: Signal | null) => void;
+};
+
+function SelectSignal({ style, className, value, onSelectSignal }: SelectSignalProps) {
+  const [internalSignal, setInternalSignal] = useState<null | Signal>(null);
+  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
+  const [showMenu, setShowMenu] = useState(false);
+
+  const signal = value !== undefined ? (signals.find((s) => s.name === value) ?? null) : internalSignal;
+
+  const onOpenMenu = useCallback(
+    (event: React.MouseEvent<HTMLElement>) => {
+      setAnchorEl(anchorEl ? null : event.currentTarget);
+      setShowMenu(!showMenu);
+    },
+    [anchorEl, showMenu],
+  );
+  const onSelectRecipe = useCallback(
+    (name: string) => {
+      const found = signals.find((o) => o.name === name) ?? null;
+      onSelectSignal && onSelectSignal(name, found);
+      setInternalSignal(found);
+      setAnchorEl(null);
+      setShowMenu(false);
+    },
+    [onSelectSignal],
+  );
+
+  return (
+    <div className={className} style={style}>
+      <button type="button" className={`${styles.trigger} ${showMenu ? styles.open : ""}`} onClick={onOpenMenu}>
+        {signal == null ? (
+          <span className={styles.placeholder}>Select signal</span>
+        ) : (
+          <Tooltip title={signal.name}>
+            <Icon iconName={signal.icon ?? ""} size={24} />
+          </Tooltip>
+        )}
+      </button>
+      <Popper open={showMenu} anchorEl={anchorEl} placement="right">
+        <SelectMenu
+          title="Select signal"
+          categories={data.signalGroup}
+          onSelectItem={onSelectRecipe}
+          onClose={() => setShowMenu(false)}
+        />
+      </Popper>
+    </div>
+  );
+}
+export default SelectSignal;

+ 19 - 19
src/assets/Tooltip.css

@@ -1,19 +1,19 @@
-.panel-tooltip-title {
-  background-color: #ffe6c0;
-  color: black;
-  font-weight: bold;
-  font-size: 107%;
-  word-wrap: normal;
-  word-break: keep-all;
-  border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYRpQ0NQSUNDIHByb2ZpbGUAACiRfZE9SMNAHMVfW6VSWhzaQcQhQ3Wy4BfiqFUoQoVQK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxNHJSdFFSvxfUmgR48FxP97de9y9A/zNKlPNnjFA1Swjk0oKufyqEHxFCFFEMI6oxEx9ThTT8Bxf9/Dx9S7Bs7zP/TkiSsFkgE8gnmW6YRFvEE9vWjrnfeIYK0sK8TnxqEEXJH7kuuzyG+eSw36eGTOymXniGLFQ6mK5i1nZUImniOOKqlG+P+eywnmLs1qts/Y9+QvDBW1lmes0h5DCIpYgQoCMOiqowkKCVo0UExnaT3r4Bx2/SC6ZXBUwciygBhWS4wf/g9/dmsXJCTcpnAR6X2z7YxgI7gKthm1/H9t26wQIPANXWsdfawIzn6Q3Olr8COjfBi6uO5q8B1zuAANPumRIjhSg6S8Wgfcz+qY8EL0FQmtub+19nD4AWeoqfQMcHAIjJcpe93h3X3dv/55p9/cDgjtyrWdvGYQAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmChMNMzGpuES2AAAA7VBMVEX/5sD8473437r13Ljy2bXs07Dr0q/oz6ziyafcw6PZwaHZwKDTvJzSu5zQuZrLtJbLtJXErpHAqo29qIu7pYi5o4a1ooi0oYexnoSynICxm3+tmoGsmX+ql36olXylk3ylk3qkk3uikXqij3agj3ifjXSii3KdjHWcjHabi3Wai3WdiHGZiXObiG+Zh3GXh3KYhGyXhG2UhG+Xg2yRf2mRfWaOfWiHdmCHclyCcFyCblp5aVV7aFN4Z1R1ZVJzY1ByYE5eUEBEPDFDOzFBOS8/Ny09Niw+NSo9NSs/NCohGxYQDgsIBgUBAQEAAAB0PBllAAAAAWJLR0RPbmZBSQAAAJxJREFUeF5lTkFOA0EMix3vUlClXjjzD/7/JCiI3RkHhVJVUF+cWHYcvM4xp6siAGYqBY+v6XIEwXyg5HHe57x4MgclfXxut9SaR+n0NvarZ1lOkj3GNRUYNmuzq9xo2ko9NCKiyWb8x70iEo3oywBIYSUN/nSBXCFSqvj9R73i6WV732cruRzXA3Two5/r4gFYFETcisol/ulH+hsBOWLTe3a33AAAAABJRU5ErkJggg==")
-    8/4px repeat;
-  margin: 0;
-  padding: 4px;
-}
-.panel-tooltip {
-  background-color: #313031;
-  border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYRpQ0NQSUNDIHByb2ZpbGUAACiRfZE9SMNAHMVfW6VSWhzaQcQhQ3Wy4BfiqFUoQoVQK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxNHJSdFFSvxfUmgR48FxP97de9y9A/zNKlPNnjFA1Swjk0oKufyqEHxFCFFEMI6oxEx9ThTT8Bxf9/Dx9S7Bs7zP/TkiSsFkgE8gnmW6YRFvEE9vWjrnfeIYK0sK8TnxqEEXJH7kuuzyG+eSw36eGTOymXniGLFQ6mK5i1nZUImniOOKqlG+P+eywnmLs1qts/Y9+QvDBW1lmes0h5DCIpYgQoCMOiqowkKCVo0UExnaT3r4Bx2/SC6ZXBUwciygBhWS4wf/g9/dmsXJCTcpnAR6X2z7YxgI7gKthm1/H9t26wQIPANXWsdfawIzn6Q3Olr8COjfBi6uO5q8B1zuAANPumRIjhSg6S8Wgfcz+qY8EL0FQmtub+19nD4AWeoqfQMcHAIjJcpe93h3X3dv/55p9/cDgjtyrWdvGYQAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmChMNNByjJo4EAAAAvVBMVEWBdGOBc2J/cmF7bV11aVt1aVpzZ1hxY1RsX1FhV01hV0xhVktfVUldUUVeUERWS0FJPDJHPDRDPThDPDhCOzZBOjQ6MSwxMDExMDAxLzAxLy8xLi8wLi4wLS0wLCwwLCswKyovKigzJyEsJyUsJiUuJSEsJSQoIR8lIiEnIR4lISEnIB0kIR8kIB4nHRojHRsjHBkjGxgjGhcgGhceFhQVDw0TDg0PDg0RDQwQDQwPDQ0MCQcFBAMDAgIAAAAndX8aAAAAAWJLR0Q/PmMwdQAAAIdJREFUeJxljrsOQkEUAoFlV43G1///o7E292CxV41KQTEFQzsCxBpKY28NVhAAIKguX29akknIposP7Z4P4Wlnj/MjlQCk2LudqtRKoKoYlSQVgEpScACsQ7OE3/wTc2o/bYgklXmHgilReP2RaG29SRUASJRpqpPLHG1JxYMC/DZVLH65lSerxj77wTd86AAAAABJRU5ErkJggg==")
-    8/4px repeat;
-  margin: 0;
-  padding: 4px;
-}
+.panel-tooltip-title {
+  background-color: #ffe6c0;
+  color: black;
+  font-weight: bold;
+  font-size: 107%;
+  word-wrap: normal;
+  word-break: keep-all;
+  border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYRpQ0NQSUNDIHByb2ZpbGUAACiRfZE9SMNAHMVfW6VSWhzaQcQhQ3Wy4BfiqFUoQoVQK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxNHJSdFFSvxfUmgR48FxP97de9y9A/zNKlPNnjFA1Swjk0oKufyqEHxFCFFEMI6oxEx9ThTT8Bxf9/Dx9S7Bs7zP/TkiSsFkgE8gnmW6YRFvEE9vWjrnfeIYK0sK8TnxqEEXJH7kuuzyG+eSw36eGTOymXniGLFQ6mK5i1nZUImniOOKqlG+P+eywnmLs1qts/Y9+QvDBW1lmes0h5DCIpYgQoCMOiqowkKCVo0UExnaT3r4Bx2/SC6ZXBUwciygBhWS4wf/g9/dmsXJCTcpnAR6X2z7YxgI7gKthm1/H9t26wQIPANXWsdfawIzn6Q3Olr8COjfBi6uO5q8B1zuAANPumRIjhSg6S8Wgfcz+qY8EL0FQmtub+19nD4AWeoqfQMcHAIjJcpe93h3X3dv/55p9/cDgjtyrWdvGYQAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmChMNMzGpuES2AAAA7VBMVEX/5sD8473437r13Ljy2bXs07Dr0q/oz6ziyafcw6PZwaHZwKDTvJzSu5zQuZrLtJbLtJXErpHAqo29qIu7pYi5o4a1ooi0oYexnoSynICxm3+tmoGsmX+ql36olXylk3ylk3qkk3uikXqij3agj3ifjXSii3KdjHWcjHabi3Wai3WdiHGZiXObiG+Zh3GXh3KYhGyXhG2UhG+Xg2yRf2mRfWaOfWiHdmCHclyCcFyCblp5aVV7aFN4Z1R1ZVJzY1ByYE5eUEBEPDFDOzFBOS8/Ny09Niw+NSo9NSs/NCohGxYQDgsIBgUBAQEAAAB0PBllAAAAAWJLR0RPbmZBSQAAAJxJREFUeF5lTkFOA0EMix3vUlClXjjzD/7/JCiI3RkHhVJVUF+cWHYcvM4xp6siAGYqBY+v6XIEwXyg5HHe57x4MgclfXxut9SaR+n0NvarZ1lOkj3GNRUYNmuzq9xo2ko9NCKiyWb8x70iEo3oywBIYSUN/nSBXCFSqvj9R73i6WV732cruRzXA3Two5/r4gFYFETcisol/ulH+hsBOWLTe3a33AAAAABJRU5ErkJggg==")
+    8/4px repeat;
+  margin: 0;
+  padding: 4px;
+}
+.panel-tooltip {
+  background-color: #313031;
+  border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYRpQ0NQSUNDIHByb2ZpbGUAACiRfZE9SMNAHMVfW6VSWhzaQcQhQ3Wy4BfiqFUoQoVQK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxNHJSdFFSvxfUmgR48FxP97de9y9A/zNKlPNnjFA1Swjk0oKufyqEHxFCFFEMI6oxEx9ThTT8Bxf9/Dx9S7Bs7zP/TkiSsFkgE8gnmW6YRFvEE9vWjrnfeIYK0sK8TnxqEEXJH7kuuzyG+eSw36eGTOymXniGLFQ6mK5i1nZUImniOOKqlG+P+eywnmLs1qts/Y9+QvDBW1lmes0h5DCIpYgQoCMOiqowkKCVo0UExnaT3r4Bx2/SC6ZXBUwciygBhWS4wf/g9/dmsXJCTcpnAR6X2z7YxgI7gKthm1/H9t26wQIPANXWsdfawIzn6Q3Olr8COjfBi6uO5q8B1zuAANPumRIjhSg6S8Wgfcz+qY8EL0FQmtub+19nD4AWeoqfQMcHAIjJcpe93h3X3dv/55p9/cDgjtyrWdvGYQAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmChMNNByjJo4EAAAAvVBMVEWBdGOBc2J/cmF7bV11aVt1aVpzZ1hxY1RsX1FhV01hV0xhVktfVUldUUVeUERWS0FJPDJHPDRDPThDPDhCOzZBOjQ6MSwxMDExMDAxLzAxLy8xLi8wLi4wLS0wLCwwLCswKyovKigzJyEsJyUsJiUuJSEsJSQoIR8lIiEnIR4lISEnIB0kIR8kIB4nHRojHRsjHBkjGxgjGhcgGhceFhQVDw0TDg0PDg0RDQwQDQwPDQ0MCQcFBAMDAgIAAAAndX8aAAAAAWJLR0Q/PmMwdQAAAIdJREFUeJxljrsOQkEUAoFlV43G1///o7E292CxV41KQTEFQzsCxBpKY28NVhAAIKguX29akknIposP7Z4P4Wlnj/MjlQCk2LudqtRKoKoYlSQVgEpScACsQ7OE3/wTc2o/bYgklXmHgilReP2RaG29SRUASJRpqpPLHG1JxYMC/DZVLH65lSerxj77wTd86AAAAABJRU5ErkJggg==")
+    8/4px repeat;
+  margin: 0;
+  padding: 4px;
+}

+ 39 - 48
src/assets/Tooltip.tsx

@@ -1,48 +1,39 @@
-import { styled } from "@mui/material/styles";
-import Tooltip, { tooltipClasses } from "@mui/material/Tooltip";
-import type { TooltipProps } from "@mui/material/Tooltip";
-import "./Tooltip.css";
-interface TooltipCustoProps {
-  title: string;
-  children: React.ReactElement;
-  content?: React.ReactNode;
-}
-const RawTooltip = styled(({ className, ...props }: TooltipProps) => (
-  <Tooltip {...props} classes={{ popper: className }} />
-))(({ theme }) => ({
-  [`& .${tooltipClasses.tooltip}`]: {
-    backgroundColor: "#f5f5f9",
-    color: "#fff",
-    maxWidth: 300,
-    fontSize: theme.typography.pxToRem(12),
-    border: "none",
-    padding: 0,
-    display: "flex",
-    flexDirection: "column",
-  },
-}));
-
-function TooltipFull({ title, children, content }: TooltipCustoProps) {
-  return (
-    <>
-      <RawTooltip
-        title={
-          <>
-            <div className="panel-tooltip-title">{title}</div>
-            {content && <div className="panel-tooltip">{content}</div>}
-          </>
-        }
-      >
-        {children}
-      </RawTooltip>
-    </>
-  );
-}
-export function TooltipSimple({ title, children }: TooltipCustoProps) {
-  return (
-    <>
-      <RawTooltip title={title}>{children}</RawTooltip>
-    </>
-  );
-}
-export default TooltipFull;
+import { styled } from "@mui/material/styles";
+import Tooltip, { tooltipClasses } from "@mui/material/Tooltip";
+import type { TooltipProps } from "@mui/material/Tooltip";
+import "./Tooltip.css";
+interface TooltipCustoProps {
+  content?: React.ReactNode;
+}
+const RawTooltip = styled(({ className, ...props }: TooltipProps) => (
+  <Tooltip {...props} classes={{ popper: className }} />
+))(({ theme }) => ({
+  [`& .${tooltipClasses.tooltip}`]: {
+    backgroundColor: "#f5f5f9",
+    color: "#fff",
+    maxWidth: 300,
+    fontSize: theme.typography.pxToRem(12),
+    border: "none",
+    padding: 0,
+    display: "flex",
+    flexDirection: "column",
+  },
+}));
+
+function TooltipFull({ title, content, ...props }: TooltipProps & TooltipCustoProps) {
+  return (
+    <>
+      <RawTooltip
+        title={
+          <>
+            <div className="panel-tooltip-title">{title}</div>
+            {content && <div className="panel-tooltip">{content}</div>}
+          </>
+        }
+        {...props}
+      ></RawTooltip>
+    </>
+  );
+}
+
+export default TooltipFull;

+ 14 - 0
src/assets/components/ExpressionInput.module.css

@@ -0,0 +1,14 @@
+.input {
+  background-color: #373737;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 5px 8px;
+  font-family: ui-monospace, monospace;
+  font-size: 12.5px;
+  width: 100px;
+}
+.input:focus {
+  outline: 2px solid #f1be64;
+  outline-offset: 1px;
+}

+ 65 - 0
src/assets/components/ExpressionInpux.tsx

@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+import styles from "./ExpressionInput.module.css";
+
+type Props = {
+  value: number;
+  onCommit: (value: number) => void;
+  min?: number;
+  className?: string;
+};
+
+const SAFE_EXPR = /^[\d+\-*/().\s]+$/;
+
+function evaluate(expr: string): number | null {
+  if (!expr.trim() || !SAFE_EXPR.test(expr)) return null;
+  try {
+    // Sanitized above to only allow digits/operators/parens — not a general eval.
+    // eslint-disable-next-line no-new-func
+    const result = Function(`"use strict"; return (${expr});`)();
+    return typeof result === "number" && Number.isFinite(result) ? result : null;
+  } catch {
+    return null;
+  }
+}
+
+/**
+ * A plain text input, not <input type="number">, because browsers fight you
+ * on typing "120 + 6" into a number input. Local text state only; the parent
+ * only hears about a new value on Enter or blur, and reverts visually if the
+ * expression doesn't evaluate to a finite number.
+ */
+export default function ExpressionInput({ value, onCommit, min, className }: Props) {
+  const [text, setText] = useState(String(value));
+
+  useEffect(() => setText(String(value)), [value]);
+
+  const commit = () => {
+    const result = evaluate(text);
+    if (result === null) {
+      setText(String(value));
+      return;
+    }
+    const rounded = Math.round(result);
+    const clamped = min !== undefined ? Math.max(min, rounded) : rounded;
+    setText(String(clamped));
+    if (clamped !== value) onCommit(clamped);
+  };
+
+  return (
+    <input
+      type="text"
+      inputMode="numeric"
+      className={className ?? styles.input}
+      value={text}
+      onChange={(e) => setText(e.target.value)}
+      onBlur={commit}
+      onKeyDown={(e) => {
+        if (e.key === "Enter") (e.target as HTMLInputElement).blur();
+        else if (e.key === "Escape") {
+          setText(String(value));
+          (e.target as HTMLInputElement).blur();
+        }
+      }}
+    />
+  );
+}

+ 80 - 0
src/assets/components/SelectedBlockPanel.module.css

@@ -0,0 +1,80 @@
+.wrap {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+  background-color: #242324;
+  border: 1px solid #646464;
+  padding: 14px 18px;
+  min-height: 64px;
+}
+.empty {
+  display: flex;
+  align-items: center;
+  background-color: #242324;
+  border: 1px dashed #646464;
+  color: #999;
+  padding: 14px 18px;
+  min-height: 64px;
+  font-size: 12.5px;
+}
+.preview {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding-right: 20px;
+  border-right: 1px solid #3a3a3a;
+}
+.arrow {
+  color: #999;
+  font-size: 14px;
+}
+.fields {
+  display: flex;
+  gap: 18px;
+  flex-wrap: wrap;
+}
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.field label {
+  font-size: 11px;
+  color: #999;
+}
+.field input,
+.field select {
+  background-color: #373737;
+  border: 1px solid #646464;
+  color: #ffe6c0;
+  border-radius: 4px;
+  padding: 5px 8px;
+  font-family: ui-monospace, monospace;
+  font-size: 12.5px;
+  width: 100px;
+}
+.field input:focus,
+.field select:focus {
+  outline: 2px solid #f1be64;
+  outline-offset: 1px;
+}
+.removeBtn {
+  margin-left: auto;
+  background-color: #fe5a5a;
+  border: none;
+  color: #000;
+  font-weight: 600;
+  border-radius: 4px;
+  padding: 8px 14px;
+  font-size: 12.5px;
+  cursor: pointer;
+}
+.removeBtn:hover {
+  background-color: #ff9b9b;
+}
+
+.multilabel {
+  color: #f1be64;
+  font-weight: 600;
+  font-size: 13px;
+}

+ 80 - 0
src/assets/components/SelectedBlockPanel.tsx

@@ -0,0 +1,80 @@
+import styles from "./SelectedBlockPanel.module.css";
+import { ACTION_PRESETS, getPreset, type ClockBlock } from "../types";
+import Icon from "../icon";
+import ExpressionInput from "./ExpressionInpux";
+
+type Props = {
+  selectedBlocks: ClockBlock[]; // 0, 1, or many
+  onChangeOne: (blockId: string, patch: Partial<ClockBlock>) => void;
+  onRemoveSelected: () => void;
+};
+
+export default function SelectedBlockPanel({ selectedBlocks, onChangeOne, onRemoveSelected }: Props) {
+  if (selectedBlocks.length === 0) {
+    return <div className={styles.empty}>Select a block on the timeline to edit it.</div>;
+  }
+
+  if (selectedBlocks.length > 1) {
+    return (
+      <div className={styles.wrap}>
+        <span className={styles.multiLabel}>{selectedBlocks.length} blocks selected</span>
+        <button className={styles.removeBtn} onClick={onRemoveSelected}>
+          Remove all
+        </button>
+      </div>
+    );
+  }
+
+  const block = selectedBlocks[0];
+  const preset = getPreset(block.presetId);
+  const change = (patch: Partial<ClockBlock>) => onChangeOne(block.id, patch);
+
+  return (
+    <div className={styles.wrap}>
+      <div className={styles.preview}>
+        {preset.fromItem && <Icon iconName={`item/${preset.fromItem}.png`} size={40} />}
+        <span className={styles.arrow}>→</span>
+        {preset.toItem && <Icon iconName={`item/${preset.fromItem}.png`} size={40} />}
+      </div>
+
+      <div className={styles.fields}>
+        <div className={styles.field}>
+          <label>Action</label>
+          <select
+            value={block.presetId}
+            onChange={(e) => {
+              const p = getPreset(e.target.value);
+              change({ presetId: p.id, duration: p.ticks + 1 });
+            }}
+          >
+            {ACTION_PRESETS.map((p) => (
+              <option key={p.id} value={p.id}>
+                {p.label}
+              </option>
+            ))}
+          </select>
+        </div>
+        <div className={styles.field}>
+          <label>Start (tick)</label>
+          <ExpressionInput value={block.start} min={0} onCommit={(v) => change({ start: v })} />
+        </div>
+        <div className={styles.field}>
+          <label>Duration (ticks)</label>
+          <ExpressionInput value={block.duration} min={1} onCommit={(v) => change({ duration: v })} />
+        </div>
+        <div className={styles.field}>
+          <label>Count</label>
+          <ExpressionInput value={block.count} min={1} onCommit={(v) => change({ count: v })} />
+        </div>
+        <div className={styles.field}>
+          <label>Repeat ×</label>
+          <ExpressionInput value={block.repeat ?? 1} min={1} onCommit={(v) => change({ repeat: v })} />
+        </div>
+      </div>
+
+      <button className={styles.removeBtn} onClick={onRemoveSelected}>
+        Remove block
+      </button>
+    </div>
+  );
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 215 - 355
src/assets/data/2.0/data.json


+ 162 - 4
src/assets/data/2.0/i18n/de.json

@@ -33,11 +33,13 @@
       "parameter-7": "Parameter 7",
       "parameter-8": "Parameter 8",
       "parameter-9": "Parameter 9",
+      "ee-super-pump-speed-fluid": "Schnellflüssigkeit",
       "fluid-unknown": "Unbekannte Flüssigkeit"
     },
     "descriptions": {
       "thruster-fuel": "Flüssiger Treibstoff für Triebwerke",
       "fusion-plasma": "Ultrahochtemperatur-Ionen, die in einem [entity=fusion-reactor] erzeugt und von einem [entity=fusion-generator] verbraucht werden. Es ist keine normale Flüssigkeit und kann nicht in [entity=pipe] befördert werden, es kann nur durch den Fusionsreaktor und den Fusionsgenerator befördert werden.\nDie Temperatur des Plasmas bestimmt seinen Energiewert.\nEine Plasmamenge repräsentiert auch die Menge an Kühlmittel, das mit ihr wandert, und das erhitzte Kühlmittel wird vom Fusionsgenerator ausgegeben, während das Plasma zur Energiegewinnung genutzt wird.",
+      "ee-super-pump-speed-fluid": "Wenn du das siehst, dreh dich jetzt um! Dies wird von Editor Extensions verwendet, um die Geschwindigkeit der Superpumpe zu steuern, und hat keinen anderen Nutzen.",
       "fluid-unknown": "Diese Flüssigkeit ist nicht verfügbar, da die zugehörige Mod entfernt wurde. Sie wird wiederhergestellt, wenn die Mod wieder aktiviert wird."
     }
   },
@@ -118,6 +120,7 @@
       "ice-platform": "Eisplattform",
       "foundation": "Fundament",
       "cliff-explosives": "Klippensprengstoff",
+      "region-cloner_selection-tool": "Region Cloner Selection Tool",
       "repair-pack": "Reparaturkit",
       "blueprint": "Blaupause",
       "deconstruction-planner": "Abrissplan",
@@ -366,6 +369,9 @@
       "artillery-targeting-remote": "Fernsteuerung für Artillerie",
       "item-unknown": "Unbekannter Gegenstand",
       "no-item": "Kein Gegenstand",
+      "rcalc-heat-dummy": "Wärme",
+      "rcalc-pollution-dummy": "Umweltverschmutzung",
+      "rcalc-power-dummy": "Strom",
       "electric-energy-interface": "Schnittstelle für elektrischen Strom",
       "linked-chest": "Verbundene Kiste",
       "proxy-container": "Proxy-Container",
@@ -379,10 +385,52 @@
       "infinity-cargo-wagon": "Unendlicher Güterwaggon",
       "infinity-chest": "Unendlichkeitskiste",
       "infinity-pipe": "Unendlichkeitsrohr",
+      "rcalc-selection-tool": "Wähler für Ratenrechner",
       "selection-tool": "Auswahlwerkzeug",
       "simple-entity-with-force": "Einfaches Objekt mit Partei",
       "simple-entity-with-owner": "Einfaches Objekt mit Besitzer",
-      "burner-generator": "Befeuerter Generator"
+      "burner-generator": "Befeuerter Generator",
+      "ee-infinity-chest": "Unendliche Kiste",
+      "ee-infinity-chest-active-provider": "Unendliche aktive Anbieterkiste",
+      "ee-infinity-chest-passive-provider": "Unendliche passive Anbieterkiste",
+      "ee-infinity-chest-storage": "Unendliche Lagerkiste",
+      "ee-infinity-chest-buffer": "Unendliche Pufferkiste",
+      "ee-infinity-chest-requester": "Unendliche Anforderungskiste",
+      "ee-aggregate-chest": "Aggregattruhe",
+      "ee-aggregate-chest-passive-provider": "Passive Gesamtanbieterkiste",
+      "ee-linked-chest": "Verbundene Kiste",
+      "ee-infinity-loader": "Unendlicher Belader",
+      "ee-linked-belt": "Verbundenes Fließband",
+      "ee-super-inserter": "Super Greifarm",
+      "ee-infinity-pipe": "Unendliches Rohr",
+      "ee-super-pump": "Super Gewässerpumpe",
+      "ee-infinity-heat-pipe": "Unendliches Wärmerohr",
+      "ee-super-radar": "Super Radar",
+      "ee-super-lab": "Super Labor",
+      "ee-infinity-accumulator": "Unendlicher Akku",
+      "ee-super-electric-pole": "Super Strommast",
+      "ee-super-substation": "Super Umspannwerk",
+      "ee-super-locomotive": "Super Lokomotive",
+      "ee-infinity-cargo-wagon": "Unendlicher Güterwaggon",
+      "ee-infinity-fluid-wagon": "Unendlicher Tankwaggon",
+      "ee-super-fuel": "Super Brennstoff",
+      "ee-super-roboport": "Super Roboterhangar",
+      "ee-super-construction-robot": "Super Bauroboter",
+      "ee-super-logistic-robot": "Super Logistikroboter",
+      "ee-super-beacon": "Super Effektverteiler",
+      "ee-super-speed-module": "Superschnelles Modul",
+      "ee-super-efficiency-module": "Super Effizienzmodul",
+      "ee-super-productivity-module": "Superproduktivitäts Modul",
+      "ee-super-clean-module": "Supersauberes Modul",
+      "ee-super-slow-module": "Superlangsames Modul",
+      "ee-super-inefficiency-module": "Super Ineffizienzmodul",
+      "ee-super-dirty-module": "Superschmutziges Modul",
+      "ee-infinity-fission-reactor-equipment": "Unendlicher Fissionsreaktor",
+      "ee-super-personal-roboport-equipment": "Super persönlicher Roboterhangar",
+      "ee-super-exoskeleton-equipment": "Super Exoskelett-Ausrüstung",
+      "ee-super-energy-shield-equipment": "Super Energieschild-Ausrüstung",
+      "ee-super-night-vision-equipment": "Super Nachtsicht-Ausrüstung",
+      "ee-super-battery-equipment": "Super persönlicher Akku"
     },
     "descriptions": {
       "rail": "Zum manuellen Platzieren gerader Gleise oder durch den Gleisplaner.\n[font=default-semibold][color=#80cef0]Linksklick[/color][/font] um kurze Strecken direkt zu platzieren.\n[font=default-semibold][color=#80cef0]Umschalt + Linksklick[/color][/font] um lange Strecken als Geist zu platzieren.\n[font=default-semibold][color=#80cef0]KEY-CODE-NOT-DEFINE-IN-HEADLESS-MODE[/color][/font] um zwischen ebenerdigen und Hochbahn-Gleisen zu wechseln.",
@@ -456,7 +504,29 @@
       "green-wire": "Verbindet Maschinen mit dem Schaltungsnetz. [font=default-semibold][color=#80cef0]Klicke links[/color][/font], um Verbindungen zu erzeugen oder zu kappen.",
       "red-wire": "Verbindet Maschinen mit dem Schaltungsnetz. [font=default-semibold][color=#80cef0]Klicke links[/color][/font], um Verbindungen zu erzeugen oder zu kappen.",
       "artillery-targeting-remote": "Erlaubt es, Artillerie manuell abzufeuern. Entweder von der Kartenansicht oder aus der Welt heraus.",
-      "item-unknown": "Dieser Gegenstand ist nicht verfügbar, da die zugehörige Mod entfernt wurde. Er wird wiederhergestellt, wenn die Mod wieder aktiviert wird."
+      "item-unknown": "Dieser Gegenstand ist nicht verfügbar, da die zugehörige Mod entfernt wurde. Er wird wiederhergestellt, wenn die Mod wieder aktiviert wird.",
+      "ee-infinity-chest": "Erstellt oder zerstört Items mit anpassbaren Item-Filtern.",
+      "ee-aggregate-chest": "Enthält jedes Item im Spiel.\n[color=255,57,48]Kann bei Missbrauch zu Leistungsproblemen führen, daher sparsam verwenden![/color]",
+      "ee-infinity-loader": "Erzeugt oder zerstört Items auf einem Fließband mit anpassbaren Filtern.",
+      "ee-linked-belt": "Transportiert Gegenstände sofort zu einem anderen verbundenen Fließband.",
+      "ee-infinity-heat-pipe": "Erzeugt oder zerstört eine anpassbare Menge an Wärme.",
+      "ee-infinity-accumulator": "Erzeugt, entleert oder speichert eine konfigurierbare Menge an elektrischer Energie.",
+      "ee-infinity-cargo-wagon": "Erzeugt oder zerstört Items mit anpassbaren Itemfiltern. (Identisch zur unendlichen Truhe)",
+      "ee-infinity-fluid-wagon": "Erstellt oder zerstört Flüssigkeiten mit einem anpassbaren Flüssigkeitsfilter (identisch zum unendlichen Rohr).\n[color=255,57,48]Kann bei Missbrauch zu Leistungsproblemen führen, daher sparsam verwenden![/color]",
+      "ee-super-fuel": "Brennelement, das quasi für immer hält.",
+      "ee-super-speed-module": "Erhöht die Maschinengeschwindigkeit massiv.",
+      "ee-super-efficiency-module": "Senkt den Energieverbrauch der Maschine massiv. Der minimale Energieverbrauch beträgt 20%.",
+      "ee-super-productivity-module": "Erhöht die Produktivität der Maschine erheblich. Kann für alle Rezepte verwendet werden.",
+      "ee-super-clean-module": "Reduziert massiv die Maschinenumweltverschmutzung, die minimale Verschmutzung liegt bei 20 %.",
+      "ee-super-slow-module": "Verringert die Maschinengeschwindigkeit massiv. Die Mindestgeschwindigkeit beträgt 20 %.",
+      "ee-super-inefficiency-module": "Erhöht den Energieverbrauch der Maschine massiv.",
+      "ee-super-dirty-module": "Erhöht massiv die Verschmutzung durch Maschinen.",
+      "ee-infinity-fission-reactor-equipment": "Erzeugt praktisch unbegrenzte Energie für deine Ausrüstung.",
+      "ee-super-personal-roboport-equipment": "Persönlicher Roboterhangar mit großem Baubereich und Roboterkapazität.",
+      "ee-super-exoskeleton-equipment": "Sehr kleines und sehr schnelles Exoskelett.",
+      "ee-super-energy-shield-equipment": "Lächerlich übermächtiger Energieschild, der dich praktisch unsterblich macht.",
+      "ee-super-night-vision-equipment": "Perfekte Nachtsicht, man sieht, als wäre es Tag.",
+      "ee-super-battery-equipment": "Lächerlich große Batterie."
     }
   },
   "recipe": {
@@ -543,6 +613,7 @@
       "ice-platform": "Eisplattform",
       "foundation": "Fundament",
       "cliff-explosives": "Klippensprengstoff",
+      "region-cloner_selection-tool-recycling": "Region Cloner Selection Tool (Wiederverwertung)",
       "repair-pack": "Reparaturkit",
       "blueprint-recycling": "Blaupause (Wiederverwertung)",
       "deconstruction-planner-recycling": "Abrissplan (Wiederverwertung)",
@@ -1055,6 +1126,9 @@
       "railgun-ammo-recycling": "Schienenkanonen-Munition (Wiederverwertung)",
       "railgun-recycling": "Schienenkanone (Wiederverwertung)",
       "railgun-turret-recycling": "Schienenkanonen-Geschützturm (Wiederverwertung)",
+      "rcalc-heat-dummy-recycling": "Wärme (Wiederverwertung)",
+      "rcalc-pollution-dummy-recycling": "Umweltverschmutzung (Wiederverwertung)",
+      "rcalc-power-dummy-recycling": "Strom (Wiederverwertung)",
       "recipe-unknown": "Unbekannter Bauplan",
       "recycler-recycling": "Wiederverwerter (Wiederverwertung)",
       "refined-concrete-recycling": "Stahlbeton (Wiederverwertung)",
@@ -1116,10 +1190,93 @@
       "infinity-cargo-wagon-recycling": "Unendlicher Güterwaggon (Wiederverwertung)",
       "infinity-chest": "Unendlichkeitskiste",
       "infinity-pipe": "Unendlichkeitsrohr",
+      "rcalc-selection-tool-recycling": "Wähler für Ratenrechner (Wiederverwertung)",
       "selection-tool-recycling": "Auswahlwerkzeug (Wiederverwertung)",
       "simple-entity-with-force-recycling": "Einfaches Objekt mit Partei (Wiederverwertung)",
       "simple-entity-with-owner-recycling": "Einfaches Objekt mit Besitzer (Wiederverwertung)",
-      "burner-generator-recycling": "Befeuerter Generator (Wiederverwertung)"
+      "burner-generator-recycling": "Befeuerter Generator (Wiederverwertung)",
+      "ee-infinity-chest": "Unendliche Kiste",
+      "ee-infinity-chest-recycling": "Unendliche Kiste (Wiederverwertung)",
+      "ee-infinity-chest-active-provider": "Unendliche aktive Anbieterkiste",
+      "ee-infinity-chest-active-provider-recycling": "Unendliche aktive Anbieterkiste (Wiederverwertung)",
+      "ee-infinity-chest-passive-provider": "Unendliche passive Anbieterkiste",
+      "ee-infinity-chest-passive-provider-recycling": "Unendliche passive Anbieterkiste (Wiederverwertung)",
+      "ee-infinity-chest-storage": "Unendliche Lagerkiste",
+      "ee-infinity-chest-storage-recycling": "Unendliche Lagerkiste (Wiederverwertung)",
+      "ee-infinity-chest-buffer": "Unendliche Pufferkiste",
+      "ee-infinity-chest-buffer-recycling": "Unendliche Pufferkiste (Wiederverwertung)",
+      "ee-infinity-chest-requester": "Unendliche Anforderungskiste",
+      "ee-infinity-chest-requester-recycling": "Unendliche Anforderungskiste (Wiederverwertung)",
+      "ee-aggregate-chest": "Aggregattruhe",
+      "ee-aggregate-chest-recycling": "Aggregattruhe (Wiederverwertung)",
+      "ee-aggregate-chest-passive-provider": "Passive Gesamtanbieterkiste",
+      "ee-aggregate-chest-passive-provider-recycling": "Passive Gesamtanbieterkiste (Wiederverwertung)",
+      "ee-linked-chest": "Verbundene Kiste",
+      "ee-linked-chest-recycling": "Verbundene Kiste (Wiederverwertung)",
+      "ee-infinity-loader": "Unendlicher Belader",
+      "ee-infinity-loader-recycling": "Unendlicher Belader (Wiederverwertung)",
+      "ee-linked-belt": "Verbundenes Fließband",
+      "ee-linked-belt-recycling": "Verbundenes Fließband (Wiederverwertung)",
+      "ee-super-inserter": "Super Greifarm",
+      "ee-super-inserter-recycling": "Super Greifarm (Wiederverwertung)",
+      "ee-infinity-pipe": "Unendliches Rohr",
+      "ee-infinity-pipe-recycling": "Unendliches Rohr (Wiederverwertung)",
+      "ee-super-pump": "Super Gewässerpumpe",
+      "ee-super-pump-recycling": "Super Gewässerpumpe (Wiederverwertung)",
+      "ee-infinity-heat-pipe": "Unendliches Wärmerohr",
+      "ee-infinity-heat-pipe-recycling": "Unendliches Wärmerohr (Wiederverwertung)",
+      "ee-super-radar": "Super Radar",
+      "ee-super-radar-recycling": "Super Radar (Wiederverwertung)",
+      "ee-super-lab": "Super Labor",
+      "ee-super-lab-recycling": "Super Labor (Wiederverwertung)",
+      "ee-infinity-accumulator": "Unendlicher Akku",
+      "ee-infinity-accumulator-recycling": "Unendlicher Akku (Wiederverwertung)",
+      "ee-super-electric-pole": "Super Strommast",
+      "ee-super-electric-pole-recycling": "Super Strommast (Wiederverwertung)",
+      "ee-super-substation": "Super Umspannwerk",
+      "ee-super-substation-recycling": "Super Umspannwerk (Wiederverwertung)",
+      "ee-super-locomotive": "Super Lokomotive",
+      "ee-super-locomotive-recycling": "Super Lokomotive (Wiederverwertung)",
+      "ee-infinity-cargo-wagon": "Unendlicher Güterwaggon",
+      "ee-infinity-cargo-wagon-recycling": "Unendlicher Güterwaggon (Wiederverwertung)",
+      "ee-infinity-fluid-wagon": "Unendlicher Tankwaggon",
+      "ee-infinity-fluid-wagon-recycling": "Unendlicher Tankwaggon (Wiederverwertung)",
+      "ee-super-fuel": "Super Brennstoff",
+      "ee-super-fuel-recycling": "Super Brennstoff (Wiederverwertung)",
+      "ee-super-roboport": "Super Roboterhangar",
+      "ee-super-roboport-recycling": "Super Roboterhangar (Wiederverwertung)",
+      "ee-super-construction-robot": "Super Bauroboter",
+      "ee-super-construction-robot-recycling": "Super Bauroboter (Wiederverwertung)",
+      "ee-super-logistic-robot": "Super Logistikroboter",
+      "ee-super-logistic-robot-recycling": "Super Logistikroboter (Wiederverwertung)",
+      "ee-super-beacon": "Super Effektverteiler",
+      "ee-super-beacon-recycling": "Super Effektverteiler (Wiederverwertung)",
+      "ee-super-speed-module": "Superschnelles Modul",
+      "ee-super-speed-module-recycling": "Superschnelles Modul (Wiederverwertung)",
+      "ee-super-efficiency-module": "Super Effizienzmodul",
+      "ee-super-efficiency-module-recycling": "Super Effizienzmodul (Wiederverwertung)",
+      "ee-super-productivity-module": "Superproduktivitäts Modul",
+      "ee-super-productivity-module-recycling": "Superproduktivitäts Modul (Wiederverwertung)",
+      "ee-super-clean-module": "Supersauberes Modul",
+      "ee-super-clean-module-recycling": "Supersauberes Modul (Wiederverwertung)",
+      "ee-super-slow-module": "Superlangsames Modul",
+      "ee-super-slow-module-recycling": "Superlangsames Modul (Wiederverwertung)",
+      "ee-super-inefficiency-module": "Super Ineffizienzmodul",
+      "ee-super-inefficiency-module-recycling": "Super Ineffizienzmodul (Wiederverwertung)",
+      "ee-super-dirty-module": "Superschmutziges Modul",
+      "ee-super-dirty-module-recycling": "Superschmutziges Modul (Wiederverwertung)",
+      "ee-infinity-fission-reactor-equipment": "Unendlicher Fissionsreaktor",
+      "ee-infinity-fission-reactor-equipment-recycling": "Unendlicher Fissionsreaktor (Wiederverwertung)",
+      "ee-super-personal-roboport-equipment": "Super persönlicher Roboterhangar",
+      "ee-super-personal-roboport-equipment-recycling": "Super persönlicher Roboterhangar (Wiederverwertung)",
+      "ee-super-exoskeleton-equipment": "Super Exoskelett-Ausrüstung",
+      "ee-super-exoskeleton-equipment-recycling": "Super Exoskelett-Ausrüstung (Wiederverwertung)",
+      "ee-super-energy-shield-equipment": "Super Energieschild-Ausrüstung",
+      "ee-super-energy-shield-equipment-recycling": "Super Energieschild-Ausrüstung (Wiederverwertung)",
+      "ee-super-night-vision-equipment": "Super Nachtsicht-Ausrüstung",
+      "ee-super-night-vision-equipment-recycling": "Super Nachtsicht-Ausrüstung (Wiederverwertung)",
+      "ee-super-battery-equipment": "Super persönlicher Akku",
+      "ee-super-battery-equipment-recycling": "Super persönlicher Akku (Wiederverwertung)"
     },
     "descriptions": {
       "ammoniacal-solution-separation": "[fluid=ammoniacal-solution] wird durch ein [entity=offshore-pump] in den Ozeanen von [planet=aquilo] gewonnen.",
@@ -1140,7 +1297,8 @@
       "tiles": "Kacheln",
       "environment": "Umgebung",
       "effects": "Effekte",
-      "other": "Unsortiert"
+      "other": "Unsortiert",
+      "ee-tools": "Prüfwerkzeuge"
     }
   },
   "quality": {

+ 162 - 4
src/assets/data/2.0/i18n/en.json

@@ -33,11 +33,13 @@
       "parameter-7": "Parameter 7",
       "parameter-8": "Parameter 8",
       "parameter-9": "Parameter 9",
+      "ee-super-pump-speed-fluid": "Speedfluid",
       "fluid-unknown": "Unknown fluid"
     },
     "descriptions": {
       "thruster-fuel": "Liquid thruster fuel",
       "fusion-plasma": "Ultra-high-temperature ions generated in [entity=fusion-reactor] and consumed by a [entity=fusion-generator]. It is not a normal fluid and cannot be moved in [entity=pipe], it can only be moved through the Fusion reactor and Fusion generator.\nThe temperature of plasma determines its energy value.\nA quantity of plasma also represents that amount of coolant that travels with it, and the heated coolant is output by the Fusion generator as the plasma is used for energy.",
+      "ee-super-pump-speed-fluid": "If you're seeing this, turn back now! This is used by Editor Extensions to control the speed of the super pump, and has no usefulness elsewhere.",
       "fluid-unknown": "This fluid is not available due to mod removal, it will be restored if the mod is re-enabled."
     }
   },
@@ -118,6 +120,7 @@
       "ice-platform": "Ice platform",
       "foundation": "Foundation",
       "cliff-explosives": "Cliff explosives",
+      "region-cloner_selection-tool": "Region Cloner Selection Tool",
       "repair-pack": "Repair pack",
       "blueprint": "Blueprint",
       "deconstruction-planner": "Deconstruction planner",
@@ -366,6 +369,9 @@
       "artillery-targeting-remote": "Artillery targeting remote",
       "item-unknown": "Unknown item",
       "no-item": "No item",
+      "rcalc-heat-dummy": "Heat",
+      "rcalc-pollution-dummy": "Pollution",
+      "rcalc-power-dummy": "Power",
       "electric-energy-interface": "Electric energy interface",
       "linked-chest": "Linked chest",
       "proxy-container": "Proxy container",
@@ -379,10 +385,52 @@
       "infinity-cargo-wagon": "Infinity cargo wagon",
       "infinity-chest": "Infinity chest",
       "infinity-pipe": "Infinity pipe",
+      "rcalc-selection-tool": "Rate Calculator selector",
       "selection-tool": "Selection tool",
       "simple-entity-with-force": "Simple entity with force",
       "simple-entity-with-owner": "Simple entity with owner",
-      "burner-generator": "Burner generator"
+      "burner-generator": "Burner generator",
+      "ee-infinity-chest": "Infinity chest",
+      "ee-infinity-chest-active-provider": "Infinity active provider chest",
+      "ee-infinity-chest-passive-provider": "Infinity passive provider chest",
+      "ee-infinity-chest-storage": "Infinity storage chest",
+      "ee-infinity-chest-buffer": "Infinity buffer chest",
+      "ee-infinity-chest-requester": "Infinity requester chest",
+      "ee-aggregate-chest": "Aggregate chest",
+      "ee-aggregate-chest-passive-provider": "Aggregate passive provider chest",
+      "ee-linked-chest": "Linked chest",
+      "ee-infinity-loader": "Infinity loader",
+      "ee-linked-belt": "Linked belt",
+      "ee-super-inserter": "Super inserter",
+      "ee-infinity-pipe": "Infinity pipe",
+      "ee-super-pump": "Super pump",
+      "ee-infinity-heat-pipe": "Infinity heat pipe",
+      "ee-super-radar": "Super radar",
+      "ee-super-lab": "Super lab",
+      "ee-infinity-accumulator": "Infinity accumulator",
+      "ee-super-electric-pole": "Super electric pole",
+      "ee-super-substation": "Super substation",
+      "ee-super-locomotive": "Super locomotive",
+      "ee-infinity-cargo-wagon": "Infinity cargo wagon",
+      "ee-infinity-fluid-wagon": "Infinity fluid wagon",
+      "ee-super-fuel": "Super fuel",
+      "ee-super-roboport": "Super roboport",
+      "ee-super-construction-robot": "Super construction robot",
+      "ee-super-logistic-robot": "Super logistic robot",
+      "ee-super-beacon": "Super beacon",
+      "ee-super-speed-module": "Super speed module",
+      "ee-super-efficiency-module": "Super efficiency module",
+      "ee-super-productivity-module": "Super productivity module",
+      "ee-super-clean-module": "Super clean module",
+      "ee-super-slow-module": "Super slow module",
+      "ee-super-inefficiency-module": "Super inefficiency module",
+      "ee-super-dirty-module": "Super dirty module",
+      "ee-infinity-fission-reactor-equipment": "Infinity fission reactor",
+      "ee-super-personal-roboport-equipment": "Super personal roboport",
+      "ee-super-exoskeleton-equipment": "Super exoskeleton",
+      "ee-super-energy-shield-equipment": "Super energy shield",
+      "ee-super-night-vision-equipment": "Super night vision",
+      "ee-super-battery-equipment": "Super personal battery"
     },
     "descriptions": {
       "rail": "Use to build straight rails manually or through the rail planner.\n[font=default-semibold][color=#80cef0]Left-click[/color][/font] to build short paths directly.\n[font=default-semibold][color=#80cef0]Shift + Left-click[/color][/font] to place long ghost paths.\n[font=default-semibold][color=#80cef0]KEY-CODE-NOT-DEFINE-IN-HEADLESS-MODE[/color][/font] to switch between ground and elevated paths.",
@@ -456,7 +504,29 @@
       "green-wire": "Used to connect machines to the circuit network using [font=default-semibold][color=#80cef0]Left-click[/color][/font].",
       "red-wire": "Used to connect machines to the circuit network using [font=default-semibold][color=#80cef0]Left-click[/color][/font].",
       "artillery-targeting-remote": "Allows firing artillery manually from the map or the world.",
-      "item-unknown": "This item is not available due to mod removal, it will be restored if the mod is re-enabled."
+      "item-unknown": "This item is not available due to mod removal, it will be restored if the mod is re-enabled.",
+      "ee-infinity-chest": "Creates or destroys items using customizable item filters.",
+      "ee-aggregate-chest": "Contains every item in the game.\n[color=255,57,48]Will cause performance issues if abused, use sparingly![/color]",
+      "ee-infinity-loader": "Creates or destroys items on a belt using customizable filters.",
+      "ee-linked-belt": "Instantly transports items to another linked belt.",
+      "ee-infinity-heat-pipe": "Creates or destroys a configurable amount of heat.",
+      "ee-infinity-accumulator": "Produces, drains, or stores a configurable amount of electric energy.",
+      "ee-infinity-cargo-wagon": "Creates or destroys items using customizable item filters (identical to infinity chest).",
+      "ee-infinity-fluid-wagon": "Creates or destroys fluids using a customizable fluid filter (identical to infinity pipe).\n[color=255,57,48]Will cause performance issues if abused, use sparingly![/color]",
+      "ee-super-fuel": "Nuclear fuel that lasts pretty much forever.",
+      "ee-super-speed-module": "Massively increases machine speed.",
+      "ee-super-efficiency-module": "Massively decreases machine energy consumption. Minimum energy consumption is 20%.",
+      "ee-super-productivity-module": "Massively increases machine productivity.",
+      "ee-super-clean-module": "Massively decreases machine pollution. Minimum pollution is 20%.",
+      "ee-super-slow-module": "Massively decreases machine speed. Minimum speed is 20%.",
+      "ee-super-inefficiency-module": "Massively increases machine energy consumption.",
+      "ee-super-dirty-module": "Massively increases machine pollution.",
+      "ee-infinity-fission-reactor-equipment": "Generates virtually unlimited power for your equipment.",
+      "ee-super-personal-roboport-equipment": "Personal robport with massive construction area and robot capacity.",
+      "ee-super-exoskeleton-equipment": "Very small and very quick exoskeleton.",
+      "ee-super-energy-shield-equipment": "Ridiculously overpowered energy shield, makes you practically immortal.",
+      "ee-super-night-vision-equipment": "Perfect night vision, you can see as if it's daytime.",
+      "ee-super-battery-equipment": "Ridiculously massive battery."
     }
   },
   "recipe": {
@@ -543,6 +613,7 @@
       "ice-platform": "Ice platform",
       "foundation": "Foundation",
       "cliff-explosives": "Cliff explosives",
+      "region-cloner_selection-tool-recycling": "Region Cloner Selection Tool recycling",
       "repair-pack": "Repair pack",
       "blueprint-recycling": "Blueprint recycling",
       "deconstruction-planner-recycling": "Deconstruction planner recycling",
@@ -1055,6 +1126,9 @@
       "railgun-ammo-recycling": "Railgun ammo recycling",
       "railgun-recycling": "Railgun recycling",
       "railgun-turret-recycling": "Railgun turret recycling",
+      "rcalc-heat-dummy-recycling": "Heat recycling",
+      "rcalc-pollution-dummy-recycling": "Pollution recycling",
+      "rcalc-power-dummy-recycling": "Power recycling",
       "recipe-unknown": "Unknown recipe",
       "recycler-recycling": "Recycler recycling",
       "refined-concrete-recycling": "Refined concrete recycling",
@@ -1116,10 +1190,93 @@
       "infinity-cargo-wagon-recycling": "Infinity cargo wagon recycling",
       "infinity-chest": "Infinity chest",
       "infinity-pipe": "Infinity pipe",
+      "rcalc-selection-tool-recycling": "Rate Calculator selector recycling",
       "selection-tool-recycling": "Selection tool recycling",
       "simple-entity-with-force-recycling": "Simple entity with force recycling",
       "simple-entity-with-owner-recycling": "Simple entity with owner recycling",
-      "burner-generator-recycling": "Burner generator recycling"
+      "burner-generator-recycling": "Burner generator recycling",
+      "ee-infinity-chest": "Infinity chest",
+      "ee-infinity-chest-recycling": "Infinity chest recycling",
+      "ee-infinity-chest-active-provider": "Infinity active provider chest",
+      "ee-infinity-chest-active-provider-recycling": "Infinity active provider chest recycling",
+      "ee-infinity-chest-passive-provider": "Infinity passive provider chest",
+      "ee-infinity-chest-passive-provider-recycling": "Infinity passive provider chest recycling",
+      "ee-infinity-chest-storage": "Infinity storage chest",
+      "ee-infinity-chest-storage-recycling": "Infinity storage chest recycling",
+      "ee-infinity-chest-buffer": "Infinity buffer chest",
+      "ee-infinity-chest-buffer-recycling": "Infinity buffer chest recycling",
+      "ee-infinity-chest-requester": "Infinity requester chest",
+      "ee-infinity-chest-requester-recycling": "Infinity requester chest recycling",
+      "ee-aggregate-chest": "Aggregate chest",
+      "ee-aggregate-chest-recycling": "Aggregate chest recycling",
+      "ee-aggregate-chest-passive-provider": "Aggregate passive provider chest",
+      "ee-aggregate-chest-passive-provider-recycling": "Aggregate passive provider chest recycling",
+      "ee-linked-chest": "Linked chest",
+      "ee-linked-chest-recycling": "Linked chest recycling",
+      "ee-infinity-loader": "Infinity loader",
+      "ee-infinity-loader-recycling": "Infinity loader recycling",
+      "ee-linked-belt": "Linked belt",
+      "ee-linked-belt-recycling": "Linked belt recycling",
+      "ee-super-inserter": "Super inserter",
+      "ee-super-inserter-recycling": "Super inserter recycling",
+      "ee-infinity-pipe": "Infinity pipe",
+      "ee-infinity-pipe-recycling": "Infinity pipe recycling",
+      "ee-super-pump": "Super pump",
+      "ee-super-pump-recycling": "Super pump recycling",
+      "ee-infinity-heat-pipe": "Infinity heat pipe",
+      "ee-infinity-heat-pipe-recycling": "Infinity heat pipe recycling",
+      "ee-super-radar": "Super radar",
+      "ee-super-radar-recycling": "Super radar recycling",
+      "ee-super-lab": "Super lab",
+      "ee-super-lab-recycling": "Super lab recycling",
+      "ee-infinity-accumulator": "Infinity accumulator",
+      "ee-infinity-accumulator-recycling": "Infinity accumulator recycling",
+      "ee-super-electric-pole": "Super electric pole",
+      "ee-super-electric-pole-recycling": "Super electric pole recycling",
+      "ee-super-substation": "Super substation",
+      "ee-super-substation-recycling": "Super substation recycling",
+      "ee-super-locomotive": "Super locomotive",
+      "ee-super-locomotive-recycling": "Super locomotive recycling",
+      "ee-infinity-cargo-wagon": "Infinity cargo wagon",
+      "ee-infinity-cargo-wagon-recycling": "Infinity cargo wagon recycling",
+      "ee-infinity-fluid-wagon": "Infinity fluid wagon",
+      "ee-infinity-fluid-wagon-recycling": "Infinity fluid wagon recycling",
+      "ee-super-fuel": "Super fuel",
+      "ee-super-fuel-recycling": "Super fuel recycling",
+      "ee-super-roboport": "Super roboport",
+      "ee-super-roboport-recycling": "Super roboport recycling",
+      "ee-super-construction-robot": "Super construction robot",
+      "ee-super-construction-robot-recycling": "Super construction robot recycling",
+      "ee-super-logistic-robot": "Super logistic robot",
+      "ee-super-logistic-robot-recycling": "Super logistic robot recycling",
+      "ee-super-beacon": "Super beacon",
+      "ee-super-beacon-recycling": "Super beacon recycling",
+      "ee-super-speed-module": "Super speed module",
+      "ee-super-speed-module-recycling": "Super speed module recycling",
+      "ee-super-efficiency-module": "Super efficiency module",
+      "ee-super-efficiency-module-recycling": "Super efficiency module recycling",
+      "ee-super-productivity-module": "Super productivity module",
+      "ee-super-productivity-module-recycling": "Super productivity module recycling",
+      "ee-super-clean-module": "Super clean module",
+      "ee-super-clean-module-recycling": "Super clean module recycling",
+      "ee-super-slow-module": "Super slow module",
+      "ee-super-slow-module-recycling": "Super slow module recycling",
+      "ee-super-inefficiency-module": "Super inefficiency module",
+      "ee-super-inefficiency-module-recycling": "Super inefficiency module recycling",
+      "ee-super-dirty-module": "Super dirty module",
+      "ee-super-dirty-module-recycling": "Super dirty module recycling",
+      "ee-infinity-fission-reactor-equipment": "Infinity fission reactor",
+      "ee-infinity-fission-reactor-equipment-recycling": "Infinity fission reactor recycling",
+      "ee-super-personal-roboport-equipment": "Super personal roboport",
+      "ee-super-personal-roboport-equipment-recycling": "Super personal roboport recycling",
+      "ee-super-exoskeleton-equipment": "Super exoskeleton",
+      "ee-super-exoskeleton-equipment-recycling": "Super exoskeleton recycling",
+      "ee-super-energy-shield-equipment": "Super energy shield",
+      "ee-super-energy-shield-equipment-recycling": "Super energy shield recycling",
+      "ee-super-night-vision-equipment": "Super night vision",
+      "ee-super-night-vision-equipment-recycling": "Super night vision recycling",
+      "ee-super-battery-equipment": "Super personal battery",
+      "ee-super-battery-equipment-recycling": "Super personal battery recycling"
     },
     "descriptions": {
       "ammoniacal-solution-separation": "[fluid=ammoniacal-solution] is gained by an [entity=offshore-pump] in the oceans of [planet=aquilo].",
@@ -1140,7 +1297,8 @@
       "tiles": "Tiles",
       "environment": "Environment",
       "effects": "Effects",
-      "other": "Unsorted"
+      "other": "Unsorted",
+      "ee-tools": "Testing Tools"
     }
   },
   "quality": {

+ 162 - 4
src/assets/data/2.0/i18n/fr.json

@@ -33,11 +33,13 @@
       "parameter-7": "Paramètre 7",
       "parameter-8": "Paramètre 8",
       "parameter-9": "Paramètre 9",
+      "ee-super-pump-speed-fluid": "Fluide de vitesse",
       "fluid-unknown": "Fluide inconnu"
     },
     "descriptions": {
       "thruster-fuel": "Carburant liquide pour propulseur",
       "fusion-plasma": "Ions à très haute température générés dans un [entity=fusion-reactor] et consommés par un [entity=fusion-generator]. Il ne s'agit pas d'un fluide normal et il ne peut pas être transporté dans un [entity=pipe], il ne peut être transporté que entre le Réacteur de fusion et le Générateur de fusion.\nLa température du plasma détermine sa valeur énergétique.\nLa quantité de plasma représente également la quantité de liquide de refroidissement qui l'accompagne. Du liquide de refroidissement réchauffé est produit par le Générateur de fusion lorsque le plasma est utilisé pour produire de l'énergie.",
+      "ee-super-pump-speed-fluid": "Si vous voyez ce message, retournez maintenant! Ceci est utilisé par Editor Extensions pour contrôler la vitesse de la super pompe et n'a aucune utilité ailleurs.",
       "fluid-unknown": "Le fluide n'est pas disponible dû à la suppression d'un mod, il sera restauré lorsque celui-ci sera réactivé."
     }
   },
@@ -118,6 +120,7 @@
       "ice-platform": "Plateforme de glace",
       "foundation": "Fondation",
       "cliff-explosives": "Explosifs de falaise",
+      "region-cloner_selection-tool": "Region Cloner Selection Tool",
       "repair-pack": "Kit de réparation",
       "blueprint": "Plan",
       "deconstruction-planner": "Planificateur de déconstruction",
@@ -366,6 +369,9 @@
       "artillery-targeting-remote": "Commande à distance de l'artillerie",
       "item-unknown": "Objet inconnu.",
       "no-item": "Aucun objet",
+      "rcalc-heat-dummy": "Chaleur",
+      "rcalc-pollution-dummy": "Pollution",
+      "rcalc-power-dummy": "Énergie",
       "electric-energy-interface": "Interface de l’énergie électrique",
       "linked-chest": "Coffre lié",
       "proxy-container": "Conteneur fictif",
@@ -379,10 +385,52 @@
       "infinity-cargo-wagon": "Wagon de marchandises infini",
       "infinity-chest": "Coffre infini",
       "infinity-pipe": "Tuyau infini",
+      "rcalc-selection-tool": "Sélecteur du Calculateur de Taux",
       "selection-tool": "Outil de sélection",
       "simple-entity-with-force": "Entité simple avec une force",
       "simple-entity-with-owner": "Entité simple avec un propriétaire",
-      "burner-generator": "Générateur thermique"
+      "burner-generator": "Générateur thermique",
+      "ee-infinity-chest": "Coffre infini",
+      "ee-infinity-chest-active-provider": "Coffre logistique d'approvisionnement actif infini",
+      "ee-infinity-chest-passive-provider": "Coffre logistique d'approvisionnement passif infini",
+      "ee-infinity-chest-storage": "Coffre logistique de stockage infini",
+      "ee-infinity-chest-buffer": "Coffre logistique tampon infini",
+      "ee-infinity-chest-requester": "Coffre logistique demandeur infini",
+      "ee-aggregate-chest": "Coffre agrégé",
+      "ee-aggregate-chest-passive-provider": "Coffre logistique d'approvisionnement passif agrégé",
+      "ee-linked-chest": "Coffre lié",
+      "ee-infinity-loader": "Chargeur infini",
+      "ee-linked-belt": "Convoyeur lié",
+      "ee-super-inserter": "Super bras robotisé",
+      "ee-infinity-pipe": "Tuyau infini",
+      "ee-super-pump": "Super pompe",
+      "ee-infinity-heat-pipe": "Conduite de chaleur infinie",
+      "ee-super-radar": "Super radar",
+      "ee-super-lab": "Super laboratoire",
+      "ee-infinity-accumulator": "Accumulateur infini",
+      "ee-super-electric-pole": "Super poteau électrique",
+      "ee-super-substation": "Super poste électrique",
+      "ee-super-locomotive": "Super locomotive",
+      "ee-infinity-cargo-wagon": "Wagon de marchandises infini",
+      "ee-infinity-fluid-wagon": "Wagon de fluide infini",
+      "ee-super-fuel": "Super combustibles",
+      "ee-super-roboport": "Super roboport",
+      "ee-super-construction-robot": "Super robot de construction",
+      "ee-super-logistic-robot": "Super robot logistique",
+      "ee-super-beacon": "Super diffuseur",
+      "ee-super-speed-module": "Super module vite",
+      "ee-super-efficiency-module": "Super module d'efficacité",
+      "ee-super-productivity-module": "Super module de productivité",
+      "ee-super-clean-module": "Super module propre",
+      "ee-super-slow-module": "Super module lent",
+      "ee-super-inefficiency-module": "Super module d'inefficacité",
+      "ee-super-dirty-module": "Super module sale",
+      "ee-infinity-fission-reactor-equipment": "Réacteur à fission infini",
+      "ee-super-personal-roboport-equipment": "Super roboport personnel",
+      "ee-super-exoskeleton-equipment": "Super exosquelette",
+      "ee-super-energy-shield-equipment": "Super bouclier d'énergie",
+      "ee-super-night-vision-equipment": "Super vision nocturne",
+      "ee-super-battery-equipment": "Super batterie personnelle"
     },
     "descriptions": {
       "rail": "À utiliser pour construire des rails droits manuellement ou à l'aide du planificateur ferroviaire.\n[font=default-semibold][color=#80cef0]Clic gauche[/color][/font] pour construire des voies courtes directement.\n[font=default-semibold][color=#80cef0]Shift + Clic gauche[/color][/font] pour placer de longues voies fantômes.\n[font=default-semibold][color=#80cef0]KEY-CODE-NOT-DEFINE-IN-HEADLESS-MODE[/color][/font] pour basculer entre des voies au sol et des voies surélevées.",
@@ -456,7 +504,29 @@
       "green-wire": "Utilisé pour connecter les machines au réseau logique à l’aide de [font=default-semibold][color=#80cef0]Clic gauche[/color][/font].",
       "red-wire": "Utilisé pour connecter les machines au réseau logique à l’aide de [font=default-semibold][color=#80cef0]Clic gauche[/color][/font].",
       "artillery-targeting-remote": "Permet le tir d’artillerie manuellement à partir de la carte ou du monde.",
-      "item-unknown": "Cet objet n'est pas disponible dû à la suppression d'un mod, il sera restauré lorsque celui-ci sera réactivé."
+      "item-unknown": "Cet objet n'est pas disponible dû à la suppression d'un mod, il sera restauré lorsque celui-ci sera réactivé.",
+      "ee-infinity-chest": "Crée ou détruit des objets à l'aide de filtres personnalisables.",
+      "ee-aggregate-chest": "Contient tous les objets du jeu.\n[color=255,57,48]Cela causera des problèmes de performance en cas d'abus, utilisez avec modération![/color]",
+      "ee-infinity-loader": "Crée ou détruit des objets sur un convoyeur à l'aide des filtres personnalisables.",
+      "ee-linked-belt": "Transporte instantanément des items vers une autre convoyeur lié.",
+      "ee-infinity-heat-pipe": "Crée ou détruit une quantité configurable de chaleur.",
+      "ee-infinity-accumulator": "Produit, consomme ou stocke une quantité configurable d'énergie électrique.",
+      "ee-infinity-cargo-wagon": "Crée ou détruit des objets en utilisant des filtres d'objet personnalisables (identique au coffre infini).",
+      "ee-infinity-fluid-wagon": "Crée ou détruit des fluides à l'aide d'un filtre de fluide (identique au tuyau infini).\n[color=255,57,48]Cela causera des problèmes de performance en cas d'abus, utilisez avec modération![/color]",
+      "ee-super-fuel": "Combustible nucléaire qui dure pour presque tout temps.",
+      "ee-super-speed-module": "Augmente massivement la vitesse de la machine.",
+      "ee-super-efficiency-module": "Réduit massivement la consommation d'énergie de la machine. La consommation d'énergie minimale est de 20%.",
+      "ee-super-productivity-module": "Augmente massivement la productivité de la machine.",
+      "ee-super-clean-module": "Réduit massivement la pollution de la machine. La pollution minimale est de 20%.",
+      "ee-super-slow-module": "Réduit massivement la vitesse de la machine. La vitesse minimale est de 20%.",
+      "ee-super-inefficiency-module": "Augmente massivement la consommation d'énergie de la machine.",
+      "ee-super-dirty-module": "Augmente massivement la pollution de la machine.",
+      "ee-infinity-fission-reactor-equipment": "Génère de l'énergie quasiment illimitée pour votre équipement.",
+      "ee-super-personal-roboport-equipment": "Roboport personnelle avec une zone de construction et capacité de robot énorme.",
+      "ee-super-exoskeleton-equipment": "Exosquelette très vite et très petit.",
+      "ee-super-energy-shield-equipment": "Bouclier d'énergie superpuissant ridicule, vous rends pratiquement immortel.",
+      "ee-super-night-vision-equipment": "La vision nocturne parfaite, vous voyez comme si c'était la journée.",
+      "ee-super-battery-equipment": "Batterie de capacité énorme."
     }
   },
   "recipe": {
@@ -543,6 +613,7 @@
       "ice-platform": "Plateforme de glace",
       "foundation": "Fondation",
       "cliff-explosives": "Explosifs de falaise",
+      "region-cloner_selection-tool-recycling": "Recyclage de Region Cloner Selection Tool",
       "repair-pack": "Kit de réparation",
       "blueprint-recycling": "Recyclage de Plan",
       "deconstruction-planner-recycling": "Recyclage de Planificateur de déconstruction",
@@ -1055,6 +1126,9 @@
       "railgun-ammo-recycling": "Recyclage de Munition pour canon électromagnétique",
       "railgun-recycling": "Recyclage de Canon électromagnétique",
       "railgun-turret-recycling": "Recyclage de Tourelle à canon électromagnétique",
+      "rcalc-heat-dummy-recycling": "Recyclage de Chaleur",
+      "rcalc-pollution-dummy-recycling": "Recyclage de Pollution",
+      "rcalc-power-dummy-recycling": "Recyclage de Énergie",
       "recipe-unknown": "Recette inconnue",
       "recycler-recycling": "Recyclage de Recycleur",
       "refined-concrete-recycling": "Recyclage de Béton armé",
@@ -1116,10 +1190,93 @@
       "infinity-cargo-wagon-recycling": "Recyclage de Wagon de marchandises infini",
       "infinity-chest": "Coffre infini",
       "infinity-pipe": "Tuyau infini",
+      "rcalc-selection-tool-recycling": "Recyclage de Sélecteur du Calculateur de Taux",
       "selection-tool-recycling": "Recyclage de Outil de sélection",
       "simple-entity-with-force-recycling": "Recyclage de Entité simple avec une force",
       "simple-entity-with-owner-recycling": "Recyclage de Entité simple avec un propriétaire",
-      "burner-generator-recycling": "Recyclage de Générateur thermique"
+      "burner-generator-recycling": "Recyclage de Générateur thermique",
+      "ee-infinity-chest": "Coffre infini",
+      "ee-infinity-chest-recycling": "Recyclage de Coffre infini",
+      "ee-infinity-chest-active-provider": "Coffre logistique d'approvisionnement actif infini",
+      "ee-infinity-chest-active-provider-recycling": "Recyclage de Coffre logistique d'approvisionnement actif infini",
+      "ee-infinity-chest-passive-provider": "Coffre logistique d'approvisionnement passif infini",
+      "ee-infinity-chest-passive-provider-recycling": "Recyclage de Coffre logistique d'approvisionnement passif infini",
+      "ee-infinity-chest-storage": "Coffre logistique de stockage infini",
+      "ee-infinity-chest-storage-recycling": "Recyclage de Coffre logistique de stockage infini",
+      "ee-infinity-chest-buffer": "Coffre logistique tampon infini",
+      "ee-infinity-chest-buffer-recycling": "Recyclage de Coffre logistique tampon infini",
+      "ee-infinity-chest-requester": "Coffre logistique demandeur infini",
+      "ee-infinity-chest-requester-recycling": "Recyclage de Coffre logistique demandeur infini",
+      "ee-aggregate-chest": "Coffre agrégé",
+      "ee-aggregate-chest-recycling": "Recyclage de Coffre agrégé",
+      "ee-aggregate-chest-passive-provider": "Coffre logistique d'approvisionnement passif agrégé",
+      "ee-aggregate-chest-passive-provider-recycling": "Recyclage de Coffre logistique d'approvisionnement passif agrégé",
+      "ee-linked-chest": "Coffre lié",
+      "ee-linked-chest-recycling": "Recyclage de Coffre lié",
+      "ee-infinity-loader": "Chargeur infini",
+      "ee-infinity-loader-recycling": "Recyclage de Chargeur infini",
+      "ee-linked-belt": "Convoyeur lié",
+      "ee-linked-belt-recycling": "Recyclage de Convoyeur lié",
+      "ee-super-inserter": "Super bras robotisé",
+      "ee-super-inserter-recycling": "Recyclage de Super bras robotisé",
+      "ee-infinity-pipe": "Tuyau infini",
+      "ee-infinity-pipe-recycling": "Recyclage de Tuyau infini",
+      "ee-super-pump": "Super pompe",
+      "ee-super-pump-recycling": "Recyclage de Super pompe",
+      "ee-infinity-heat-pipe": "Conduite de chaleur infinie",
+      "ee-infinity-heat-pipe-recycling": "Recyclage de Conduite de chaleur infinie",
+      "ee-super-radar": "Super radar",
+      "ee-super-radar-recycling": "Recyclage de Super radar",
+      "ee-super-lab": "Super laboratoire",
+      "ee-super-lab-recycling": "Recyclage de Super laboratoire",
+      "ee-infinity-accumulator": "Accumulateur infini",
+      "ee-infinity-accumulator-recycling": "Recyclage de Accumulateur infini",
+      "ee-super-electric-pole": "Super poteau électrique",
+      "ee-super-electric-pole-recycling": "Recyclage de Super poteau électrique",
+      "ee-super-substation": "Super poste électrique",
+      "ee-super-substation-recycling": "Recyclage de Super poste électrique",
+      "ee-super-locomotive": "Super locomotive",
+      "ee-super-locomotive-recycling": "Recyclage de Super locomotive",
+      "ee-infinity-cargo-wagon": "Wagon de marchandises infini",
+      "ee-infinity-cargo-wagon-recycling": "Recyclage de Wagon de marchandises infini",
+      "ee-infinity-fluid-wagon": "Wagon de fluide infini",
+      "ee-infinity-fluid-wagon-recycling": "Recyclage de Wagon de fluide infini",
+      "ee-super-fuel": "Super combustibles",
+      "ee-super-fuel-recycling": "Recyclage de Super combustibles",
+      "ee-super-roboport": "Super roboport",
+      "ee-super-roboport-recycling": "Recyclage de Super roboport",
+      "ee-super-construction-robot": "Super robot de construction",
+      "ee-super-construction-robot-recycling": "Recyclage de Super robot de construction",
+      "ee-super-logistic-robot": "Super robot logistique",
+      "ee-super-logistic-robot-recycling": "Recyclage de Super robot logistique",
+      "ee-super-beacon": "Super diffuseur",
+      "ee-super-beacon-recycling": "Recyclage de Super diffuseur",
+      "ee-super-speed-module": "Super module vite",
+      "ee-super-speed-module-recycling": "Recyclage de Super module vite",
+      "ee-super-efficiency-module": "Super module d'efficacité",
+      "ee-super-efficiency-module-recycling": "Recyclage de Super module d'efficacité",
+      "ee-super-productivity-module": "Super module de productivité",
+      "ee-super-productivity-module-recycling": "Recyclage de Super module de productivité",
+      "ee-super-clean-module": "Super module propre",
+      "ee-super-clean-module-recycling": "Recyclage de Super module propre",
+      "ee-super-slow-module": "Super module lent",
+      "ee-super-slow-module-recycling": "Recyclage de Super module lent",
+      "ee-super-inefficiency-module": "Super module d'inefficacité",
+      "ee-super-inefficiency-module-recycling": "Recyclage de Super module d'inefficacité",
+      "ee-super-dirty-module": "Super module sale",
+      "ee-super-dirty-module-recycling": "Recyclage de Super module sale",
+      "ee-infinity-fission-reactor-equipment": "Réacteur à fission infini",
+      "ee-infinity-fission-reactor-equipment-recycling": "Recyclage de Réacteur à fission infini",
+      "ee-super-personal-roboport-equipment": "Super roboport personnel",
+      "ee-super-personal-roboport-equipment-recycling": "Recyclage de Super roboport personnel",
+      "ee-super-exoskeleton-equipment": "Super exosquelette",
+      "ee-super-exoskeleton-equipment-recycling": "Recyclage de Super exosquelette",
+      "ee-super-energy-shield-equipment": "Super bouclier d'énergie",
+      "ee-super-energy-shield-equipment-recycling": "Recyclage de Super bouclier d'énergie",
+      "ee-super-night-vision-equipment": "Super vision nocturne",
+      "ee-super-night-vision-equipment-recycling": "Recyclage de Super vision nocturne",
+      "ee-super-battery-equipment": "Super batterie personnelle",
+      "ee-super-battery-equipment-recycling": "Recyclage de Super batterie personnelle"
     },
     "descriptions": {
       "ammoniacal-solution-separation": "La [fluid=ammoniacal-solution] est prélevée par une [entity=offshore-pump] depuis les océans de [planet=aquilo].",
@@ -1140,7 +1297,8 @@
       "tiles": "Tuiles",
       "environment": "Environnement",
       "effects": "Effets",
-      "other": "Non triés"
+      "other": "Non triés",
+      "ee-tools": "Outils de test"
     }
   },
   "quality": {

+ 162 - 4
src/assets/data/2.0/i18n/ja.json

@@ -33,11 +33,13 @@
       "parameter-7": "パラメーター 7",
       "parameter-8": "パラメーター 8",
       "parameter-9": "パラメーター 9",
+      "ee-super-pump-speed-fluid": "Speedfluid",
       "fluid-unknown": "不明な流体"
     },
     "descriptions": {
       "thruster-fuel": "スラスターの液体燃料",
       "fusion-plasma": "[entity=fusion-reactor] の中で生成され、 [entity=fusion-generator] で消費される超高温のイオン。通常の流体ではないため、 [entity=pipe] で移動させることはできず、核融合炉と核融合発電機の間でのみ移動させることができます。\nプラズマは温度によってエネルギー量が決まります。\nプラズマの量は共に移動する冷却剤の量も示し、加熱された冷却剤はプラズマをエネルギーとして利用する際に放出されます。",
+      "ee-super-pump-speed-fluid": "If you're seeing this, turn back now! This is used by Editor Extensions to control the speed of the super pump, and has no usefulness elsewhere.",
       "fluid-unknown": "MODが削除されたためこの流体は利用できません。MODを再度有効にすれば復元されます。"
     }
   },
@@ -118,6 +120,7 @@
       "ice-platform": "氷のプラットフォーム",
       "foundation": "基盤",
       "cliff-explosives": "崖用爆薬",
+      "region-cloner_selection-tool": "Region Cloner Selection Tool",
       "repair-pack": "リペアキット",
       "blueprint": "建設計画",
       "deconstruction-planner": "解体プランナー",
@@ -366,6 +369,9 @@
       "artillery-targeting-remote": "遠方照準器",
       "item-unknown": "不明なアイテム",
       "no-item": "アイテムなし",
+      "rcalc-heat-dummy": "熱",
+      "rcalc-pollution-dummy": "汚染",
+      "rcalc-power-dummy": "電力",
       "electric-energy-interface": "電力インターフェイス",
       "linked-chest": "リンクされたチェスト",
       "proxy-container": "プロキシコンテナ",
@@ -379,10 +385,52 @@
       "infinity-cargo-wagon": "無限貨物車両",
       "infinity-chest": "無限チェスト",
       "infinity-pipe": "無限パイプ",
+      "rcalc-selection-tool": "Rate Calculator ツール",
       "selection-tool": "選択ツール",
       "simple-entity-with-force": "勢力を持つ一般エンティティ",
       "simple-entity-with-owner": "所有権を持つ一般エンティティ",
-      "burner-generator": "燃料式発電機"
+      "burner-generator": "燃料式発電機",
+      "ee-infinity-chest": "無限チェスト",
+      "ee-infinity-chest-active-provider": "無限アクティブ供給チェスト",
+      "ee-infinity-chest-passive-provider": "無限パッシブ供給チェスト",
+      "ee-infinity-chest-storage": "無限貯蔵チェスト",
+      "ee-infinity-chest-buffer": "無限バッファーチェスト",
+      "ee-infinity-chest-requester": "無限要求チェスト",
+      "ee-aggregate-chest": "Aggregate chest",
+      "ee-aggregate-chest-passive-provider": "Aggregate passive provider chest",
+      "ee-linked-chest": "Linked chest",
+      "ee-infinity-loader": "無限ローダー",
+      "ee-linked-belt": "Linked belt",
+      "ee-super-inserter": "スーパーインサータ",
+      "ee-infinity-pipe": "無限パイプ",
+      "ee-super-pump": "スーパーポンプ",
+      "ee-infinity-heat-pipe": "無限ヒートパイプ",
+      "ee-super-radar": "スーパーレーダー",
+      "ee-super-lab": "スーパー研究所",
+      "ee-infinity-accumulator": "無限蓄電池",
+      "ee-super-electric-pole": "スーパー電柱",
+      "ee-super-substation": "スーパー広域電柱",
+      "ee-super-locomotive": "スーパー機関車",
+      "ee-infinity-cargo-wagon": "無限貨車",
+      "ee-infinity-fluid-wagon": "無限タンク貨車",
+      "ee-super-fuel": "Super fuel",
+      "ee-super-roboport": "スーパーロボットステーション",
+      "ee-super-construction-robot": "スーパー建設ロボット",
+      "ee-super-logistic-robot": "スーパー物流ロボット",
+      "ee-super-beacon": "スーパービーコン",
+      "ee-super-speed-module": "Super speed module",
+      "ee-super-efficiency-module": "Super efficiency module",
+      "ee-super-productivity-module": "Super productivity module",
+      "ee-super-clean-module": "Super clean module",
+      "ee-super-slow-module": "Super slow module",
+      "ee-super-inefficiency-module": "Super inefficiency module",
+      "ee-super-dirty-module": "Super dirty module",
+      "ee-infinity-fission-reactor-equipment": "Infinity fission reactor",
+      "ee-super-personal-roboport-equipment": "Super personal roboport",
+      "ee-super-exoskeleton-equipment": "Super exoskeleton",
+      "ee-super-energy-shield-equipment": "Super energy shield",
+      "ee-super-night-vision-equipment": "Super night vision",
+      "ee-super-battery-equipment": "スーパー個人用バッテリー"
     },
     "descriptions": {
       "rail": "手動またはレールプランナーを使用して、直線レールを敷設できます。\n[font=default-semibold][color=#80cef0]左クリック[/color][/font] を使用して短いパスを直接敷設できます。\n[font=default-semibold][color=#80cef0]Shift + 左クリック[/color][/font] を使用して長いゴーストパスを敷設できます。\n[font=default-semibold][color=#80cef0]KEY-CODE-NOT-DEFINE-IN-HEADLESS-MODE[/color][/font] で地上と高架のパスを切り替えられます。",
@@ -456,7 +504,29 @@
       "green-wire": "[font=default-semibold][color=#80cef0]左クリック[/color][/font]で設備を回路ネットワークに接続します。",
       "red-wire": "[font=default-semibold][color=#80cef0]左クリック[/color][/font]で設備を回路ネットワークに接続します。",
       "artillery-targeting-remote": "画面または地図上でプレイヤーの手動による砲撃が可能となります。",
-      "item-unknown": "MODが削除されたためこのアイテムは利用できません。MODを再度有効にすれば復元されます。"
+      "item-unknown": "MODが削除されたためこのアイテムは利用できません。MODを再度有効にすれば復元されます。",
+      "ee-infinity-chest": "Creates or destroys items using customizable item filters.",
+      "ee-aggregate-chest": "Contains every item in the game.\n[color=255,57,48]Will cause performance issues if abused, use sparingly![/color]",
+      "ee-infinity-loader": "Creates or destroys items on a belt using customizable filters.",
+      "ee-linked-belt": "Instantly transports items to another linked belt.",
+      "ee-infinity-heat-pipe": "Creates or destroys a configurable amount of heat.",
+      "ee-infinity-accumulator": "Produces, drains, or stores a configurable amount of electric energy.",
+      "ee-infinity-cargo-wagon": "Creates or destroys items using customizable item filters (identical to infinity chest).",
+      "ee-infinity-fluid-wagon": "Creates or destroys fluids using a customizable fluid filter (identical to infinity pipe).\n[color=255,57,48]Will cause performance issues if abused, use sparingly![/color]",
+      "ee-super-fuel": "Nuclear fuel that lasts pretty much forever.",
+      "ee-super-speed-module": "Massively increases machine speed.",
+      "ee-super-efficiency-module": "Massively decreases machine energy consumption. Minimum energy consumption is 20%.",
+      "ee-super-productivity-module": "Massively increases machine productivity.",
+      "ee-super-clean-module": "Massively decreases machine pollution. Minimum pollution is 20%.",
+      "ee-super-slow-module": "Massively decreases machine speed. Minimum speed is 20%.",
+      "ee-super-inefficiency-module": "Massively increases machine energy consumption.",
+      "ee-super-dirty-module": "Massively increases machine pollution.",
+      "ee-infinity-fission-reactor-equipment": "Generates virtually unlimited power for your equipment.",
+      "ee-super-personal-roboport-equipment": "Personal robport with massive construction area and robot capacity.",
+      "ee-super-exoskeleton-equipment": "Very small and very quick exoskeleton.",
+      "ee-super-energy-shield-equipment": "Ridiculously overpowered energy shield, makes you practically immortal.",
+      "ee-super-night-vision-equipment": "Perfect night vision, you can see as if it's daytime.",
+      "ee-super-battery-equipment": "Ridiculously massive battery."
     }
   },
   "recipe": {
@@ -543,6 +613,7 @@
       "ice-platform": "氷のプラットフォーム",
       "foundation": "基盤",
       "cliff-explosives": "崖用爆薬",
+      "region-cloner_selection-tool-recycling": "Region Cloner Selection Tool (リサイクル)",
       "repair-pack": "リペアキット",
       "blueprint-recycling": "建設計画 (リサイクル)",
       "deconstruction-planner-recycling": "解体プランナー (リサイクル)",
@@ -1055,6 +1126,9 @@
       "railgun-ammo-recycling": "レールガン弾 (リサイクル)",
       "railgun-recycling": "レールガン (リサイクル)",
       "railgun-turret-recycling": "レールガンタレット (リサイクル)",
+      "rcalc-heat-dummy-recycling": "熱 (リサイクル)",
+      "rcalc-pollution-dummy-recycling": "汚染 (リサイクル)",
+      "rcalc-power-dummy-recycling": "電力 (リサイクル)",
       "recipe-unknown": "不明なレシピ",
       "recycler-recycling": "リサイクラー (リサイクル)",
       "refined-concrete-recycling": "鉄筋コンクリート (リサイクル)",
@@ -1116,10 +1190,93 @@
       "infinity-cargo-wagon-recycling": "無限貨物車両 (リサイクル)",
       "infinity-chest": "無限チェスト",
       "infinity-pipe": "無限パイプ",
+      "rcalc-selection-tool-recycling": "Rate Calculator ツール (リサイクル)",
       "selection-tool-recycling": "選択ツール (リサイクル)",
       "simple-entity-with-force-recycling": "勢力を持つ一般エンティティ (リサイクル)",
       "simple-entity-with-owner-recycling": "所有権を持つ一般エンティティ (リサイクル)",
-      "burner-generator-recycling": "燃料式発電機 (リサイクル)"
+      "burner-generator-recycling": "燃料式発電機 (リサイクル)",
+      "ee-infinity-chest": "無限チェスト",
+      "ee-infinity-chest-recycling": "無限チェスト (リサイクル)",
+      "ee-infinity-chest-active-provider": "無限アクティブ供給チェスト",
+      "ee-infinity-chest-active-provider-recycling": "無限アクティブ供給チェスト (リサイクル)",
+      "ee-infinity-chest-passive-provider": "無限パッシブ供給チェスト",
+      "ee-infinity-chest-passive-provider-recycling": "無限パッシブ供給チェスト (リサイクル)",
+      "ee-infinity-chest-storage": "無限貯蔵チェスト",
+      "ee-infinity-chest-storage-recycling": "無限貯蔵チェスト (リサイクル)",
+      "ee-infinity-chest-buffer": "無限バッファーチェスト",
+      "ee-infinity-chest-buffer-recycling": "無限バッファーチェスト (リサイクル)",
+      "ee-infinity-chest-requester": "無限要求チェスト",
+      "ee-infinity-chest-requester-recycling": "無限要求チェスト (リサイクル)",
+      "ee-aggregate-chest": "Aggregate chest",
+      "ee-aggregate-chest-recycling": "Aggregate chest (リサイクル)",
+      "ee-aggregate-chest-passive-provider": "Aggregate passive provider chest",
+      "ee-aggregate-chest-passive-provider-recycling": "Aggregate passive provider chest (リサイクル)",
+      "ee-linked-chest": "Linked chest",
+      "ee-linked-chest-recycling": "Linked chest (リサイクル)",
+      "ee-infinity-loader": "無限ローダー",
+      "ee-infinity-loader-recycling": "無限ローダー (リサイクル)",
+      "ee-linked-belt": "Linked belt",
+      "ee-linked-belt-recycling": "Linked belt (リサイクル)",
+      "ee-super-inserter": "スーパーインサータ",
+      "ee-super-inserter-recycling": "スーパーインサータ (リサイクル)",
+      "ee-infinity-pipe": "無限パイプ",
+      "ee-infinity-pipe-recycling": "無限パイプ (リサイクル)",
+      "ee-super-pump": "スーパーポンプ",
+      "ee-super-pump-recycling": "スーパーポンプ (リサイクル)",
+      "ee-infinity-heat-pipe": "無限ヒートパイプ",
+      "ee-infinity-heat-pipe-recycling": "無限ヒートパイプ (リサイクル)",
+      "ee-super-radar": "スーパーレーダー",
+      "ee-super-radar-recycling": "スーパーレーダー (リサイクル)",
+      "ee-super-lab": "スーパー研究所",
+      "ee-super-lab-recycling": "スーパー研究所 (リサイクル)",
+      "ee-infinity-accumulator": "無限蓄電池",
+      "ee-infinity-accumulator-recycling": "無限蓄電池 (リサイクル)",
+      "ee-super-electric-pole": "スーパー電柱",
+      "ee-super-electric-pole-recycling": "スーパー電柱 (リサイクル)",
+      "ee-super-substation": "スーパー広域電柱",
+      "ee-super-substation-recycling": "スーパー広域電柱 (リサイクル)",
+      "ee-super-locomotive": "スーパー機関車",
+      "ee-super-locomotive-recycling": "スーパー機関車 (リサイクル)",
+      "ee-infinity-cargo-wagon": "無限貨車",
+      "ee-infinity-cargo-wagon-recycling": "無限貨車 (リサイクル)",
+      "ee-infinity-fluid-wagon": "無限タンク貨車",
+      "ee-infinity-fluid-wagon-recycling": "無限タンク貨車 (リサイクル)",
+      "ee-super-fuel": "Super fuel",
+      "ee-super-fuel-recycling": "Super fuel (リサイクル)",
+      "ee-super-roboport": "スーパーロボットステーション",
+      "ee-super-roboport-recycling": "スーパーロボットステーション (リサイクル)",
+      "ee-super-construction-robot": "スーパー建設ロボット",
+      "ee-super-construction-robot-recycling": "スーパー建設ロボット (リサイクル)",
+      "ee-super-logistic-robot": "スーパー物流ロボット",
+      "ee-super-logistic-robot-recycling": "スーパー物流ロボット (リサイクル)",
+      "ee-super-beacon": "スーパービーコン",
+      "ee-super-beacon-recycling": "スーパービーコン (リサイクル)",
+      "ee-super-speed-module": "Super speed module",
+      "ee-super-speed-module-recycling": "Super speed module (リサイクル)",
+      "ee-super-efficiency-module": "Super efficiency module",
+      "ee-super-efficiency-module-recycling": "Super efficiency module (リサイクル)",
+      "ee-super-productivity-module": "Super productivity module",
+      "ee-super-productivity-module-recycling": "Super productivity module (リサイクル)",
+      "ee-super-clean-module": "Super clean module",
+      "ee-super-clean-module-recycling": "Super clean module (リサイクル)",
+      "ee-super-slow-module": "Super slow module",
+      "ee-super-slow-module-recycling": "Super slow module (リサイクル)",
+      "ee-super-inefficiency-module": "Super inefficiency module",
+      "ee-super-inefficiency-module-recycling": "Super inefficiency module (リサイクル)",
+      "ee-super-dirty-module": "Super dirty module",
+      "ee-super-dirty-module-recycling": "Super dirty module (リサイクル)",
+      "ee-infinity-fission-reactor-equipment": "Infinity fission reactor",
+      "ee-infinity-fission-reactor-equipment-recycling": "Infinity fission reactor (リサイクル)",
+      "ee-super-personal-roboport-equipment": "Super personal roboport",
+      "ee-super-personal-roboport-equipment-recycling": "Super personal roboport (リサイクル)",
+      "ee-super-exoskeleton-equipment": "Super exoskeleton",
+      "ee-super-exoskeleton-equipment-recycling": "Super exoskeleton (リサイクル)",
+      "ee-super-energy-shield-equipment": "Super energy shield",
+      "ee-super-energy-shield-equipment-recycling": "Super energy shield (リサイクル)",
+      "ee-super-night-vision-equipment": "Super night vision",
+      "ee-super-night-vision-equipment-recycling": "Super night vision (リサイクル)",
+      "ee-super-battery-equipment": "スーパー個人用バッテリー",
+      "ee-super-battery-equipment-recycling": "スーパー個人用バッテリー (リサイクル)"
     },
     "descriptions": {
       "ammoniacal-solution-separation": "[fluid=ammoniacal-solution] は [entity=offshore-pump] を [planet=aquilo] の海に使用すると得られます。",
@@ -1140,7 +1297,8 @@
       "tiles": "タイル",
       "environment": "環境",
       "effects": "効果",
-      "other": "その他"
+      "other": "その他",
+      "ee-tools": "Testing Tools"
     }
   },
   "quality": {

+ 1486 - 0
src/assets/data/2.0/i18n/zh-CH.json

@@ -0,0 +1,1486 @@
+{
+  "fluid": {
+    "names": {
+      "water": "Water",
+      "steam": "Steam",
+      "crude-oil": "Crude oil",
+      "petroleum-gas": "Petroleum gas",
+      "light-oil": "Light oil",
+      "heavy-oil": "Heavy oil",
+      "lubricant": "Lubricant",
+      "sulfuric-acid": "Sulfuric acid",
+      "thruster-fuel": "Thruster fuel",
+      "thruster-oxidizer": "Thruster oxidizer",
+      "lava": "Lava",
+      "molten-iron": "Molten iron",
+      "molten-copper": "Molten copper",
+      "holmium-solution": "Holmium solution",
+      "electrolyte": "Electrolyte",
+      "ammoniacal-solution": "Ammoniacal solution",
+      "ammonia": "Ammonia",
+      "fluorine": "Fluorine",
+      "fluoroketone-hot": "Fluoroketone (Hot)",
+      "fluoroketone-cold": "Fluoroketone (Cold)",
+      "lithium-brine": "Lithium brine",
+      "fusion-plasma": "Plasma",
+      "parameter-0": "Parameter 0",
+      "parameter-1": "Parameter 1",
+      "parameter-2": "Parameter 2",
+      "parameter-3": "Parameter 3",
+      "parameter-4": "Parameter 4",
+      "parameter-5": "Parameter 5",
+      "parameter-6": "Parameter 6",
+      "parameter-7": "Parameter 7",
+      "parameter-8": "Parameter 8",
+      "parameter-9": "Parameter 9",
+      "ee-super-pump-speed-fluid": "Speedfluid",
+      "fluid-unknown": "Unknown fluid"
+    },
+    "descriptions": {
+      "thruster-fuel": "Liquid thruster fuel",
+      "fusion-plasma": "Ultra-high-temperature ions generated in [entity=fusion-reactor] and consumed by a [entity=fusion-generator]. It is not a normal fluid and cannot be moved in [entity=pipe], it can only be moved through the Fusion reactor and Fusion generator.\nThe temperature of plasma determines its energy value.\nA quantity of plasma also represents that amount of coolant that travels with it, and the heated coolant is output by the Fusion generator as the plasma is used for energy.",
+      "ee-super-pump-speed-fluid": "If you're seeing this, turn back now! This is used by Editor Extensions to control the speed of the super pump, and has no usefulness elsewhere.",
+      "fluid-unknown": "This fluid is not available due to mod removal, it will be restored if the mod is re-enabled."
+    }
+  },
+  "item": {
+    "names": {
+      "wooden-chest": "Wooden chest",
+      "iron-chest": "Iron chest",
+      "steel-chest": "Steel chest",
+      "storage-tank": "Storage tank",
+      "transport-belt": "Transport belt",
+      "fast-transport-belt": "Fast transport belt",
+      "express-transport-belt": "Express transport belt",
+      "turbo-transport-belt": "Turbo transport belt",
+      "underground-belt": "Underground belt",
+      "fast-underground-belt": "Fast underground belt",
+      "express-underground-belt": "Express underground belt",
+      "turbo-underground-belt": "Turbo underground belt",
+      "splitter": "Splitter",
+      "fast-splitter": "Fast splitter",
+      "express-splitter": "Express splitter",
+      "turbo-splitter": "Turbo splitter",
+      "loader": "Loader",
+      "fast-loader": "Fast loader",
+      "express-loader": "Express loader",
+      "turbo-loader": "Turbo loader",
+      "burner-inserter": "Burner inserter",
+      "inserter": "Inserter",
+      "long-handed-inserter": "Long-handed inserter",
+      "fast-inserter": "Fast inserter",
+      "bulk-inserter": "Bulk inserter",
+      "stack-inserter": "Stack inserter",
+      "small-electric-pole": "Small electric pole",
+      "medium-electric-pole": "Medium electric pole",
+      "big-electric-pole": "Big electric pole",
+      "substation": "Substation",
+      "pipe": "Pipe",
+      "pipe-to-ground": "Pipe to ground",
+      "pump": "Pump",
+      "rail": "Rail",
+      "rail-ramp": "Rail ramp",
+      "rail-support": "Rail support",
+      "train-stop": "Train stop",
+      "rail-signal": "Rail signal",
+      "rail-chain-signal": "Rail chain signal",
+      "locomotive": "Locomotive",
+      "cargo-wagon": "Cargo wagon",
+      "fluid-wagon": "Fluid wagon",
+      "artillery-wagon": "Artillery wagon",
+      "car": "Car",
+      "tank": "Tank",
+      "spidertron": "Spidertron",
+      "logistic-robot": "Logistic robot",
+      "construction-robot": "Construction robot",
+      "active-provider-chest": "Active provider chest",
+      "passive-provider-chest": "Passive provider chest",
+      "storage-chest": "Storage chest",
+      "buffer-chest": "Buffer chest",
+      "requester-chest": "Requester chest",
+      "roboport": "Roboport",
+      "small-lamp": "Lamp",
+      "arithmetic-combinator": "Arithmetic combinator",
+      "decider-combinator": "Decider combinator",
+      "selector-combinator": "Selector combinator",
+      "constant-combinator": "Constant combinator",
+      "power-switch": "Power switch",
+      "programmable-speaker": "Programmable speaker",
+      "display-panel": "Display panel",
+      "stone-brick": "Stone brick",
+      "concrete": "Concrete",
+      "hazard-concrete": "Hazard concrete",
+      "refined-concrete": "Refined concrete",
+      "refined-hazard-concrete": "Refined hazard concrete",
+      "landfill": "Landfill",
+      "artificial-yumako-soil": "Artificial yumako soil",
+      "overgrowth-yumako-soil": "Overgrowth yumako soil",
+      "artificial-jellynut-soil": "Artificial jellynut soil",
+      "overgrowth-jellynut-soil": "Overgrowth jellynut soil",
+      "ice-platform": "Ice platform",
+      "foundation": "Foundation",
+      "cliff-explosives": "Cliff explosives",
+      "region-cloner_selection-tool": "Region Cloner Selection Tool",
+      "repair-pack": "Repair pack",
+      "blueprint": "Blueprint",
+      "deconstruction-planner": "Deconstruction planner",
+      "upgrade-planner": "Upgrade planner",
+      "blueprint-book": "Blueprint book",
+      "copy-paste-tool": "Copy paste tool",
+      "cut-paste-tool": "Cut paste tool",
+      "boiler": "Boiler",
+      "steam-engine": "Steam engine",
+      "solar-panel": "Solar panel",
+      "accumulator": "Accumulator",
+      "nuclear-reactor": "Nuclear reactor",
+      "heat-pipe": "Heat pipe",
+      "heat-exchanger": "Heat exchanger",
+      "steam-turbine": "Steam turbine",
+      "fusion-reactor": "Fusion reactor",
+      "fusion-generator": "Fusion generator",
+      "burner-mining-drill": "Burner mining drill",
+      "electric-mining-drill": "Electric mining drill",
+      "big-mining-drill": "Big mining drill",
+      "offshore-pump": "Offshore pump",
+      "pumpjack": "Pumpjack",
+      "stone-furnace": "Stone furnace",
+      "steel-furnace": "Steel furnace",
+      "electric-furnace": "Electric furnace",
+      "foundry": "Foundry",
+      "recycler": "Recycler",
+      "agricultural-tower": "Agricultural tower",
+      "biochamber": "Biochamber",
+      "captive-biter-spawner": "Captive biter spawner",
+      "assembling-machine-1": "Assembling machine 1",
+      "assembling-machine-2": "Assembling machine 2",
+      "assembling-machine-3": "Assembling machine 3",
+      "oil-refinery": "Oil refinery",
+      "chemical-plant": "Chemical plant",
+      "centrifuge": "Centrifuge",
+      "electromagnetic-plant": "Electromagnetic plant",
+      "cryogenic-plant": "Cryogenic plant",
+      "lab": "Lab",
+      "biolab": "Biolab",
+      "lightning-rod": "Lightning rod",
+      "lightning-collector": "Lightning collector",
+      "heating-tower": "Heating tower",
+      "beacon": "Beacon",
+      "speed-module": "Speed module",
+      "speed-module-2": "Speed module 2",
+      "speed-module-3": "Speed module 3",
+      "efficiency-module": "Efficiency module",
+      "efficiency-module-2": "Efficiency module 2",
+      "efficiency-module-3": "Efficiency module 3",
+      "productivity-module": "Productivity module",
+      "productivity-module-2": "Productivity module 2",
+      "productivity-module-3": "Productivity module 3",
+      "quality-module": "Quality module",
+      "quality-module-2": "Quality module 2",
+      "quality-module-3": "Quality module 3",
+      "empty-module-slot": "Empty module slot",
+      "wood": "Wood",
+      "coal": "Coal",
+      "stone": "Stone",
+      "iron-ore": "Iron ore",
+      "copper-ore": "Copper ore",
+      "uranium-ore": "Uranium ore",
+      "raw-fish": "Raw fish",
+      "ice": "Ice",
+      "iron-plate": "Iron plate",
+      "copper-plate": "Copper plate",
+      "steel-plate": "Steel plate",
+      "solid-fuel": "Solid fuel",
+      "plastic-bar": "Plastic bar",
+      "sulfur": "Sulfur",
+      "battery": "Battery",
+      "explosives": "Explosives",
+      "carbon": "Carbon",
+      "water-barrel": "Water barrel",
+      "crude-oil-barrel": "Crude oil barrel",
+      "petroleum-gas-barrel": "Petroleum gas barrel",
+      "light-oil-barrel": "Light oil barrel",
+      "heavy-oil-barrel": "Heavy oil barrel",
+      "lubricant-barrel": "Lubricant barrel",
+      "sulfuric-acid-barrel": "Sulfuric acid barrel",
+      "fluoroketone-hot-barrel": "Fluoroketone (Hot) barrel",
+      "fluoroketone-cold-barrel": "Fluoroketone (Cold) barrel",
+      "iron-gear-wheel": "Iron gear wheel",
+      "iron-stick": "Iron stick",
+      "copper-cable": "Copper cable",
+      "barrel": "Barrel",
+      "electronic-circuit": "Electronic circuit",
+      "advanced-circuit": "Advanced circuit",
+      "processing-unit": "Processing unit",
+      "engine-unit": "Engine unit",
+      "electric-engine-unit": "Electric engine unit",
+      "flying-robot-frame": "Flying robot frame",
+      "low-density-structure": "Low density structure",
+      "rocket-fuel": "Rocket fuel",
+      "uranium-235": "Uranium-235",
+      "uranium-238": "Uranium-238",
+      "uranium-fuel-cell": "Uranium fuel cell",
+      "depleted-uranium-fuel-cell": "Depleted uranium fuel cell",
+      "nuclear-fuel": "Nuclear fuel",
+      "calcite": "Calcite",
+      "tungsten-ore": "Tungsten ore",
+      "tungsten-carbide": "Tungsten carbide",
+      "tungsten-plate": "Tungsten plate",
+      "scrap": "Scrap",
+      "holmium-ore": "Holmium ore",
+      "holmium-plate": "Holmium plate",
+      "superconductor": "Superconductor",
+      "supercapacitor": "Supercapacitor",
+      "yumako-seed": "Yumako seed",
+      "jellynut-seed": "Jellynut seed",
+      "yumako": "Yumako",
+      "jellynut": "Jellynut",
+      "iron-bacteria": "Iron bacteria",
+      "copper-bacteria": "Copper bacteria",
+      "spoilage": "Spoilage",
+      "nutrients": "Nutrients",
+      "bioflux": "Bioflux",
+      "yumako-mash": "Yumako mash",
+      "jelly": "Jelly",
+      "carbon-fiber": "Carbon fiber",
+      "biter-egg": "Biter egg",
+      "pentapod-egg": "Pentapod egg",
+      "tree-seed": "Tree seed",
+      "lithium": "Lithium",
+      "lithium-plate": "Lithium plate",
+      "quantum-processor": "Quantum processor",
+      "fusion-power-cell": "Fusion power cell",
+      "automation-science-pack": "Automation science pack",
+      "logistic-science-pack": "Logistic science pack",
+      "military-science-pack": "Military science pack",
+      "chemical-science-pack": "Chemical science pack",
+      "production-science-pack": "Production science pack",
+      "utility-science-pack": "Utility science pack",
+      "space-science-pack": "Space science pack",
+      "metallurgic-science-pack": "Metallurgic science pack",
+      "agricultural-science-pack": "Agricultural science pack",
+      "electromagnetic-science-pack": "Electromagnetic science pack",
+      "cryogenic-science-pack": "Cryogenic science pack",
+      "promethium-science-pack": "Promethium science pack",
+      "coin": "Coin",
+      "science": "Science",
+      "rocket-silo": "Rocket silo",
+      "rocket-part": "Rocket part",
+      "cargo-landing-pad": "Cargo landing pad",
+      "space-platform-foundation": "Space platform foundation",
+      "cargo-bay": "Cargo bay",
+      "asteroid-collector": "Asteroid collector",
+      "crusher": "Crusher",
+      "thruster": "Thruster",
+      "space-platform-starter-pack": "Space platform starter pack",
+      "space-platform-hub": "Space platform hub",
+      "metallic-asteroid-chunk": "Metallic asteroid chunk",
+      "carbonic-asteroid-chunk": "Carbonic asteroid chunk",
+      "oxide-asteroid-chunk": "Oxide asteroid chunk",
+      "promethium-asteroid-chunk": "Promethium asteroid chunk",
+      "pistol": "Pistol",
+      "submachine-gun": "Submachine gun",
+      "tank-machine-gun": "Vehicle machine gun",
+      "vehicle-machine-gun": "Vehicle machine gun",
+      "railgun": "Railgun",
+      "teslagun": "Tesla gun",
+      "tank-flamethrower": "Vehicle flamethrower",
+      "shotgun": "Shotgun",
+      "combat-shotgun": "Combat shotgun",
+      "rocket-launcher": "Rocket launcher",
+      "flamethrower": "Flamethrower",
+      "artillery-wagon-cannon": "Artillery cannon",
+      "spidertron-rocket-launcher-1": "Spidertron rocket launcher",
+      "spidertron-rocket-launcher-2": "Spidertron rocket launcher",
+      "spidertron-rocket-launcher-3": "Spidertron rocket launcher",
+      "spidertron-rocket-launcher-4": "Spidertron rocket launcher",
+      "tank-cannon": "Tank cannon",
+      "firearm-magazine": "Firearm magazine",
+      "piercing-rounds-magazine": "Piercing rounds magazine",
+      "uranium-rounds-magazine": "Uranium rounds magazine",
+      "shotgun-shell": "Shotgun shells",
+      "piercing-shotgun-shell": "Piercing shotgun shells",
+      "cannon-shell": "Cannon shell",
+      "explosive-cannon-shell": "Explosive cannon shell",
+      "uranium-cannon-shell": "Uranium cannon shell",
+      "explosive-uranium-cannon-shell": "Explosive uranium cannon shell",
+      "artillery-shell": "Artillery shell",
+      "rocket": "Rocket",
+      "explosive-rocket": "Explosive rocket",
+      "atomic-bomb": "Atomic bomb",
+      "capture-robot-rocket": "Capture bot rocket",
+      "flamethrower-ammo": "Flamethrower ammo",
+      "railgun-ammo": "Railgun ammo",
+      "tesla-ammo": "Tesla ammo",
+      "grenade": "Grenade",
+      "cluster-grenade": "Cluster grenade",
+      "poison-capsule": "Poison capsule",
+      "slowdown-capsule": "Slowdown capsule",
+      "defender-capsule": "Defender capsule",
+      "distractor-capsule": "Distractor capsule",
+      "destroyer-capsule": "Destroyer capsule",
+      "light-armor": "Light armor",
+      "heavy-armor": "Heavy armor",
+      "modular-armor": "Modular armor",
+      "power-armor": "Power armor",
+      "power-armor-mk2": "Power armor MK2",
+      "mech-armor": "Mech armor",
+      "solar-panel-equipment": "Portable solar panel",
+      "fission-reactor-equipment": "Portable fission reactor",
+      "fusion-reactor-equipment": "Portable fusion reactor",
+      "battery-equipment": "Personal battery",
+      "battery-mk2-equipment": "Personal battery MK2",
+      "battery-mk3-equipment": "Personal battery MK3",
+      "belt-immunity-equipment": "Belt immunity equipment",
+      "exoskeleton-equipment": "Exoskeleton",
+      "personal-roboport-equipment": "Personal roboport",
+      "personal-roboport-mk2-equipment": "Personal roboport MK2",
+      "night-vision-equipment": "Nightvision",
+      "toolbelt-equipment": "Toolbelt equipment",
+      "energy-shield-equipment": "Energy shield",
+      "energy-shield-mk2-equipment": "Energy shield MK2",
+      "personal-laser-defense-equipment": "Personal laser defense",
+      "discharge-defense-equipment": "Discharge defense",
+      "stone-wall": "Wall",
+      "gate": "Gate",
+      "radar": "Radar",
+      "land-mine": "Land mine",
+      "gun-turret": "Gun turret",
+      "laser-turret": "Laser turret",
+      "flamethrower-turret": "Flamethrower turret",
+      "artillery-turret": "Artillery turret",
+      "rocket-turret": "Rocket turret",
+      "tesla-turret": "Tesla turret",
+      "railgun-turret": "Railgun turret",
+      "parameter-0": "Parameter 0",
+      "parameter-1": "Parameter 1",
+      "parameter-2": "Parameter 2",
+      "parameter-3": "Parameter 3",
+      "parameter-4": "Parameter 4",
+      "parameter-5": "Parameter 5",
+      "parameter-6": "Parameter 6",
+      "parameter-7": "Parameter 7",
+      "parameter-8": "Parameter 8",
+      "parameter-9": "Parameter 9",
+      "copper-wire": "Copper wire",
+      "green-wire": "Green wire",
+      "red-wire": "Red wire",
+      "spidertron-remote": "Spidertron remote",
+      "discharge-defense-remote": "Discharge defense remote",
+      "artillery-targeting-remote": "Artillery targeting remote",
+      "item-unknown": "Unknown item",
+      "no-item": "No item",
+      "rcalc-heat-dummy": "Heat",
+      "rcalc-pollution-dummy": "Pollution",
+      "rcalc-power-dummy": "Power",
+      "electric-energy-interface": "Electric energy interface",
+      "linked-chest": "Linked chest",
+      "proxy-container": "Proxy container",
+      "bottomless-chest": "Bottomless chest",
+      "heat-interface": "Heat interface",
+      "lane-splitter": "Lane splitter",
+      "linked-belt": "Linked belt",
+      "one-way-valve": "One-way valve",
+      "overflow-valve": "Overflow valve",
+      "top-up-valve": "Top-up valve",
+      "infinity-cargo-wagon": "Infinity cargo wagon",
+      "infinity-chest": "Infinity chest",
+      "infinity-pipe": "Infinity pipe",
+      "rcalc-selection-tool": "Rate Calculator selector",
+      "selection-tool": "Selection tool",
+      "simple-entity-with-force": "Simple entity with force",
+      "simple-entity-with-owner": "Simple entity with owner",
+      "burner-generator": "Burner generator",
+      "ee-infinity-chest": "Infinity chest",
+      "ee-infinity-chest-active-provider": "Infinity active provider chest",
+      "ee-infinity-chest-passive-provider": "Infinity passive provider chest",
+      "ee-infinity-chest-storage": "Infinity storage chest",
+      "ee-infinity-chest-buffer": "Infinity buffer chest",
+      "ee-infinity-chest-requester": "Infinity requester chest",
+      "ee-aggregate-chest": "Aggregate chest",
+      "ee-aggregate-chest-passive-provider": "Aggregate passive provider chest",
+      "ee-linked-chest": "Linked chest",
+      "ee-infinity-loader": "Infinity loader",
+      "ee-linked-belt": "Linked belt",
+      "ee-super-inserter": "Super inserter",
+      "ee-infinity-pipe": "Infinity pipe",
+      "ee-super-pump": "Super pump",
+      "ee-infinity-heat-pipe": "Infinity heat pipe",
+      "ee-super-radar": "Super radar",
+      "ee-super-lab": "Super lab",
+      "ee-infinity-accumulator": "Infinity accumulator",
+      "ee-super-electric-pole": "Super electric pole",
+      "ee-super-substation": "Super substation",
+      "ee-super-locomotive": "Super locomotive",
+      "ee-infinity-cargo-wagon": "Infinity cargo wagon",
+      "ee-infinity-fluid-wagon": "Infinity fluid wagon",
+      "ee-super-fuel": "Super fuel",
+      "ee-super-roboport": "Super roboport",
+      "ee-super-construction-robot": "Super construction robot",
+      "ee-super-logistic-robot": "Super logistic robot",
+      "ee-super-beacon": "Super beacon",
+      "ee-super-speed-module": "Super speed module",
+      "ee-super-efficiency-module": "Super efficiency module",
+      "ee-super-productivity-module": "Super productivity module",
+      "ee-super-clean-module": "Super clean module",
+      "ee-super-slow-module": "Super slow module",
+      "ee-super-inefficiency-module": "Super inefficiency module",
+      "ee-super-dirty-module": "Super dirty module",
+      "ee-infinity-fission-reactor-equipment": "Infinity fission reactor",
+      "ee-super-personal-roboport-equipment": "Super personal roboport",
+      "ee-super-exoskeleton-equipment": "Super exoskeleton",
+      "ee-super-energy-shield-equipment": "Super energy shield",
+      "ee-super-night-vision-equipment": "Super night vision",
+      "ee-super-battery-equipment": "Super personal battery"
+    },
+    "descriptions": {
+      "rail": "Use to build straight rails manually or through the rail planner.\n[font=default-semibold][color=#80cef0]Left-click[/color][/font] to build short paths directly.\n[font=default-semibold][color=#80cef0]Shift + Left-click[/color][/font] to place long ghost paths.\n[font=default-semibold][color=#80cef0]KEY-CODE-NOT-DEFINE-IN-HEADLESS-MODE[/color][/font] to switch between ground and elevated paths.",
+      "landfill": "Can be placed on water to create terrain you can build on.",
+      "artificial-yumako-soil": "Must be placed over [tile=wetland-yumako].",
+      "overgrowth-yumako-soil": "Soil for [entity=yumako-tree] placeable anywhere in the green biome.",
+      "artificial-jellynut-soil": "Must be placed over [tile=wetland-jellynut].",
+      "overgrowth-jellynut-soil": "Soil for [entity=jellystem] placeable anywhere in the red biome.",
+      "ice-platform": "A stable floating platform of ice for sufficiently cold planets.",
+      "foundation": "Engineered structural foundation with heat shielding and deep screw piles. Can be placed on lava, oil ocean, and most water.",
+      "repair-pack": "Used to repair friendly entities.",
+      "blueprint": "Save designs for automated construction.",
+      "deconstruction-planner": "Marks items for deconstruction by construction robots.",
+      "upgrade-planner": "Marks items for upgrade by construction robots.",
+      "blueprint-book": "Stores blueprints and similar items.",
+      "speed-module": "Increases machine speed at a cost of increased energy consumption.",
+      "speed-module-2": "Increases machine speed at a cost of increased energy consumption.",
+      "speed-module-3": "Increases machine speed at a cost of increased energy consumption.",
+      "efficiency-module": "Decreases machine energy consumption. Minimum energy consumption is 20%.",
+      "efficiency-module-2": "Decreases machine energy consumption. Minimum energy consumption is 20%.",
+      "efficiency-module-3": "Decreases machine energy consumption. Minimum energy consumption is 20%.",
+      "productivity-module": "Machine will create extra products at a cost of increased energy consumption and reduced speed.\nUsable only on intermediate products.",
+      "productivity-module-2": "Machine will create extra products at a cost of increased energy consumption and reduced speed.\nUsable only on intermediate products.",
+      "productivity-module-3": "Machine will create extra products at a cost of increased energy consumption and reduced speed.\nUsable only on intermediate products.",
+      "quality-module": "Allows machine to make higher quality products.",
+      "quality-module-2": "Allows machine to make higher quality products.",
+      "quality-module-3": "Allows machine to make higher quality products.",
+      "empty-module-slot": "An empty module slot in a machine. Used in upgrade planners to install new modules or uninstall existing modules.",
+      "yumako-seed": "Can be planted on yumako soil.",
+      "jellynut-seed": "Can be planted on jellynut soil.",
+      "yumako": "Nutritious farmable crop. Grants burst of health regeneration when eaten.",
+      "jellynut": "Slimy farmable crop. Grants burst of movement speed when eaten.",
+      "bioflux": "Highly nutritious blend of Gleba crops. Grants burst of both health regeneration and movement speed when eaten.",
+      "yumako-mash": "Grants burst of health regeneration when eaten.",
+      "jelly": "Grants burst of movement speed when eaten.",
+      "automation-science-pack": "Used by labs for research.",
+      "logistic-science-pack": "Used by labs for research.",
+      "military-science-pack": "Used by labs for research.",
+      "chemical-science-pack": "Used by labs for research.",
+      "production-science-pack": "Used by labs for research.",
+      "utility-science-pack": "Used by labs for research.",
+      "space-science-pack": "Used by labs for research. Obtained by processing asteroids in space.",
+      "metallurgic-science-pack": "Used by labs for research.",
+      "agricultural-science-pack": "Used by labs for research.",
+      "electromagnetic-science-pack": "Used by labs for research.",
+      "cryogenic-science-pack": "Used by labs for research.",
+      "promethium-science-pack": "Used by labs for research.",
+      "science": "Represents the overall research output.",
+      "space-platform-foundation": "Can be placed in space to extend existing space platforms.",
+      "space-platform-starter-pack": "Contains everything needed to set up a space platform foundation.",
+      "metallic-asteroid-chunk": "An asteroid chunk with high metal content.",
+      "carbonic-asteroid-chunk": "An asteroid chunk with high carbon content.",
+      "oxide-asteroid-chunk": "An asteroid chunk with high oxygen content.",
+      "promethium-asteroid-chunk": "An asteroid chunk only found when approaching the shattered planet.",
+      "capture-robot-rocket": "Grapples on and captures the targeted [entity=biter-spawner] into [entity=captive-biter-spawner].",
+      "railgun-ammo": "Used to break even the biggest asteroids.",
+      "slowdown-capsule": "Reduces the movement speed of affected enemies.",
+      "solar-panel-equipment": "Provides power for equipment modules.",
+      "fission-reactor-equipment": "Provides power for equipment modules.",
+      "fusion-reactor-equipment": "Provides power for equipment modules.",
+      "belt-immunity-equipment": "Prevents belts from moving the character.",
+      "exoskeleton-equipment": "Increases your movement speed.",
+      "personal-roboport-equipment": "Allows construction bots to work from your inventory.",
+      "personal-roboport-mk2-equipment": "Allows construction bots to work from your inventory.",
+      "night-vision-equipment": "Allows you to see more clearly in darkness.",
+      "energy-shield-equipment": "Provides an energy shield to protect the character.",
+      "energy-shield-mk2-equipment": "Provides an energy shield to protect the character.",
+      "discharge-defense-equipment": "Damages, pushes back and stuns nearby enemies when activated using the remote.",
+      "land-mine": "Explodes when enemies are nearby, damaging and stunning them.",
+      "copper-wire": "Used to manually connect and disconnect electric poles and power switches with [font=default-semibold][color=#80cef0]Left-click[/color][/font].",
+      "green-wire": "Used to connect machines to the circuit network using [font=default-semibold][color=#80cef0]Left-click[/color][/font].",
+      "red-wire": "Used to connect machines to the circuit network using [font=default-semibold][color=#80cef0]Left-click[/color][/font].",
+      "artillery-targeting-remote": "Allows firing artillery manually from the map or the world.",
+      "item-unknown": "This item is not available due to mod removal, it will be restored if the mod is re-enabled.",
+      "ee-infinity-chest": "Creates or destroys items using customizable item filters.",
+      "ee-aggregate-chest": "Contains every item in the game.\n[color=255,57,48]Will cause performance issues if abused, use sparingly![/color]",
+      "ee-infinity-loader": "Creates or destroys items on a belt using customizable filters.",
+      "ee-linked-belt": "Instantly transports items to another linked belt.",
+      "ee-infinity-heat-pipe": "Creates or destroys a configurable amount of heat.",
+      "ee-infinity-accumulator": "Produces, drains, or stores a configurable amount of electric energy.",
+      "ee-infinity-cargo-wagon": "Creates or destroys items using customizable item filters (identical to infinity chest).",
+      "ee-infinity-fluid-wagon": "Creates or destroys fluids using a customizable fluid filter (identical to infinity pipe).\n[color=255,57,48]Will cause performance issues if abused, use sparingly![/color]",
+      "ee-super-fuel": "Nuclear fuel that lasts pretty much forever.",
+      "ee-super-speed-module": "Massively increases machine speed.",
+      "ee-super-efficiency-module": "Massively decreases machine energy consumption. Minimum energy consumption is 20%.",
+      "ee-super-productivity-module": "Massively increases machine productivity.",
+      "ee-super-clean-module": "Massively decreases machine pollution. Minimum pollution is 20%.",
+      "ee-super-slow-module": "Massively decreases machine speed. Minimum speed is 20%.",
+      "ee-super-inefficiency-module": "Massively increases machine energy consumption.",
+      "ee-super-dirty-module": "Massively increases machine pollution.",
+      "ee-infinity-fission-reactor-equipment": "Generates virtually unlimited power for your equipment.",
+      "ee-super-personal-roboport-equipment": "Personal robport with massive construction area and robot capacity.",
+      "ee-super-exoskeleton-equipment": "Very small and very quick exoskeleton.",
+      "ee-super-energy-shield-equipment": "Ridiculously overpowered energy shield, makes you practically immortal.",
+      "ee-super-night-vision-equipment": "Perfect night vision, you can see as if it's daytime.",
+      "ee-super-battery-equipment": "Ridiculously massive battery."
+    }
+  },
+  "recipe": {
+    "names": {
+      "wooden-chest": "Wooden chest",
+      "iron-chest": "Iron chest",
+      "steel-chest": "Steel chest",
+      "storage-tank": "Storage tank",
+      "transport-belt": "Transport belt",
+      "fast-transport-belt": "Fast transport belt",
+      "express-transport-belt": "Express transport belt",
+      "turbo-transport-belt": "Turbo transport belt",
+      "underground-belt": "Underground belt",
+      "fast-underground-belt": "Fast underground belt",
+      "express-underground-belt": "Express underground belt",
+      "turbo-underground-belt": "Turbo underground belt",
+      "splitter": "Splitter",
+      "fast-splitter": "Fast splitter",
+      "express-splitter": "Express splitter",
+      "turbo-splitter": "Turbo splitter",
+      "loader": "Loader",
+      "fast-loader": "Fast loader",
+      "express-loader": "Express loader",
+      "turbo-loader": "Turbo loader",
+      "burner-inserter": "Burner inserter",
+      "inserter": "Inserter",
+      "long-handed-inserter": "Long-handed inserter",
+      "fast-inserter": "Fast inserter",
+      "bulk-inserter": "Bulk inserter",
+      "stack-inserter": "Stack inserter",
+      "small-electric-pole": "Small electric pole",
+      "medium-electric-pole": "Medium electric pole",
+      "big-electric-pole": "Big electric pole",
+      "substation": "Substation",
+      "pipe": "Pipe",
+      "pipe-to-ground": "Pipe to ground",
+      "casting-pipe": "Casting pipe",
+      "casting-pipe-to-ground": "Casting pipe to ground",
+      "pump": "Pump",
+      "rail": "Rail",
+      "rail-ramp": "Rail ramp",
+      "rail-support": "Rail support",
+      "train-stop": "Train stop",
+      "rail-signal": "Rail signal",
+      "rail-chain-signal": "Rail chain signal",
+      "locomotive": "Locomotive",
+      "cargo-wagon": "Cargo wagon",
+      "fluid-wagon": "Fluid wagon",
+      "artillery-wagon": "Artillery wagon",
+      "car": "Car",
+      "tank": "Tank",
+      "spidertron": "Spidertron",
+      "logistic-robot": "Logistic robot",
+      "construction-robot": "Construction robot",
+      "active-provider-chest": "Active provider chest",
+      "passive-provider-chest": "Passive provider chest",
+      "storage-chest": "Storage chest",
+      "buffer-chest": "Buffer chest",
+      "requester-chest": "Requester chest",
+      "roboport": "Roboport",
+      "small-lamp": "Lamp",
+      "arithmetic-combinator": "Arithmetic combinator",
+      "decider-combinator": "Decider combinator",
+      "selector-combinator": "Selector combinator",
+      "constant-combinator": "Constant combinator",
+      "power-switch": "Power switch",
+      "programmable-speaker": "Programmable speaker",
+      "display-panel": "Display panel",
+      "stone-brick": "Stone brick",
+      "stone-brick-recycling": "Stone brick recycling",
+      "stone-wall-recycling": "Wall recycling",
+      "concrete": "Concrete",
+      "hazard-concrete-recycling": "Hazard concrete recycling",
+      "hazard-concrete": "Hazard concrete",
+      "refined-concrete": "Refined concrete",
+      "refined-hazard-concrete-recycling": "Refined hazard concrete recycling",
+      "refined-hazard-concrete": "Refined hazard concrete",
+      "landfill": "Landfill",
+      "landfill-recycling": "Landfill recycling",
+      "artificial-yumako-soil": "Artificial yumako soil",
+      "overgrowth-yumako-soil": "Overgrowth yumako soil",
+      "artificial-jellynut-soil": "Artificial jellynut soil",
+      "overgrowth-jellynut-soil": "Overgrowth jellynut soil",
+      "ice-platform": "Ice platform",
+      "foundation": "Foundation",
+      "cliff-explosives": "Cliff explosives",
+      "region-cloner_selection-tool-recycling": "Region Cloner Selection Tool recycling",
+      "repair-pack": "Repair pack",
+      "blueprint-recycling": "Blueprint recycling",
+      "deconstruction-planner-recycling": "Deconstruction planner recycling",
+      "upgrade-planner-recycling": "Upgrade planner recycling",
+      "blueprint-book-recycling": "Blueprint book recycling",
+      "boiler": "Boiler",
+      "steam-engine": "Steam engine",
+      "solar-panel": "Solar panel",
+      "accumulator": "Accumulator",
+      "nuclear-reactor": "Nuclear reactor",
+      "heat-pipe": "Heat pipe",
+      "heat-exchanger": "Heat exchanger",
+      "steam-turbine": "Steam turbine",
+      "fusion-reactor": "Fusion reactor",
+      "fusion-generator": "Fusion generator",
+      "burner-mining-drill": "Burner mining drill",
+      "electric-mining-drill": "Electric mining drill",
+      "big-mining-drill": "Big mining drill",
+      "offshore-pump": "Offshore pump",
+      "pumpjack": "Pumpjack",
+      "stone-furnace": "Stone furnace",
+      "steel-furnace": "Steel furnace",
+      "electric-furnace": "Electric furnace",
+      "foundry": "Foundry",
+      "recycler": "Recycler",
+      "agricultural-tower": "Agricultural tower",
+      "biochamber": "Biochamber",
+      "captive-biter-spawner": "Captive biter spawner",
+      "captive-biter-spawner-recycling": "Captive biter spawner recycling",
+      "assembling-machine-1": "Assembling machine 1",
+      "assembling-machine-2": "Assembling machine 2",
+      "assembling-machine-3": "Assembling machine 3",
+      "oil-refinery": "Oil refinery",
+      "chemical-plant": "Chemical plant",
+      "centrifuge": "Centrifuge",
+      "electromagnetic-plant": "Electromagnetic plant",
+      "cryogenic-plant": "Cryogenic plant",
+      "lab": "Lab",
+      "biolab": "Biolab",
+      "biolab-recycling": "Biolab recycling",
+      "lightning-rod": "Lightning rod",
+      "lightning-collector": "Lightning collector",
+      "heating-tower": "Heating tower",
+      "beacon": "Beacon",
+      "speed-module": "Speed module",
+      "speed-module-2": "Speed module 2",
+      "speed-module-3": "Speed module 3",
+      "efficiency-module": "Efficiency module",
+      "efficiency-module-2": "Efficiency module 2",
+      "efficiency-module-3": "Efficiency module 3",
+      "productivity-module": "Productivity module",
+      "productivity-module-2": "Productivity module 2",
+      "productivity-module-3": "Productivity module 3",
+      "quality-module": "Quality module",
+      "quality-module-2": "Quality module 2",
+      "quality-module-3": "Quality module 3",
+      "empty-module-slot-recycling": "Empty module slot recycling",
+      "basic-oil-processing": "Basic oil processing",
+      "advanced-oil-processing": "Advanced oil processing",
+      "simple-coal-liquefaction": "Simple coal liquefaction",
+      "coal-liquefaction": "Coal liquefaction",
+      "heavy-oil-cracking": "Heavy oil cracking to light oil",
+      "light-oil-cracking": "Light oil cracking to petroleum gas",
+      "solid-fuel-from-petroleum-gas": "Solid fuel from petroleum gas",
+      "solid-fuel-from-light-oil": "Solid fuel from light oil",
+      "solid-fuel-from-heavy-oil": "Solid fuel from heavy oil",
+      "lubricant": "Lubricant",
+      "sulfuric-acid": "Sulfuric acid",
+      "acid-neutralisation": "Acid neutralisation",
+      "steam-condensation": "Steam condensation",
+      "ice-melting": "Ice melting",
+      "wood-recycling": "Wood recycling",
+      "wooden-chest-recycling": "Wooden chest recycling",
+      "coal-recycling": "Coal recycling",
+      "stone-furnace-recycling": "Stone furnace recycling",
+      "stone-recycling": "Stone recycling",
+      "iron-ore-recycling": "Iron ore recycling",
+      "copper-ore-recycling": "Copper ore recycling",
+      "uranium-ore-recycling": "Uranium ore recycling",
+      "raw-fish-recycling": "Raw fish recycling",
+      "ice-platform-recycling": "Ice platform recycling",
+      "ice-recycling": "Ice recycling",
+      "firearm-magazine-recycling": "Firearm magazine recycling",
+      "iron-chest-recycling": "Iron chest recycling",
+      "iron-gear-wheel-recycling": "Iron gear wheel recycling",
+      "iron-plate": "Iron plate",
+      "iron-plate-recycling": "Iron plate recycling",
+      "iron-stick-recycling": "Iron stick recycling",
+      "light-armor-recycling": "Light armor recycling",
+      "pipe-recycling": "Pipe recycling",
+      "copper-cable-recycling": "Copper cable recycling",
+      "copper-plate": "Copper plate",
+      "copper-plate-recycling": "Copper plate recycling",
+      "steel-chest-recycling": "Steel chest recycling",
+      "steel-plate": "Steel plate",
+      "steel-plate-recycling": "Steel plate recycling",
+      "rocket-fuel-recycling": "Rocket fuel recycling",
+      "solid-fuel-recycling": "Solid fuel recycling",
+      "plastic-bar": "Plastic bar",
+      "plastic-bar-recycling": "Plastic bar recycling",
+      "sulfur": "Sulfur",
+      "sulfur-recycling": "Sulfur recycling",
+      "battery": "Battery",
+      "explosives": "Explosives",
+      "explosives-recycling": "Explosives recycling",
+      "carbon": "Carbon",
+      "carbon-recycling": "Carbon recycling",
+      "coal-synthesis": "Coal synthesis",
+      "crude-oil-barrel-recycling": "Crude oil barrel recycling",
+      "fluoroketone-cold-barrel-recycling": "Fluoroketone (Cold) barrel recycling",
+      "fluoroketone-hot-barrel-recycling": "Fluoroketone (Hot) barrel recycling",
+      "heavy-oil-barrel-recycling": "Heavy oil barrel recycling",
+      "light-oil-barrel-recycling": "Light oil barrel recycling",
+      "lubricant-barrel-recycling": "Lubricant barrel recycling",
+      "petroleum-gas-barrel-recycling": "Petroleum gas barrel recycling",
+      "sulfuric-acid-barrel-recycling": "Sulfuric acid barrel recycling",
+      "water-barrel-recycling": "Water barrel recycling",
+      "water-barrel": "Fill Water barrel",
+      "crude-oil-barrel": "Fill Crude oil barrel",
+      "petroleum-gas-barrel": "Fill Petroleum gas barrel",
+      "light-oil-barrel": "Fill Light oil barrel",
+      "heavy-oil-barrel": "Fill Heavy oil barrel",
+      "lubricant-barrel": "Fill Lubricant barrel",
+      "sulfuric-acid-barrel": "Fill Sulfuric acid barrel",
+      "fluoroketone-hot-barrel": "Fill Fluoroketone (Hot) barrel",
+      "fluoroketone-cold-barrel": "Fill Fluoroketone (Cold) barrel",
+      "empty-water-barrel": "Empty Water barrel",
+      "empty-crude-oil-barrel": "Empty Crude oil barrel",
+      "empty-petroleum-gas-barrel": "Empty Petroleum gas barrel",
+      "empty-light-oil-barrel": "Empty Light oil barrel",
+      "empty-heavy-oil-barrel": "Empty Heavy oil barrel",
+      "empty-lubricant-barrel": "Empty Lubricant barrel",
+      "empty-sulfuric-acid-barrel": "Empty Sulfuric acid barrel",
+      "empty-fluoroketone-hot-barrel": "Empty Fluoroketone (Hot) barrel",
+      "empty-fluoroketone-cold-barrel": "Empty Fluoroketone (Cold) barrel",
+      "iron-gear-wheel": "Iron gear wheel",
+      "iron-stick": "Iron stick",
+      "copper-cable": "Copper cable",
+      "barrel": "Barrel",
+      "barrel-recycling": "Barrel recycling",
+      "electronic-circuit": "Electronic circuit",
+      "advanced-circuit": "Advanced circuit",
+      "processing-unit": "Processing unit",
+      "engine-unit": "Engine unit",
+      "electric-engine-unit": "Electric engine unit",
+      "flying-robot-frame": "Flying robot frame",
+      "low-density-structure": "Low density structure",
+      "rocket-fuel": "Rocket fuel",
+      "nuclear-fuel-recycling": "Nuclear fuel recycling",
+      "uranium-processing": "Uranium processing",
+      "uranium-235-recycling": "Uranium-235 recycling",
+      "uranium-238-recycling": "Uranium-238 recycling",
+      "uranium-fuel-cell": "Uranium fuel cell",
+      "uranium-fuel-cell-recycling": "Uranium fuel cell recycling",
+      "depleted-uranium-fuel-cell-recycling": "Depleted uranium fuel cell recycling",
+      "nuclear-fuel-reprocessing": "Nuclear fuel reprocessing",
+      "kovarex-enrichment-process": "Kovarex enrichment process",
+      "nuclear-fuel": "Nuclear fuel",
+      "calcite-recycling": "Calcite recycling",
+      "molten-iron-from-lava": "Molten iron from lava",
+      "molten-copper-from-lava": "Molten copper from lava",
+      "molten-iron": "Iron ore melting",
+      "molten-copper": "Copper ore melting",
+      "casting-iron": "Casting iron",
+      "casting-copper": "Casting copper",
+      "casting-steel": "Casting steel",
+      "casting-iron-gear-wheel": "Casting iron gear wheel",
+      "casting-iron-stick": "Casting iron stick",
+      "casting-low-density-structure": "Casting low density structure",
+      "concrete-from-molten-iron": "Concrete from molten iron",
+      "casting-copper-cable": "Casting copper cable",
+      "tungsten-ore-recycling": "Tungsten ore recycling",
+      "tungsten-carbide": "Tungsten carbide",
+      "tungsten-carbide-recycling": "Tungsten carbide recycling",
+      "tungsten-plate": "Tungsten plate",
+      "tungsten-plate-recycling": "Tungsten plate recycling",
+      "supercapacitor-recycling": "Supercapacitor recycling",
+      "scrap-recycling": "Scrap recycling",
+      "holmium-ore-recycling": "Holmium ore recycling",
+      "holmium-solution": "Holmium solution",
+      "holmium-plate": "Holmium plate",
+      "holmium-plate-recycling": "Holmium plate recycling",
+      "superconductor": "Superconductor",
+      "superconductor-recycling": "Superconductor recycling",
+      "electrolyte": "Electrolyte",
+      "supercapacitor": "Supercapacitor",
+      "yumako-processing": "Yumako processing",
+      "yumako-seed-recycling": "Yumako seed recycling",
+      "jellynut-processing": "Jellynut processing",
+      "jellynut-seed-recycling": "Jellynut seed recycling",
+      "yumako-recycling": "Yumako recycling",
+      "jellynut-recycling": "Jellynut recycling",
+      "iron-bacteria": "Iron bacteria",
+      "iron-bacteria-recycling": "Iron bacteria recycling",
+      "iron-bacteria-cultivation": "Iron bacteria cultivation",
+      "copper-bacteria": "Copper bacteria",
+      "copper-bacteria-recycling": "Copper bacteria recycling",
+      "copper-bacteria-cultivation": "Copper bacteria cultivation",
+      "nutrients-recycling": "Nutrients recycling",
+      "spoilage-recycling": "Spoilage recycling",
+      "nutrients-from-spoilage": "Nutrients from spoilage",
+      "nutrients-from-yumako-mash": "Nutrients from yumako mash",
+      "nutrients-from-bioflux": "Nutrients from bioflux",
+      "pentapod-egg": "Pentapod egg",
+      "bioflux-recycling": "Bioflux recycling",
+      "yumako-mash-recycling": "Yumako mash recycling",
+      "jelly-recycling": "Jelly recycling",
+      "rocket-fuel-from-jelly": "Rocket fuel from jelly",
+      "biolubricant": "Biolubricant",
+      "bioplastic": "Bioplastic",
+      "biosulfur": "Biosulfur",
+      "carbon-fiber-recycling": "Carbon fiber recycling",
+      "bioflux": "Bioflux",
+      "burnt-spoilage": "Burnt spoilage",
+      "carbon-fiber": "Carbon fiber",
+      "biter-egg": "Biter egg",
+      "biter-egg-recycling": "Biter egg recycling",
+      "pentapod-egg-recycling": "Pentapod egg recycling",
+      "wood-processing": "Wood processing",
+      "tree-seed-recycling": "Tree seed recycling",
+      "fish-breeding": "Fish breeding",
+      "nutrients-from-fish": "Nutrients from fish",
+      "nutrients-from-biter-egg": "Nutrients from biter egg",
+      "quantum-processor-recycling": "Quantum processor recycling",
+      "ammoniacal-solution-separation": "Ammoniacal solution separation",
+      "solid-fuel-from-ammonia": "Solid fuel from ammonia",
+      "ammonia-rocket-fuel": "Ammonia rocket fuel",
+      "fluoroketone": "Fluoroketone",
+      "fluoroketone-cooling": "Cooling hot fluoroketone",
+      "lithium": "Lithium",
+      "lithium-recycling": "Lithium recycling",
+      "lithium-plate": "Lithium plate",
+      "lithium-plate-recycling": "Lithium plate recycling",
+      "quantum-processor": "Quantum processor",
+      "fusion-power-cell": "Fusion power cell",
+      "fusion-power-cell-recycling": "Fusion power cell recycling",
+      "automation-science-pack": "Automation science pack",
+      "automation-science-pack-recycling": "Automation science pack recycling",
+      "logistic-science-pack": "Logistic science pack",
+      "logistic-science-pack-recycling": "Logistic science pack recycling",
+      "military-science-pack": "Military science pack",
+      "military-science-pack-recycling": "Military science pack recycling",
+      "chemical-science-pack": "Chemical science pack",
+      "chemical-science-pack-recycling": "Chemical science pack recycling",
+      "production-science-pack": "Production science pack",
+      "production-science-pack-recycling": "Production science pack recycling",
+      "utility-science-pack": "Utility science pack",
+      "utility-science-pack-recycling": "Utility science pack recycling",
+      "space-science-pack": "Space science pack",
+      "space-science-pack-recycling": "Space science pack recycling",
+      "metallurgic-science-pack": "Metallurgic science pack",
+      "metallurgic-science-pack-recycling": "Metallurgic science pack recycling",
+      "agricultural-science-pack": "Agricultural science pack",
+      "agricultural-science-pack-recycling": "Agricultural science pack recycling",
+      "electromagnetic-science-pack": "Electromagnetic science pack",
+      "electromagnetic-science-pack-recycling": "Electromagnetic science pack recycling",
+      "cryogenic-science-pack": "Cryogenic science pack",
+      "cryogenic-science-pack-recycling": "Cryogenic science pack recycling",
+      "promethium-science-pack": "Promethium science pack",
+      "promethium-science-pack-recycling": "Promethium science pack recycling",
+      "coin-recycling": "Coin recycling",
+      "science-recycling": "Science recycling",
+      "rocket-silo": "Rocket silo",
+      "rocket-part": "Rocket part",
+      "cargo-landing-pad": "Cargo landing pad",
+      "space-platform-foundation": "Space platform foundation",
+      "cargo-bay": "Cargo bay",
+      "asteroid-collector": "Asteroid collector",
+      "crusher": "Crusher",
+      "thruster": "Thruster",
+      "space-platform-starter-pack": "Space platform starter pack",
+      "space-platform-hub-recycling": "Space platform hub recycling",
+      "metallic-asteroid-chunk-recycling": "Metallic asteroid chunk recycling",
+      "carbonic-asteroid-chunk-recycling": "Carbonic asteroid chunk recycling",
+      "oxide-asteroid-chunk-recycling": "Oxide asteroid chunk recycling",
+      "promethium-asteroid-chunk-recycling": "Promethium asteroid chunk recycling",
+      "metallic-asteroid-crushing": "Metallic asteroid crushing",
+      "carbonic-asteroid-crushing": "Carbonic asteroid crushing",
+      "oxide-asteroid-crushing": "Oxide asteroid crushing",
+      "metallic-asteroid-reprocessing": "Metallic asteroid reprocessing",
+      "carbonic-asteroid-reprocessing": "Carbonic asteroid reprocessing",
+      "oxide-asteroid-reprocessing": "Oxide asteroid reprocessing",
+      "advanced-metallic-asteroid-crushing": "Advanced metallic asteroid crushing",
+      "advanced-carbonic-asteroid-crushing": "Advanced carbonic asteroid crushing",
+      "advanced-oxide-asteroid-crushing": "Advanced oxide asteroid crushing",
+      "thruster-fuel": "Thruster fuel",
+      "advanced-thruster-fuel": "Advanced thruster fuel",
+      "thruster-oxidizer": "Thruster oxidizer",
+      "advanced-thruster-oxidizer": "Advanced thruster oxidizer",
+      "pistol": "Pistol",
+      "submachine-gun": "Submachine gun",
+      "railgun": "Railgun",
+      "teslagun": "Tesla gun",
+      "shotgun": "Shotgun",
+      "combat-shotgun": "Combat shotgun",
+      "rocket-launcher": "Rocket launcher",
+      "flamethrower": "Flamethrower",
+      "firearm-magazine": "Firearm magazine",
+      "piercing-rounds-magazine": "Piercing rounds magazine",
+      "uranium-rounds-magazine": "Uranium rounds magazine",
+      "shotgun-shell": "Shotgun shells",
+      "piercing-shotgun-shell": "Piercing shotgun shells",
+      "cannon-shell": "Cannon shell",
+      "explosive-cannon-shell": "Explosive cannon shell",
+      "uranium-cannon-shell": "Uranium cannon shell",
+      "explosive-uranium-cannon-shell": "Explosive uranium cannon shell",
+      "artillery-shell": "Artillery shell",
+      "rocket": "Rocket",
+      "explosive-rocket": "Explosive rocket",
+      "atomic-bomb": "Atomic bomb",
+      "capture-robot-rocket": "Capture bot rocket",
+      "flamethrower-ammo": "Flamethrower ammo",
+      "flamethrower-ammo-recycling": "Flamethrower ammo recycling",
+      "railgun-ammo": "Railgun ammo",
+      "tesla-ammo": "Tesla ammo",
+      "grenade": "Grenade",
+      "cluster-grenade": "Cluster grenade",
+      "poison-capsule": "Poison capsule",
+      "slowdown-capsule": "Slowdown capsule",
+      "defender-capsule": "Defender capsule",
+      "distractor-capsule": "Distractor capsule",
+      "destroyer-capsule": "Destroyer capsule",
+      "light-armor": "Light armor",
+      "heavy-armor": "Heavy armor",
+      "modular-armor": "Modular armor",
+      "power-armor": "Power armor",
+      "power-armor-mk2": "Power armor MK2",
+      "mech-armor": "Mech armor",
+      "solar-panel-equipment": "Portable solar panel",
+      "fission-reactor-equipment": "Portable fission reactor",
+      "fusion-reactor-equipment": "Portable fusion reactor",
+      "battery-equipment": "Personal battery",
+      "battery-mk2-equipment": "Personal battery MK2",
+      "battery-mk3-equipment": "Personal battery MK3",
+      "belt-immunity-equipment": "Belt immunity equipment",
+      "exoskeleton-equipment": "Exoskeleton",
+      "personal-roboport-equipment": "Personal roboport",
+      "personal-roboport-mk2-equipment": "Personal roboport MK2",
+      "night-vision-equipment": "Nightvision",
+      "toolbelt-equipment": "Toolbelt equipment",
+      "energy-shield-equipment": "Energy shield",
+      "energy-shield-mk2-equipment": "Energy shield MK2",
+      "personal-laser-defense-equipment": "Personal laser defense",
+      "discharge-defense-equipment": "Discharge defense",
+      "stone-wall": "Wall",
+      "gate": "Gate",
+      "radar": "Radar",
+      "land-mine": "Land mine",
+      "gun-turret": "Gun turret",
+      "laser-turret": "Laser turret",
+      "flamethrower-turret": "Flamethrower turret",
+      "artillery-turret": "Artillery turret",
+      "rocket-turret": "Rocket turret",
+      "tesla-turret": "Tesla turret",
+      "railgun-turret": "Railgun turret",
+      "parameter-0": "Parameter 0",
+      "parameter-1": "Parameter 1",
+      "parameter-2": "Parameter 2",
+      "parameter-3": "Parameter 3",
+      "parameter-4": "Parameter 4",
+      "parameter-5": "Parameter 5",
+      "parameter-6": "Parameter 6",
+      "parameter-7": "Parameter 7",
+      "parameter-8": "Parameter 8",
+      "parameter-9": "Parameter 9",
+      "accumulator-recycling": "Accumulator recycling",
+      "active-provider-chest-recycling": "Active provider chest recycling",
+      "advanced-circuit-recycling": "Advanced circuit recycling",
+      "agricultural-tower-recycling": "Agricultural tower recycling",
+      "arithmetic-combinator-recycling": "Arithmetic combinator recycling",
+      "artificial-jellynut-soil-recycling": "Artificial jellynut soil recycling",
+      "artificial-yumako-soil-recycling": "Artificial yumako soil recycling",
+      "artillery-shell-recycling": "Artillery shell recycling",
+      "artillery-turret-recycling": "Artillery turret recycling",
+      "artillery-wagon-recycling": "Artillery wagon recycling",
+      "assembling-machine-1-recycling": "Assembling machine 1 recycling",
+      "assembling-machine-2-recycling": "Assembling machine 2 recycling",
+      "assembling-machine-3-recycling": "Assembling machine 3 recycling",
+      "asteroid-collector-recycling": "Asteroid collector recycling",
+      "atomic-bomb-recycling": "Atomic bomb recycling",
+      "battery-equipment-recycling": "Personal battery recycling",
+      "battery-mk2-equipment-recycling": "Personal battery MK2 recycling",
+      "battery-mk3-equipment-recycling": "Personal battery MK3 recycling",
+      "battery-recycling": "Battery recycling",
+      "beacon-recycling": "Beacon recycling",
+      "belt-immunity-equipment-recycling": "Belt immunity equipment recycling",
+      "big-electric-pole-recycling": "Big electric pole recycling",
+      "big-mining-drill-recycling": "Big mining drill recycling",
+      "biochamber-recycling": "Biochamber recycling",
+      "boiler-recycling": "Boiler recycling",
+      "buffer-chest-recycling": "Buffer chest recycling",
+      "bulk-inserter-recycling": "Bulk inserter recycling",
+      "burner-inserter-recycling": "Burner inserter recycling",
+      "burner-mining-drill-recycling": "Burner mining drill recycling",
+      "cannon-shell-recycling": "Cannon shell recycling",
+      "capture-robot-rocket-recycling": "Capture bot rocket recycling",
+      "car-recycling": "Car recycling",
+      "cargo-bay-recycling": "Cargo bay recycling",
+      "cargo-landing-pad-recycling": "Cargo landing pad recycling",
+      "cargo-wagon-recycling": "Cargo wagon recycling",
+      "centrifuge-recycling": "Centrifuge recycling",
+      "chemical-plant-recycling": "Chemical plant recycling",
+      "cliff-explosives-recycling": "Cliff explosives recycling",
+      "cluster-grenade-recycling": "Cluster grenade recycling",
+      "combat-shotgun-recycling": "Combat shotgun recycling",
+      "concrete-recycling": "Concrete recycling",
+      "constant-combinator-recycling": "Constant combinator recycling",
+      "construction-robot-recycling": "Construction robot recycling",
+      "crusher-recycling": "Crusher recycling",
+      "cryogenic-plant-recycling": "Cryogenic plant recycling",
+      "decider-combinator-recycling": "Decider combinator recycling",
+      "defender-capsule-recycling": "Defender capsule recycling",
+      "destroyer-capsule-recycling": "Destroyer capsule recycling",
+      "discharge-defense-equipment-recycling": "Discharge defense recycling",
+      "display-panel-recycling": "Display panel recycling",
+      "distractor-capsule-recycling": "Distractor capsule recycling",
+      "efficiency-module-2-recycling": "Efficiency module 2 recycling",
+      "efficiency-module-3-recycling": "Efficiency module 3 recycling",
+      "efficiency-module-recycling": "Efficiency module recycling",
+      "electric-engine-unit-recycling": "Electric engine unit recycling",
+      "electric-furnace-recycling": "Electric furnace recycling",
+      "electric-mining-drill-recycling": "Electric mining drill recycling",
+      "electromagnetic-plant-recycling": "Electromagnetic plant recycling",
+      "electronic-circuit-recycling": "Electronic circuit recycling",
+      "energy-shield-equipment-recycling": "Energy shield recycling",
+      "energy-shield-mk2-equipment-recycling": "Energy shield MK2 recycling",
+      "engine-unit-recycling": "Engine unit recycling",
+      "exoskeleton-equipment-recycling": "Exoskeleton recycling",
+      "explosive-cannon-shell-recycling": "Explosive cannon shell recycling",
+      "explosive-rocket-recycling": "Explosive rocket recycling",
+      "explosive-uranium-cannon-shell-recycling": "Explosive uranium cannon shell recycling",
+      "express-loader-recycling": "Express loader recycling",
+      "express-splitter-recycling": "Express splitter recycling",
+      "express-transport-belt-recycling": "Express transport belt recycling",
+      "express-underground-belt-recycling": "Express underground belt recycling",
+      "fast-inserter-recycling": "Fast inserter recycling",
+      "fast-loader-recycling": "Fast loader recycling",
+      "fast-splitter-recycling": "Fast splitter recycling",
+      "fast-transport-belt-recycling": "Fast transport belt recycling",
+      "fast-underground-belt-recycling": "Fast underground belt recycling",
+      "fission-reactor-equipment-recycling": "Portable fission reactor recycling",
+      "flamethrower-recycling": "Flamethrower recycling",
+      "flamethrower-turret-recycling": "Flamethrower turret recycling",
+      "fluid-wagon-recycling": "Fluid wagon recycling",
+      "flying-robot-frame-recycling": "Flying robot frame recycling",
+      "foundation-recycling": "Foundation recycling",
+      "foundry-recycling": "Foundry recycling",
+      "fusion-generator-recycling": "Fusion generator recycling",
+      "fusion-reactor-equipment-recycling": "Portable fusion reactor recycling",
+      "fusion-reactor-recycling": "Fusion reactor recycling",
+      "gate-recycling": "Gate recycling",
+      "grenade-recycling": "Grenade recycling",
+      "gun-turret-recycling": "Gun turret recycling",
+      "heat-exchanger-recycling": "Heat exchanger recycling",
+      "heat-interface-recycling": "Heat interface recycling",
+      "heat-pipe-recycling": "Heat pipe recycling",
+      "heating-tower-recycling": "Heating tower recycling",
+      "heavy-armor-recycling": "Heavy armor recycling",
+      "infinity-chest-recycling": "Infinity chest recycling",
+      "infinity-pipe-recycling": "Infinity pipe recycling",
+      "inserter-recycling": "Inserter recycling",
+      "item-unknown-recycling": "Unknown item recycling",
+      "lab-recycling": "Lab recycling",
+      "land-mine-recycling": "Land mine recycling",
+      "laser-turret-recycling": "Laser turret recycling",
+      "lightning-collector-recycling": "Lightning collector recycling",
+      "lightning-rod-recycling": "Lightning rod recycling",
+      "loader-recycling": "Loader recycling",
+      "locomotive-recycling": "Locomotive recycling",
+      "logistic-robot-recycling": "Logistic robot recycling",
+      "long-handed-inserter-recycling": "Long-handed inserter recycling",
+      "low-density-structure-recycling": "Low density structure recycling",
+      "mech-armor-recycling": "Mech armor recycling",
+      "medium-electric-pole-recycling": "Medium electric pole recycling",
+      "modular-armor-recycling": "Modular armor recycling",
+      "night-vision-equipment-recycling": "Nightvision recycling",
+      "nuclear-reactor-recycling": "Nuclear reactor recycling",
+      "offshore-pump-recycling": "Offshore pump recycling",
+      "oil-refinery-recycling": "Oil refinery recycling",
+      "overgrowth-jellynut-soil-recycling": "Overgrowth jellynut soil recycling",
+      "overgrowth-yumako-soil-recycling": "Overgrowth yumako soil recycling",
+      "passive-provider-chest-recycling": "Passive provider chest recycling",
+      "personal-laser-defense-equipment-recycling": "Personal laser defense recycling",
+      "personal-roboport-equipment-recycling": "Personal roboport recycling",
+      "personal-roboport-mk2-equipment-recycling": "Personal roboport MK2 recycling",
+      "piercing-rounds-magazine-recycling": "Piercing rounds magazine recycling",
+      "piercing-shotgun-shell-recycling": "Piercing shotgun shells recycling",
+      "pipe-to-ground-recycling": "Pipe to ground recycling",
+      "pistol-recycling": "Pistol recycling",
+      "poison-capsule-recycling": "Poison capsule recycling",
+      "power-armor-mk2-recycling": "Power armor MK2 recycling",
+      "power-armor-recycling": "Power armor recycling",
+      "power-switch-recycling": "Power switch recycling",
+      "processing-unit-recycling": "Processing unit recycling",
+      "productivity-module-2-recycling": "Productivity module 2 recycling",
+      "productivity-module-3-recycling": "Productivity module 3 recycling",
+      "productivity-module-recycling": "Productivity module recycling",
+      "programmable-speaker-recycling": "Programmable speaker recycling",
+      "pump-recycling": "Pump recycling",
+      "pumpjack-recycling": "Pumpjack recycling",
+      "quality-module-2-recycling": "Quality module 2 recycling",
+      "quality-module-3-recycling": "Quality module 3 recycling",
+      "quality-module-recycling": "Quality module recycling",
+      "radar-recycling": "Radar recycling",
+      "rail-chain-signal-recycling": "Rail chain signal recycling",
+      "rail-ramp-recycling": "Rail ramp recycling",
+      "rail-recycling": "Rail recycling",
+      "rail-signal-recycling": "Rail signal recycling",
+      "rail-support-recycling": "Rail support recycling",
+      "railgun-ammo-recycling": "Railgun ammo recycling",
+      "railgun-recycling": "Railgun recycling",
+      "railgun-turret-recycling": "Railgun turret recycling",
+      "rcalc-heat-dummy-recycling": "Heat recycling",
+      "rcalc-pollution-dummy-recycling": "Pollution recycling",
+      "rcalc-power-dummy-recycling": "Power recycling",
+      "recipe-unknown": "Unknown recipe",
+      "recycler-recycling": "Recycler recycling",
+      "refined-concrete-recycling": "Refined concrete recycling",
+      "repair-pack-recycling": "Repair pack recycling",
+      "requester-chest-recycling": "Requester chest recycling",
+      "roboport-recycling": "Roboport recycling",
+      "rocket-launcher-recycling": "Rocket launcher recycling",
+      "rocket-recycling": "Rocket recycling",
+      "rocket-silo-recycling": "Rocket silo recycling",
+      "rocket-turret-recycling": "Rocket turret recycling",
+      "selector-combinator-recycling": "Selector combinator recycling",
+      "shotgun-recycling": "Shotgun recycling",
+      "shotgun-shell-recycling": "Shotgun shells recycling",
+      "slowdown-capsule-recycling": "Slowdown capsule recycling",
+      "small-electric-pole-recycling": "Small electric pole recycling",
+      "small-lamp-recycling": "Lamp recycling",
+      "solar-panel-equipment-recycling": "Portable solar panel recycling",
+      "solar-panel-recycling": "Solar panel recycling",
+      "space-platform-foundation-recycling": "Space platform foundation recycling",
+      "space-platform-starter-pack-recycling": "Space platform starter pack recycling",
+      "speed-module-2-recycling": "Speed module 2 recycling",
+      "speed-module-3-recycling": "Speed module 3 recycling",
+      "speed-module-recycling": "Speed module recycling",
+      "spidertron-recycling": "Spidertron recycling",
+      "splitter-recycling": "Splitter recycling",
+      "stack-inserter-recycling": "Stack inserter recycling",
+      "steam-engine-recycling": "Steam engine recycling",
+      "steam-turbine-recycling": "Steam turbine recycling",
+      "steel-furnace-recycling": "Steel furnace recycling",
+      "storage-chest-recycling": "Storage chest recycling",
+      "storage-tank-recycling": "Storage tank recycling",
+      "submachine-gun-recycling": "Submachine gun recycling",
+      "substation-recycling": "Substation recycling",
+      "tank-recycling": "Tank recycling",
+      "tesla-ammo-recycling": "Tesla ammo recycling",
+      "tesla-turret-recycling": "Tesla turret recycling",
+      "teslagun-recycling": "Tesla gun recycling",
+      "thruster-recycling": "Thruster recycling",
+      "toolbelt-equipment-recycling": "Toolbelt equipment recycling",
+      "train-stop-recycling": "Train stop recycling",
+      "transport-belt-recycling": "Transport belt recycling",
+      "turbo-loader-recycling": "Turbo loader recycling",
+      "turbo-splitter-recycling": "Turbo splitter recycling",
+      "turbo-transport-belt-recycling": "Turbo transport belt recycling",
+      "turbo-underground-belt-recycling": "Turbo underground belt recycling",
+      "underground-belt-recycling": "Underground belt recycling",
+      "uranium-cannon-shell-recycling": "Uranium cannon shell recycling",
+      "uranium-rounds-magazine-recycling": "Uranium rounds magazine recycling",
+      "electric-energy-interface-recycling": "Electric energy interface recycling",
+      "linked-chest-recycling": "Linked chest recycling",
+      "proxy-container-recycling": "Proxy container recycling",
+      "bottomless-chest-recycling": "Bottomless chest recycling",
+      "heat-interface": "Heat interface",
+      "lane-splitter-recycling": "Lane splitter recycling",
+      "linked-belt-recycling": "Linked belt recycling",
+      "one-way-valve-recycling": "One-way valve recycling",
+      "overflow-valve-recycling": "Overflow valve recycling",
+      "top-up-valve-recycling": "Top-up valve recycling",
+      "infinity-cargo-wagon-recycling": "Infinity cargo wagon recycling",
+      "infinity-chest": "Infinity chest",
+      "infinity-pipe": "Infinity pipe",
+      "rcalc-selection-tool-recycling": "Rate Calculator selector recycling",
+      "selection-tool-recycling": "Selection tool recycling",
+      "simple-entity-with-force-recycling": "Simple entity with force recycling",
+      "simple-entity-with-owner-recycling": "Simple entity with owner recycling",
+      "burner-generator-recycling": "Burner generator recycling",
+      "ee-infinity-chest": "Infinity chest",
+      "ee-infinity-chest-recycling": "Infinity chest recycling",
+      "ee-infinity-chest-active-provider": "Infinity active provider chest",
+      "ee-infinity-chest-active-provider-recycling": "Infinity active provider chest recycling",
+      "ee-infinity-chest-passive-provider": "Infinity passive provider chest",
+      "ee-infinity-chest-passive-provider-recycling": "Infinity passive provider chest recycling",
+      "ee-infinity-chest-storage": "Infinity storage chest",
+      "ee-infinity-chest-storage-recycling": "Infinity storage chest recycling",
+      "ee-infinity-chest-buffer": "Infinity buffer chest",
+      "ee-infinity-chest-buffer-recycling": "Infinity buffer chest recycling",
+      "ee-infinity-chest-requester": "Infinity requester chest",
+      "ee-infinity-chest-requester-recycling": "Infinity requester chest recycling",
+      "ee-aggregate-chest": "Aggregate chest",
+      "ee-aggregate-chest-recycling": "Aggregate chest recycling",
+      "ee-aggregate-chest-passive-provider": "Aggregate passive provider chest",
+      "ee-aggregate-chest-passive-provider-recycling": "Aggregate passive provider chest recycling",
+      "ee-linked-chest": "Linked chest",
+      "ee-linked-chest-recycling": "Linked chest recycling",
+      "ee-infinity-loader": "Infinity loader",
+      "ee-infinity-loader-recycling": "Infinity loader recycling",
+      "ee-linked-belt": "Linked belt",
+      "ee-linked-belt-recycling": "Linked belt recycling",
+      "ee-super-inserter": "Super inserter",
+      "ee-super-inserter-recycling": "Super inserter recycling",
+      "ee-infinity-pipe": "Infinity pipe",
+      "ee-infinity-pipe-recycling": "Infinity pipe recycling",
+      "ee-super-pump": "Super pump",
+      "ee-super-pump-recycling": "Super pump recycling",
+      "ee-infinity-heat-pipe": "Infinity heat pipe",
+      "ee-infinity-heat-pipe-recycling": "Infinity heat pipe recycling",
+      "ee-super-radar": "Super radar",
+      "ee-super-radar-recycling": "Super radar recycling",
+      "ee-super-lab": "Super lab",
+      "ee-super-lab-recycling": "Super lab recycling",
+      "ee-infinity-accumulator": "Infinity accumulator",
+      "ee-infinity-accumulator-recycling": "Infinity accumulator recycling",
+      "ee-super-electric-pole": "Super electric pole",
+      "ee-super-electric-pole-recycling": "Super electric pole recycling",
+      "ee-super-substation": "Super substation",
+      "ee-super-substation-recycling": "Super substation recycling",
+      "ee-super-locomotive": "Super locomotive",
+      "ee-super-locomotive-recycling": "Super locomotive recycling",
+      "ee-infinity-cargo-wagon": "Infinity cargo wagon",
+      "ee-infinity-cargo-wagon-recycling": "Infinity cargo wagon recycling",
+      "ee-infinity-fluid-wagon": "Infinity fluid wagon",
+      "ee-infinity-fluid-wagon-recycling": "Infinity fluid wagon recycling",
+      "ee-super-fuel": "Super fuel",
+      "ee-super-fuel-recycling": "Super fuel recycling",
+      "ee-super-roboport": "Super roboport",
+      "ee-super-roboport-recycling": "Super roboport recycling",
+      "ee-super-construction-robot": "Super construction robot",
+      "ee-super-construction-robot-recycling": "Super construction robot recycling",
+      "ee-super-logistic-robot": "Super logistic robot",
+      "ee-super-logistic-robot-recycling": "Super logistic robot recycling",
+      "ee-super-beacon": "Super beacon",
+      "ee-super-beacon-recycling": "Super beacon recycling",
+      "ee-super-speed-module": "Super speed module",
+      "ee-super-speed-module-recycling": "Super speed module recycling",
+      "ee-super-efficiency-module": "Super efficiency module",
+      "ee-super-efficiency-module-recycling": "Super efficiency module recycling",
+      "ee-super-productivity-module": "Super productivity module",
+      "ee-super-productivity-module-recycling": "Super productivity module recycling",
+      "ee-super-clean-module": "Super clean module",
+      "ee-super-clean-module-recycling": "Super clean module recycling",
+      "ee-super-slow-module": "Super slow module",
+      "ee-super-slow-module-recycling": "Super slow module recycling",
+      "ee-super-inefficiency-module": "Super inefficiency module",
+      "ee-super-inefficiency-module-recycling": "Super inefficiency module recycling",
+      "ee-super-dirty-module": "Super dirty module",
+      "ee-super-dirty-module-recycling": "Super dirty module recycling",
+      "ee-infinity-fission-reactor-equipment": "Infinity fission reactor",
+      "ee-infinity-fission-reactor-equipment-recycling": "Infinity fission reactor recycling",
+      "ee-super-personal-roboport-equipment": "Super personal roboport",
+      "ee-super-personal-roboport-equipment-recycling": "Super personal roboport recycling",
+      "ee-super-exoskeleton-equipment": "Super exoskeleton",
+      "ee-super-exoskeleton-equipment-recycling": "Super exoskeleton recycling",
+      "ee-super-energy-shield-equipment": "Super energy shield",
+      "ee-super-energy-shield-equipment-recycling": "Super energy shield recycling",
+      "ee-super-night-vision-equipment": "Super night vision",
+      "ee-super-night-vision-equipment-recycling": "Super night vision recycling",
+      "ee-super-battery-equipment": "Super personal battery",
+      "ee-super-battery-equipment-recycling": "Super personal battery recycling"
+    },
+    "descriptions": {
+      "ammoniacal-solution-separation": "[fluid=ammoniacal-solution] is gained by an [entity=offshore-pump] in the oceans of [planet=aquilo].",
+      "ammonia-rocket-fuel": "A convenient use of ammonia.",
+      "recipe-unknown": "This recipe is not available due to mod removal, it will be restored if the mod is re-enabled."
+    }
+  },
+  "item-group": {
+    "names": {
+      "logistics": "Logistics",
+      "production": "Production",
+      "intermediate-products": "Intermediate products",
+      "space": "Space",
+      "combat": "Combat",
+      "fluids": "Fluids",
+      "signals": "Signals",
+      "enemies": "Enemies",
+      "tiles": "Tiles",
+      "environment": "Environment",
+      "effects": "Effects",
+      "other": "Unsorted",
+      "ee-tools": "Testing Tools"
+    }
+  },
+  "quality": {
+    "names": {
+      "normal": "Normal",
+      "uncommon": "Uncommon",
+      "rare": "Rare",
+      "epic": "Epic",
+      "legendary": "Legendary",
+      "quality-unknown": "Unknown"
+    },
+    "descriptions": {
+      "quality-unknown": "This quality is not available due to mod removal, it will be restored if the mod is re-enabled."
+    }
+  },
+  "virtual-signal": {
+    "names": {
+      "signal-everything": "Everything",
+      "signal-each": "Each",
+      "signal-anything": "Anything",
+      "signal-0": "Signal 0",
+      "signal-1": "Signal 1",
+      "signal-2": "Signal 2",
+      "signal-3": "Signal 3",
+      "signal-4": "Signal 4",
+      "signal-5": "Signal 5",
+      "signal-6": "Signal 6",
+      "signal-7": "Signal 7",
+      "signal-8": "Signal 8",
+      "signal-9": "Signal 9",
+      "signal-A": "Signal A",
+      "signal-B": "Signal B",
+      "signal-C": "Signal C",
+      "signal-D": "Signal D",
+      "signal-E": "Signal E",
+      "signal-F": "Signal F",
+      "signal-G": "Signal G",
+      "signal-H": "Signal H",
+      "signal-I": "Signal I",
+      "signal-J": "Signal J",
+      "signal-K": "Signal K",
+      "signal-L": "Signal L",
+      "signal-M": "Signal M",
+      "signal-N": "Signal N",
+      "signal-O": "Signal O",
+      "signal-P": "Signal P",
+      "signal-Q": "Signal Q",
+      "signal-R": "Signal R",
+      "signal-S": "Signal S",
+      "signal-T": "Signal T",
+      "signal-U": "Signal U",
+      "signal-V": "Signal V",
+      "signal-W": "Signal W",
+      "signal-X": "Signal X",
+      "signal-Y": "Signal Y",
+      "signal-Z": "Signal Z",
+      "signal-comma": "Comma",
+      "signal-letter-dot": "Dot",
+      "signal-exclamation-mark": "Exclamation mark",
+      "signal-question-mark": "Question mark",
+      "signal-colon": "Colon",
+      "signal-slash": "Slash",
+      "signal-apostrophe": "Apostrophe",
+      "signal-quotation-mark": "Quotation mark",
+      "signal-ampersand": "Ampersand",
+      "signal-circumflex-accent": "Circumflex accent",
+      "signal-number-sign": "Number sign",
+      "signal-percent": "Percent",
+      "signal-plus": "Plus",
+      "signal-minus": "Minus",
+      "signal-multiplication": "Multiplication",
+      "signal-division": "Division",
+      "signal-equal": "Equal",
+      "signal-not-equal": "Not equal",
+      "signal-less-than": "Less than",
+      "signal-greater-than": "Greater than",
+      "signal-less-than-or-equal-to": "Less than or equal to",
+      "signal-greater-than-or-equal-to": "Greater than or equal to",
+      "signal-left-parenthesis": "Left parenthesis",
+      "signal-right-parenthesis": "Right parenthesis",
+      "signal-left-square-bracket": "Left square bracket",
+      "signal-right-square-bracket": "Right square bracket",
+      "signal-red": "Red signal",
+      "signal-green": "Green signal",
+      "signal-blue": "Blue signal",
+      "signal-cyan": "Cyan signal",
+      "signal-pink": "Pink signal",
+      "signal-yellow": "Yellow signal",
+      "signal-white": "White signal",
+      "signal-grey": "Grey signal",
+      "signal-black": "Black signal",
+      "signal-check": "Check signal",
+      "signal-deny": "Deny signal",
+      "signal-no-entry": "No entry",
+      "signal-heart": "Heart",
+      "signal-alert": "Alert",
+      "signal-star": "Star",
+      "signal-info": "Info signal",
+      "shape-vertical": "Vertical",
+      "shape-horizontal": "Horizontal",
+      "shape-curve": "Curve",
+      "shape-curve-2": "Curve",
+      "shape-corner": "Corner",
+      "shape-corner-2": "Corner",
+      "shape-t": "T cross",
+      "shape-t-2": "T cross",
+      "shape-cross": "Cross",
+      "shape-diagonal-cross": "Diagonal Cross",
+      "shape-diagonal": "Diagonal",
+      "shape-diagonal-2": "Diagonal",
+      "shape-curve-3": "Curve",
+      "shape-curve-4": "Curve",
+      "shape-corner-4": "Corner",
+      "shape-corner-3": "Corner",
+      "shape-t-4": "T cross",
+      "shape-t-3": "T cross",
+      "shape-circle": "Circle",
+      "signal-dot": "Dot signal",
+      "up-arrow": "Up arrow",
+      "up-right-arrow": "Up right arrow",
+      "right-arrow": "Right arrow",
+      "down-right-arrow": "Down right arrow",
+      "down-arrow": "Down arrow",
+      "down-left-arrow": "Down left arrow",
+      "left-arrow": "Left arrow",
+      "up-left-arrow": "Up left arrow",
+      "signal-rightwards-leftwards-arrow": "Rightwards-leftwards arrow",
+      "signal-upwards-downwards-arrow": "Upwards-downwards arrow",
+      "signal-shuffle": "Shuffle",
+      "signal-left-right-arrow": "Left-right arrow",
+      "signal-up-down-arrow": "Up-down arrow",
+      "signal-clockwise-circle-arrow": "Clockwise circle arrow",
+      "signal-anticlockwise-circle-arrow": "Anticlockwise circle arrow",
+      "signal-input": "Input",
+      "signal-output": "Output",
+      "signal-fuel": "Fuel",
+      "signal-lightning": "Electricity",
+      "signal-battery-low": "Low battery",
+      "signal-battery-mid-level": "Mid-level battery",
+      "signal-battery-full": "Full battery",
+      "signal-radioactivity": "Radioactivity",
+      "signal-thermometer-blue": "Thermometer Low",
+      "signal-thermometer-red": "Thermometer High",
+      "signal-fire": "Fire",
+      "signal-snowflake": "Snowflake",
+      "signal-explosion": "Explosion",
+      "signal-liquid": "Liquid",
+      "signal-stack-size": "Stack size",
+      "signal-recycle": "Recycle",
+      "signal-trash-bin": "Trash bin",
+      "signal-science-pack": "Science pack",
+      "signal-map-marker": "Map marker",
+      "signal-white-flag": "Flag",
+      "signal-lock": "Lock",
+      "signal-unlock": "Unlock",
+      "signal-speed": "Speed",
+      "signal-clock": "Clock",
+      "signal-hourglass": "Wait",
+      "signal-alarm": "Alarm",
+      "signal-sun": "Sun",
+      "signal-moon": "Moon",
+      "signal-mining": "Pick",
+      "signal-skull": "Skull",
+      "signal-damage": "Damage",
+      "signal-weapon": "Weapon",
+      "signal-ghost": "Ghost",
+      "signal-item-parameter": "Item parameter",
+      "signal-fuel-parameter": "Fuel parameter",
+      "signal-fluid-parameter": "Fluid parameter",
+      "signal-signal-parameter": "Signal parameter",
+      "signal-any-quality": "Any quality",
+      "signal-unknown": "Unknown signal"
+    },
+    "descriptions": {
+      "signal-everything": "If all input signals meet the condition, it will pass.\nIt is true when there are no inputs.\nOutput all input signals.",
+      "signal-each": "Evaluates the condition for each input signal individually.\nTo pass completely, a signal has to pass all conditions.\nOutput every signal that passed all conditions.",
+      "signal-anything": "If any of the input signals meet the condition, it will pass.\nIt is false when there are no inputs.\nOutput the first input signal, or the first signal that passed all conditions, respecting signal order in both cases.",
+      "signal-item-parameter": "Special Wildcard signal\nWhen used in schedule interrupt, it will match the first item that passes all wait conditions and replace the signal with that item.\nThis also replaces the rich text tag in the target stop name.",
+      "signal-fuel-parameter": "Special Wildcard signal\nWhen used in schedule interrupt, it will match the first fuel that passes all wait conditions and replace the signal with that fuel.\nThis also replaces the rich text tag in the target stop name.",
+      "signal-fluid-parameter": "Special Wildcard signal\nWhen used in schedule interrupt, it will match the first fluid that passes all wait conditions and replace the signal with that fluid.\nThis also replaces the rich text tag in the target stop name.",
+      "signal-signal-parameter": "Special Wildcard signal\nWhen used in schedule interrupt, it will match the first signal that passes all wait conditions and replace the signal with that signal.\nThis also replaces the rich text tag in the target stop name.",
+      "signal-unknown": "This signal is not available due to mod removal, it will be restored if the mod is re-enabled."
+    }
+  }
+}

+ 1348 - 604
src/assets/data/2.0/iconMap.json

@@ -18,3569 +18,4313 @@
     "size": 56
   },
   {
-    "name": "entity/assembling-machine-1.png",
+    "name": "entity/ashland-lichen-tree-flaming.png",
     "x": 0,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/assembling-machine-2.png",
+    "name": "entity/ashland-lichen-tree.png",
     "x": 64,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/assembling-machine-3.png",
+    "name": "entity/assembling-machine-1.png",
     "x": 128,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/biochamber.png",
+    "name": "entity/assembling-machine-2.png",
     "x": 192,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/captive-biter-spawner.png",
+    "name": "entity/assembling-machine-3.png",
     "x": 256,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/centrifuge.png",
+    "name": "entity/behemoth-biter.png",
     "x": 320,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/chemical-plant.png",
+    "name": "entity/behemoth-spitter.png",
     "x": 384,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/crusher.png",
+    "name": "entity/behemoth-worm-turret.png",
     "x": 448,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/cryogenic-plant.png",
+    "name": "entity/big-biter.png",
     "x": 512,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/electromagnetic-plant.png",
+    "name": "entity/big-carbonic-asteroid.png",
     "x": 576,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/foundry.png",
+    "name": "entity/big-demolisher-corpse.png",
     "x": 640,
     "y": 0,
     "size": 64
   },
   {
-    "name": "entity/oil-refinery.png",
+    "name": "entity/big-demolisher.png",
     "x": 704,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/ammonia.png",
+    "name": "entity/big-metallic-asteroid.png",
     "x": 768,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/ammoniacal-solution.png",
+    "name": "entity/big-oxide-asteroid.png",
     "x": 832,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/crude-oil.png",
+    "name": "entity/big-promethium-asteroid.png",
     "x": 896,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/electrolyte.png",
+    "name": "entity/big-rock.png",
     "x": 960,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/fluorine.png",
+    "name": "entity/big-spitter.png",
     "x": 1024,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/fluoroketone-cold.png",
+    "name": "entity/big-stomper-pentapod.png",
     "x": 1088,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/fluoroketone-hot.png",
+    "name": "entity/big-stomper-shell.png",
     "x": 1152,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/fusion-plasma.png",
+    "name": "entity/big-strafer-pentapod.png",
     "x": 1216,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/heavy-oil.png",
+    "name": "entity/big-volcanic-rock.png",
     "x": 1280,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/holmium-solution.png",
+    "name": "entity/big-worm-turret.png",
     "x": 1344,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/lava.png",
+    "name": "entity/big-wriggler-pentapod-premature.png",
     "x": 1408,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/light-oil.png",
+    "name": "entity/big-wriggler-pentapod.png",
     "x": 1472,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/lithium-brine.png",
+    "name": "entity/biochamber.png",
     "x": 1536,
     "y": 0,
     "size": 64
   },
   {
-    "name": "fluid/lubricant.png",
+    "name": "entity/biter-spawner.png",
+    "x": 1600,
+    "y": 0,
+    "size": 64
+  },
+  {
+    "name": "entity/boompuff.png",
+    "x": 1664,
+    "y": 0,
+    "size": 64
+  },
+  {
+    "name": "entity/captive-biter-spawner.png",
     "x": 0,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/molten-copper.png",
+    "name": "entity/cargo-pod-container.png",
     "x": 64,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/molten-iron.png",
+    "name": "entity/cargo-pod.png",
     "x": 128,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-0.png",
+    "name": "entity/centrifuge.png",
     "x": 192,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-1.png",
+    "name": "entity/character.png",
     "x": 256,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-2.png",
+    "name": "entity/chemical-plant.png",
     "x": 320,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-3.png",
+    "name": "entity/cliff.png",
     "x": 384,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-4.png",
+    "name": "entity/copper-stromatolite.png",
     "x": 448,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-5.png",
+    "name": "entity/crude-oil.png",
     "x": 512,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-6.png",
+    "name": "entity/crusher.png",
     "x": 576,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-7.png",
+    "name": "entity/cryogenic-plant.png",
     "x": 640,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-8.png",
+    "name": "entity/cuttlepop.png",
     "x": 704,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/parameter-9.png",
+    "name": "entity/defender.png",
     "x": 768,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/petroleum-gas.png",
+    "name": "entity/destroyer.png",
     "x": 832,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/steam.png",
+    "name": "entity/distractor.png",
     "x": 896,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/sulfuric-acid.png",
+    "name": "entity/electromagnetic-plant.png",
     "x": 960,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/thruster-fuel.png",
+    "name": "entity/entity-ghost.png",
     "x": 1024,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/thruster-oxidizer.png",
+    "name": "entity/fish.png",
     "x": 1088,
     "y": 64,
     "size": 64
   },
   {
-    "name": "fluid/water.png",
+    "name": "entity/fluorine-vent.png",
     "x": 1152,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/accumulator.png",
+    "name": "entity/foundry.png",
     "x": 1216,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/active-provider-chest.png",
+    "name": "entity/fulgoran-ruin-attractor.png",
     "x": 1280,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/advanced-circuit.png",
+    "name": "entity/fulgoran-ruin-big.png",
     "x": 1344,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/agricultural-science-pack.png",
+    "name": "entity/fulgoran-ruin-colossal.png",
     "x": 1408,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/agricultural-tower.png",
+    "name": "entity/fulgoran-ruin-huge.png",
     "x": 1472,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/arithmetic-combinator.png",
+    "name": "entity/fulgoran-ruin-medium.png",
     "x": 1536,
     "y": 64,
     "size": 64
   },
   {
-    "name": "item/artificial-jellynut-soil.png",
+    "name": "entity/fulgoran-ruin-small.png",
+    "x": 1600,
+    "y": 64,
+    "size": 64
+  },
+  {
+    "name": "entity/fulgoran-ruin-stonehenge.png",
+    "x": 1664,
+    "y": 64,
+    "size": 64
+  },
+  {
+    "name": "entity/fulgoran-ruin-vault.png",
     "x": 0,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/artificial-yumako-soil.png",
+    "name": "entity/fulgurite-small.png",
     "x": 64,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/artillery-shell.png",
+    "name": "entity/fulgurite.png",
     "x": 128,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/artillery-targeting-remote.png",
+    "name": "entity/funneltrunk.png",
     "x": 192,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/artillery-turret.png",
+    "name": "entity/gleba-spawner-small.png",
     "x": 256,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/artillery-wagon.png",
+    "name": "entity/gleba-spawner.png",
     "x": 320,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/assembling-machine-1.png",
+    "name": "entity/hairyclubnub.png",
     "x": 384,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/assembling-machine-2.png",
+    "name": "entity/huge-carbonic-asteroid.png",
     "x": 448,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/assembling-machine-3.png",
+    "name": "entity/huge-metallic-asteroid.png",
     "x": 512,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/asteroid-collector.png",
+    "name": "entity/huge-oxide-asteroid.png",
     "x": 576,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/atomic-bomb.png",
+    "name": "entity/huge-promethium-asteroid.png",
     "x": 640,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/automation-science-pack.png",
+    "name": "entity/huge-rock.png",
     "x": 704,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/barrel.png",
+    "name": "entity/huge-volcanic-rock.png",
     "x": 768,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/battery-equipment.png",
+    "name": "entity/iron-stromatolite.png",
     "x": 832,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/battery-mk2-equipment.png",
+    "name": "entity/item-on-ground.png",
     "x": 896,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/battery-mk3-equipment.png",
+    "name": "entity/item-request-proxy.png",
     "x": 960,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/battery.png",
+    "name": "entity/jellystem.png",
     "x": 1024,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/beacon.png",
+    "name": "entity/lickmaw.png",
     "x": 1088,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/belt-immunity-equipment.png",
+    "name": "entity/lightning.png",
     "x": 1152,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/big-electric-pole.png",
+    "name": "entity/lithium-brine.png",
     "x": 1216,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/big-mining-drill.png",
+    "name": "entity/lithium-iceberg-big.png",
     "x": 1280,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/biochamber.png",
+    "name": "entity/lithium-iceberg-huge.png",
     "x": 1344,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/bioflux.png",
+    "name": "entity/medium-biter.png",
     "x": 1408,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/biolab.png",
+    "name": "entity/medium-carbonic-asteroid.png",
     "x": 1472,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/biter-egg.png",
+    "name": "entity/medium-demolisher-corpse.png",
     "x": 1536,
     "y": 128,
     "size": 64
   },
   {
-    "name": "item/boiler.png",
+    "name": "entity/medium-demolisher.png",
+    "x": 1600,
+    "y": 128,
+    "size": 64
+  },
+  {
+    "name": "entity/medium-metallic-asteroid.png",
+    "x": 1664,
+    "y": 128,
+    "size": 64
+  },
+  {
+    "name": "entity/medium-oxide-asteroid.png",
     "x": 0,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/buffer-chest.png",
+    "name": "entity/medium-promethium-asteroid.png",
     "x": 64,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/bulk-inserter.png",
+    "name": "entity/medium-spitter.png",
     "x": 128,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/burner-inserter.png",
+    "name": "entity/medium-stomper-pentapod.png",
     "x": 192,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/burner-mining-drill.png",
+    "name": "entity/medium-stomper-shell.png",
     "x": 256,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/calcite.png",
+    "name": "entity/medium-strafer-pentapod.png",
     "x": 320,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/cannon-shell.png",
+    "name": "entity/medium-worm-turret.png",
     "x": 384,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/captive-biter-spawner.png",
+    "name": "entity/medium-wriggler-pentapod-premature.png",
     "x": 448,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/capture-robot-rocket.png",
+    "name": "entity/medium-wriggler-pentapod.png",
     "x": 512,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/car.png",
+    "name": "entity/oil-refinery.png",
     "x": 576,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/carbon-fiber.png",
+    "name": "entity/slipstack.png",
     "x": 640,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/carbon.png",
+    "name": "entity/small-biter.png",
     "x": 704,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/carbonic-asteroid-chunk.png",
+    "name": "entity/small-carbonic-asteroid.png",
     "x": 768,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/cargo-bay.png",
+    "name": "entity/small-demolisher-corpse.png",
     "x": 832,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/cargo-landing-pad.png",
+    "name": "entity/small-demolisher.png",
     "x": 896,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/cargo-wagon.png",
+    "name": "entity/small-metallic-asteroid.png",
     "x": 960,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/centrifuge.png",
+    "name": "entity/small-oxide-asteroid.png",
     "x": 1024,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/chemical-plant.png",
+    "name": "entity/small-promethium-asteroid.png",
     "x": 1088,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/chemical-science-pack.png",
+    "name": "entity/small-spitter.png",
     "x": 1152,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/cliff-explosives.png",
+    "name": "entity/small-stomper-pentapod.png",
     "x": 1216,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/cluster-grenade.png",
+    "name": "entity/small-stomper-shell.png",
     "x": 1280,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/coal.png",
+    "name": "entity/small-strafer-pentapod.png",
     "x": 1344,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/combat-shotgun.png",
+    "name": "entity/small-worm-turret.png",
     "x": 1408,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/concrete.png",
+    "name": "entity/small-wriggler-pentapod-premature.png",
     "x": 1472,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/constant-combinator.png",
+    "name": "entity/small-wriggler-pentapod.png",
     "x": 1536,
     "y": 192,
     "size": 64
   },
   {
-    "name": "item/construction-robot.png",
+    "name": "entity/space-platform-hub.png",
+    "x": 1600,
+    "y": 192,
+    "size": 64
+  },
+  {
+    "name": "entity/spitter-spawner.png",
+    "x": 1664,
+    "y": 192,
+    "size": 64
+  },
+  {
+    "name": "entity/stingfrond.png",
     "x": 0,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/copper-bacteria.png",
+    "name": "entity/sulfuric-acid-geyser.png",
     "x": 64,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/copper-cable.png",
+    "name": "entity/sunnycomb.png",
     "x": 128,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/copper-ore.png",
+    "name": "entity/teflilly.png",
     "x": 192,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/copper-plate.png",
+    "name": "entity/tile-ghost.png",
     "x": 256,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/crude-oil-barrel.png",
+    "name": "entity/tree-01.png",
     "x": 320,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/crusher.png",
+    "name": "entity/vulcanus-chimney-cold.png",
     "x": 384,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/cryogenic-plant.png",
+    "name": "entity/vulcanus-chimney-faded.png",
     "x": 448,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/cryogenic-science-pack.png",
+    "name": "entity/vulcanus-chimney-short.png",
     "x": 512,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/decider-combinator.png",
+    "name": "entity/vulcanus-chimney-truncated.png",
     "x": 576,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/defender-capsule.png",
+    "name": "entity/vulcanus-chimney.png",
     "x": 640,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/depleted-uranium-fuel-cell.png",
+    "name": "entity/water-cane.png",
     "x": 704,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/destroyer-capsule.png",
+    "name": "entity/wube-logo-space-platform.png",
     "x": 768,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/discharge-defense-equipment.png",
+    "name": "entity/yumako-tree.png",
     "x": 832,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/discharge-defense-remote.png",
+    "name": "fluid/ammonia.png",
     "x": 896,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/display-panel.png",
+    "name": "fluid/ammoniacal-solution.png",
     "x": 960,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/distractor-capsule.png",
+    "name": "fluid/crude-oil.png",
     "x": 1024,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/efficiency-module-2.png",
+    "name": "fluid/electrolyte.png",
     "x": 1088,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/efficiency-module-3.png",
+    "name": "fluid/fluorine.png",
     "x": 1152,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/efficiency-module.png",
+    "name": "fluid/fluoroketone-cold.png",
     "x": 1216,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/electric-engine-unit.png",
+    "name": "fluid/fluoroketone-hot.png",
     "x": 1280,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/electric-furnace.png",
+    "name": "fluid/fusion-plasma.png",
     "x": 1344,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/electric-mining-drill.png",
+    "name": "fluid/heavy-oil.png",
     "x": 1408,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/electromagnetic-plant.png",
+    "name": "fluid/holmium-solution.png",
     "x": 1472,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/electromagnetic-science-pack.png",
+    "name": "fluid/lava.png",
     "x": 1536,
     "y": 256,
     "size": 64
   },
   {
-    "name": "item/electronic-circuit.png",
+    "name": "fluid/light-oil.png",
+    "x": 1600,
+    "y": 256,
+    "size": 64
+  },
+  {
+    "name": "fluid/lithium-brine.png",
+    "x": 1664,
+    "y": 256,
+    "size": 64
+  },
+  {
+    "name": "fluid/lubricant.png",
     "x": 0,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/energy-shield-equipment.png",
+    "name": "fluid/molten-copper.png",
     "x": 64,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/energy-shield-mk2-equipment.png",
+    "name": "fluid/molten-iron.png",
     "x": 128,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/engine-unit.png",
+    "name": "fluid/petroleum-gas.png",
     "x": 192,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/exoskeleton-equipment.png",
+    "name": "fluid/steam.png",
     "x": 256,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/explosive-cannon-shell.png",
+    "name": "fluid/sulfuric-acid.png",
     "x": 320,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/explosive-rocket.png",
+    "name": "fluid/thruster-fuel.png",
     "x": 384,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/explosive-uranium-cannon-shell.png",
+    "name": "fluid/thruster-oxidizer.png",
     "x": 448,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/explosives.png",
+    "name": "fluid/water.png",
     "x": 512,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/express-splitter.png",
+    "name": "item/accumulator.png",
     "x": 576,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/express-transport-belt.png",
+    "name": "item/active-provider-chest.png",
     "x": 640,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/express-underground-belt.png",
+    "name": "item/advanced-circuit.png",
     "x": 704,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fast-inserter.png",
+    "name": "item/agricultural-science-pack.png",
     "x": 768,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fast-splitter.png",
+    "name": "item/agricultural-tower.png",
     "x": 832,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fast-transport-belt.png",
+    "name": "item/arithmetic-combinator.png",
     "x": 896,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fast-underground-belt.png",
+    "name": "item/artificial-jellynut-soil.png",
     "x": 960,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/firearm-magazine.png",
+    "name": "item/artificial-yumako-soil.png",
     "x": 1024,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fission-reactor-equipment.png",
+    "name": "item/artillery-shell.png",
     "x": 1088,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/flamethrower-ammo.png",
+    "name": "item/artillery-targeting-remote.png",
     "x": 1152,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/flamethrower-turret.png",
+    "name": "item/artillery-turret.png",
     "x": 1216,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/flamethrower.png",
+    "name": "item/artillery-wagon.png",
     "x": 1280,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fluid-wagon.png",
+    "name": "item/assembling-machine-1.png",
     "x": 1344,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fluoroketone-cold-barrel.png",
+    "name": "item/assembling-machine-2.png",
     "x": 1408,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/fluoroketone-hot-barrel.png",
+    "name": "item/assembling-machine-3.png",
     "x": 1472,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/flying-robot-frame.png",
+    "name": "item/asteroid-collector.png",
     "x": 1536,
     "y": 320,
     "size": 64
   },
   {
-    "name": "item/foundation.png",
+    "name": "item/atomic-bomb.png",
+    "x": 1600,
+    "y": 320,
+    "size": 64
+  },
+  {
+    "name": "item/automation-science-pack.png",
+    "x": 1664,
+    "y": 320,
+    "size": 64
+  },
+  {
+    "name": "item/barrel.png",
     "x": 0,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/foundry.png",
+    "name": "item/battery-equipment.png",
     "x": 64,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/fusion-generator.png",
+    "name": "item/battery-mk2-equipment.png",
     "x": 128,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/fusion-power-cell.png",
+    "name": "item/battery-mk3-equipment.png",
     "x": 192,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/fusion-reactor-equipment.png",
+    "name": "item/battery.png",
     "x": 256,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/fusion-reactor.png",
+    "name": "item/beacon.png",
     "x": 320,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/gate.png",
+    "name": "item/belt-immunity-equipment.png",
     "x": 384,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/grenade.png",
+    "name": "item/big-electric-pole.png",
     "x": 448,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/gun-turret.png",
+    "name": "item/big-mining-drill.png",
     "x": 512,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/hazard-concrete.png",
+    "name": "item/biochamber.png",
     "x": 576,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/heat-exchanger.png",
+    "name": "item/bioflux.png",
     "x": 640,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/heat-pipe.png",
+    "name": "item/biolab.png",
     "x": 704,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/heating-tower.png",
+    "name": "item/biter-egg.png",
     "x": 768,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/heavy-armor.png",
+    "name": "item/blueprint-book.png",
     "x": 832,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/heavy-oil-barrel.png",
+    "name": "item/blueprint.png",
     "x": 896,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/holmium-ore.png",
+    "name": "item/boiler.png",
     "x": 960,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/holmium-plate.png",
+    "name": "item/buffer-chest.png",
     "x": 1024,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/ice-platform.png",
+    "name": "item/bulk-inserter.png",
     "x": 1088,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/ice.png",
+    "name": "item/burner-inserter.png",
     "x": 1152,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/inserter.png",
+    "name": "item/burner-mining-drill.png",
     "x": 1216,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/iron-bacteria.png",
+    "name": "item/calcite.png",
     "x": 1280,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/iron-chest.png",
+    "name": "item/cannon-shell.png",
     "x": 1344,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/iron-gear-wheel.png",
+    "name": "item/captive-biter-spawner.png",
     "x": 1408,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/iron-ore.png",
+    "name": "item/capture-robot-rocket.png",
     "x": 1472,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/iron-plate.png",
+    "name": "item/car.png",
     "x": 1536,
     "y": 384,
     "size": 64
   },
   {
-    "name": "item/iron-stick.png",
+    "name": "item/carbon-fiber.png",
+    "x": 1600,
+    "y": 384,
+    "size": 64
+  },
+  {
+    "name": "item/carbon.png",
+    "x": 1664,
+    "y": 384,
+    "size": 64
+  },
+  {
+    "name": "item/carbonic-asteroid-chunk.png",
     "x": 0,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/jelly.png",
+    "name": "item/cargo-bay.png",
     "x": 64,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/jellynut-seed.png",
+    "name": "item/cargo-landing-pad.png",
     "x": 128,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/jellynut.png",
+    "name": "item/cargo-wagon.png",
     "x": 192,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/lab.png",
+    "name": "item/centrifuge.png",
     "x": 256,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/land-mine.png",
+    "name": "item/chemical-plant.png",
     "x": 320,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/landfill.png",
+    "name": "item/chemical-science-pack.png",
     "x": 384,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/laser-turret.png",
+    "name": "item/cliff-explosives.png",
     "x": 448,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/light-armor.png",
+    "name": "item/cluster-grenade.png",
     "x": 512,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/light-oil-barrel.png",
+    "name": "item/coal.png",
     "x": 576,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/lightning-collector.png",
+    "name": "item/combat-shotgun.png",
     "x": 640,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/lightning-rod.png",
+    "name": "item/concrete.png",
     "x": 704,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/lithium-plate.png",
+    "name": "item/constant-combinator.png",
     "x": 768,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/lithium.png",
+    "name": "item/construction-robot.png",
     "x": 832,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/locomotive.png",
+    "name": "item/copper-bacteria.png",
     "x": 896,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/logistic-robot.png",
+    "name": "item/copper-cable.png",
     "x": 960,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/logistic-science-pack.png",
+    "name": "item/copper-ore.png",
     "x": 1024,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/long-handed-inserter.png",
+    "name": "item/copper-plate.png",
     "x": 1088,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/low-density-structure.png",
+    "name": "item/crude-oil-barrel.png",
     "x": 1152,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/lubricant-barrel.png",
+    "name": "item/crusher.png",
     "x": 1216,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/mech-armor.png",
+    "name": "item/cryogenic-plant.png",
     "x": 1280,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/medium-electric-pole.png",
+    "name": "item/cryogenic-science-pack.png",
     "x": 1344,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/metallic-asteroid-chunk.png",
+    "name": "item/decider-combinator.png",
     "x": 1408,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/metallurgic-science-pack.png",
+    "name": "item/deconstruction-planner.png",
     "x": 1472,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/military-science-pack.png",
+    "name": "item/defender-capsule.png",
     "x": 1536,
     "y": 448,
     "size": 64
   },
   {
-    "name": "item/modular-armor.png",
+    "name": "item/depleted-uranium-fuel-cell.png",
+    "x": 1600,
+    "y": 448,
+    "size": 64
+  },
+  {
+    "name": "item/destroyer-capsule.png",
+    "x": 1664,
+    "y": 448,
+    "size": 64
+  },
+  {
+    "name": "item/discharge-defense-equipment.png",
     "x": 0,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/night-vision-equipment.png",
+    "name": "item/discharge-defense-remote.png",
     "x": 64,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/nuclear-fuel.png",
+    "name": "item/display-panel.png",
     "x": 128,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/nuclear-reactor.png",
+    "name": "item/distractor-capsule.png",
     "x": 192,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/nutrients.png",
+    "name": "item/efficiency-module-2.png",
     "x": 256,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/offshore-pump.png",
+    "name": "item/efficiency-module-3.png",
     "x": 320,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/oil-refinery.png",
+    "name": "item/efficiency-module.png",
     "x": 384,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/overgrowth-jellynut-soil.png",
+    "name": "item/electric-engine-unit.png",
     "x": 448,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/overgrowth-yumako-soil.png",
+    "name": "item/electric-furnace.png",
     "x": 512,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/oxide-asteroid-chunk.png",
+    "name": "item/electric-mining-drill.png",
     "x": 576,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/passive-provider-chest.png",
+    "name": "item/electromagnetic-plant.png",
     "x": 640,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/pentapod-egg.png",
+    "name": "item/electromagnetic-science-pack.png",
     "x": 704,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/personal-laser-defense-equipment.png",
+    "name": "item/electronic-circuit.png",
     "x": 768,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/personal-roboport-equipment.png",
+    "name": "item/energy-shield-equipment.png",
     "x": 832,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/personal-roboport-mk2-equipment.png",
+    "name": "item/energy-shield-mk2-equipment.png",
     "x": 896,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/petroleum-gas-barrel.png",
+    "name": "item/engine-unit.png",
     "x": 960,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/piercing-rounds-magazine.png",
+    "name": "item/exoskeleton-equipment.png",
     "x": 1024,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/piercing-shotgun-shell.png",
+    "name": "item/explosive-cannon-shell.png",
     "x": 1088,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/pipe-to-ground.png",
+    "name": "item/explosive-rocket.png",
     "x": 1152,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/pipe.png",
+    "name": "item/explosive-uranium-cannon-shell.png",
     "x": 1216,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/pistol.png",
+    "name": "item/explosives.png",
     "x": 1280,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/plastic-bar.png",
+    "name": "item/express-splitter.png",
     "x": 1344,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/poison-capsule.png",
+    "name": "item/express-transport-belt.png",
     "x": 1408,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/power-armor-mk2.png",
+    "name": "item/express-underground-belt.png",
     "x": 1472,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/power-armor.png",
+    "name": "item/fast-inserter.png",
     "x": 1536,
     "y": 512,
     "size": 64
   },
   {
-    "name": "item/power-switch.png",
+    "name": "item/fast-splitter.png",
+    "x": 1600,
+    "y": 512,
+    "size": 64
+  },
+  {
+    "name": "item/fast-transport-belt.png",
+    "x": 1664,
+    "y": 512,
+    "size": 64
+  },
+  {
+    "name": "item/fast-underground-belt.png",
     "x": 0,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/processing-unit.png",
+    "name": "item/firearm-magazine.png",
     "x": 64,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/production-science-pack.png",
+    "name": "item/fission-reactor-equipment.png",
     "x": 128,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/productivity-module-2.png",
+    "name": "item/flamethrower-ammo.png",
     "x": 192,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/productivity-module-3.png",
+    "name": "item/flamethrower-turret.png",
     "x": 256,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/productivity-module.png",
+    "name": "item/flamethrower.png",
     "x": 320,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/programmable-speaker.png",
+    "name": "item/fluid-wagon.png",
     "x": 384,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/promethium-asteroid-chunk.png",
+    "name": "item/fluoroketone-cold-barrel.png",
     "x": 448,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/promethium-science-pack.png",
+    "name": "item/fluoroketone-hot-barrel.png",
     "x": 512,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/pump.png",
+    "name": "item/flying-robot-frame.png",
     "x": 576,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/pumpjack.png",
+    "name": "item/foundation.png",
     "x": 640,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/quality-module-2.png",
+    "name": "item/foundry.png",
     "x": 704,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/quality-module-3.png",
+    "name": "item/fusion-generator.png",
     "x": 768,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/quality-module.png",
+    "name": "item/fusion-power-cell.png",
     "x": 832,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/quantum-processor.png",
+    "name": "item/fusion-reactor-equipment.png",
     "x": 896,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/radar.png",
+    "name": "item/fusion-reactor.png",
     "x": 960,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/rail-chain-signal.png",
+    "name": "item/gate.png",
     "x": 1024,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/rail-ramp.png",
+    "name": "item/grenade.png",
     "x": 1088,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/rail-signal.png",
+    "name": "item/gun-turret.png",
     "x": 1152,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/rail-support.png",
+    "name": "item/hazard-concrete.png",
     "x": 1216,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/rail.png",
+    "name": "item/heat-exchanger.png",
     "x": 1280,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/railgun-ammo.png",
+    "name": "item/heat-pipe.png",
     "x": 1344,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/railgun-turret.png",
+    "name": "item/heating-tower.png",
     "x": 1408,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/railgun.png",
+    "name": "item/heavy-armor.png",
     "x": 1472,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/raw-fish.png",
+    "name": "item/heavy-oil-barrel.png",
     "x": 1536,
     "y": 576,
     "size": 64
   },
   {
-    "name": "item/recycler.png",
+    "name": "item/holmium-ore.png",
+    "x": 1600,
+    "y": 576,
+    "size": 64
+  },
+  {
+    "name": "item/holmium-plate.png",
+    "x": 1664,
+    "y": 576,
+    "size": 64
+  },
+  {
+    "name": "item/ice-platform.png",
     "x": 0,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/refined-concrete.png",
+    "name": "item/ice.png",
     "x": 64,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/refined-hazard-concrete.png",
+    "name": "item/inserter.png",
     "x": 128,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/repair-pack.png",
+    "name": "item/iron-bacteria.png",
     "x": 192,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/requester-chest.png",
+    "name": "item/iron-chest.png",
     "x": 256,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/roboport.png",
+    "name": "item/iron-gear-wheel.png",
     "x": 320,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/rocket-fuel.png",
+    "name": "item/iron-ore.png",
     "x": 384,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/rocket-launcher.png",
+    "name": "item/iron-plate.png",
     "x": 448,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/rocket-silo.png",
+    "name": "item/iron-stick.png",
     "x": 512,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/rocket-turret.png",
+    "name": "item/jelly.png",
     "x": 576,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/rocket.png",
+    "name": "item/jellynut-seed.png",
     "x": 640,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/scrap.png",
+    "name": "item/jellynut.png",
     "x": 704,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/selector-combinator.png",
+    "name": "item/lab.png",
     "x": 768,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/shotgun-shell.png",
+    "name": "item/land-mine.png",
     "x": 832,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/shotgun.png",
+    "name": "item/landfill.png",
     "x": 896,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/slowdown-capsule.png",
+    "name": "item/laser-turret.png",
     "x": 960,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/small-electric-pole.png",
+    "name": "item/light-armor.png",
     "x": 1024,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/small-lamp.png",
+    "name": "item/light-oil-barrel.png",
     "x": 1088,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/solar-panel-equipment.png",
+    "name": "item/lightning-collector.png",
     "x": 1152,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/solar-panel.png",
+    "name": "item/lightning-rod.png",
     "x": 1216,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/solid-fuel.png",
+    "name": "item/lithium-plate.png",
     "x": 1280,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/space-platform-foundation.png",
+    "name": "item/lithium.png",
     "x": 1344,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/space-platform-starter-pack.png",
+    "name": "item/locomotive.png",
     "x": 1408,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/space-science-pack.png",
+    "name": "item/logistic-robot.png",
     "x": 1472,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/speed-module-2.png",
+    "name": "item/logistic-science-pack.png",
     "x": 1536,
     "y": 640,
     "size": 64
   },
   {
-    "name": "item/speed-module-3.png",
+    "name": "item/long-handed-inserter.png",
+    "x": 1600,
+    "y": 640,
+    "size": 64
+  },
+  {
+    "name": "item/low-density-structure.png",
+    "x": 1664,
+    "y": 640,
+    "size": 64
+  },
+  {
+    "name": "item/lubricant-barrel.png",
     "x": 0,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/speed-module.png",
+    "name": "item/mech-armor.png",
     "x": 64,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/spidertron-remote.png",
+    "name": "item/medium-electric-pole.png",
     "x": 128,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/spidertron.png",
+    "name": "item/metallic-asteroid-chunk.png",
     "x": 192,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/splitter.png",
+    "name": "item/metallurgic-science-pack.png",
     "x": 256,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/spoilage.png",
+    "name": "item/military-science-pack.png",
     "x": 320,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/stack-inserter.png",
+    "name": "item/modular-armor.png",
     "x": 384,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/steam-engine.png",
+    "name": "item/night-vision-equipment.png",
     "x": 448,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/steam-turbine.png",
+    "name": "item/nuclear-fuel.png",
     "x": 512,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/steel-chest.png",
+    "name": "item/nuclear-reactor.png",
     "x": 576,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/steel-furnace.png",
+    "name": "item/nutrients.png",
     "x": 640,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/steel-plate.png",
+    "name": "item/offshore-pump.png",
     "x": 704,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/stone-brick.png",
+    "name": "item/oil-refinery.png",
     "x": 768,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/stone-furnace.png",
+    "name": "item/overgrowth-jellynut-soil.png",
     "x": 832,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/stone-wall.png",
+    "name": "item/overgrowth-yumako-soil.png",
     "x": 896,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/stone.png",
+    "name": "item/oxide-asteroid-chunk.png",
     "x": 960,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/storage-chest.png",
+    "name": "item/parameter-0.png",
     "x": 1024,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/storage-tank.png",
+    "name": "item/parameter-1.png",
     "x": 1088,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/submachine-gun.png",
+    "name": "item/parameter-2.png",
     "x": 1152,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/substation.png",
+    "name": "item/parameter-3.png",
     "x": 1216,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/sulfur.png",
+    "name": "item/parameter-4.png",
     "x": 1280,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/sulfuric-acid-barrel.png",
+    "name": "item/parameter-5.png",
     "x": 1344,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/supercapacitor.png",
+    "name": "item/parameter-6.png",
     "x": 1408,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/superconductor.png",
+    "name": "item/parameter-7.png",
     "x": 1472,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/tank.png",
+    "name": "item/parameter-8.png",
     "x": 1536,
     "y": 704,
     "size": 64
   },
   {
-    "name": "item/tesla-ammo.png",
+    "name": "item/parameter-9.png",
+    "x": 1600,
+    "y": 704,
+    "size": 64
+  },
+  {
+    "name": "item/passive-provider-chest.png",
+    "x": 1664,
+    "y": 704,
+    "size": 64
+  },
+  {
+    "name": "item/pentapod-egg.png",
     "x": 0,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/tesla-turret.png",
+    "name": "item/personal-laser-defense-equipment.png",
     "x": 64,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/teslagun.png",
+    "name": "item/personal-roboport-equipment.png",
     "x": 128,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/thruster.png",
+    "name": "item/personal-roboport-mk2-equipment.png",
     "x": 192,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/toolbelt-equipment.png",
+    "name": "item/petroleum-gas-barrel.png",
     "x": 256,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/train-stop.png",
+    "name": "item/piercing-rounds-magazine.png",
     "x": 320,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/transport-belt.png",
+    "name": "item/piercing-shotgun-shell.png",
     "x": 384,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/tree-seed.png",
+    "name": "item/pipe-to-ground.png",
     "x": 448,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/tungsten-carbide.png",
+    "name": "item/pipe.png",
     "x": 512,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/tungsten-ore.png",
+    "name": "item/pistol.png",
     "x": 576,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/tungsten-plate.png",
+    "name": "item/plastic-bar.png",
     "x": 640,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/turbo-splitter.png",
+    "name": "item/poison-capsule.png",
     "x": 704,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/turbo-transport-belt.png",
+    "name": "item/power-armor-mk2.png",
     "x": 768,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/turbo-underground-belt.png",
+    "name": "item/power-armor.png",
     "x": 832,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/underground-belt.png",
+    "name": "item/power-switch.png",
     "x": 896,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/uranium-235.png",
+    "name": "item/processing-unit.png",
     "x": 960,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/uranium-238.png",
+    "name": "item/production-science-pack.png",
     "x": 1024,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/uranium-cannon-shell.png",
+    "name": "item/productivity-module-2.png",
     "x": 1088,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/uranium-fuel-cell.png",
+    "name": "item/productivity-module-3.png",
     "x": 1152,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/uranium-ore.png",
+    "name": "item/productivity-module.png",
     "x": 1216,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/uranium-rounds-magazine.png",
+    "name": "item/programmable-speaker.png",
     "x": 1280,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/utility-science-pack.png",
+    "name": "item/promethium-asteroid-chunk.png",
     "x": 1344,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/water-barrel.png",
+    "name": "item/promethium-science-pack.png",
     "x": 1408,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/wood.png",
+    "name": "item/pump.png",
     "x": 1472,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/wooden-chest.png",
+    "name": "item/pumpjack.png",
     "x": 1536,
     "y": 768,
     "size": 64
   },
   {
-    "name": "item/yumako-mash.png",
+    "name": "item/quality-module-2.png",
+    "x": 1600,
+    "y": 768,
+    "size": 64
+  },
+  {
+    "name": "item/quality-module-3.png",
+    "x": 1664,
+    "y": 768,
+    "size": 64
+  },
+  {
+    "name": "item/quality-module.png",
     "x": 0,
     "y": 832,
     "size": 64
   },
   {
-    "name": "item/yumako-seed.png",
+    "name": "item/quantum-processor.png",
     "x": 64,
     "y": 832,
     "size": 64
   },
   {
-    "name": "item/yumako.png",
+    "name": "item/radar.png",
     "x": 128,
     "y": 832,
     "size": 64
   },
   {
-    "name": "quality/epic.png",
+    "name": "item/rail-chain-signal.png",
     "x": 192,
     "y": 832,
     "size": 64
   },
   {
-    "name": "quality/legendary.png",
+    "name": "item/rail-ramp.png",
     "x": 256,
     "y": 832,
     "size": 64
   },
   {
-    "name": "quality/normal.png",
+    "name": "item/rail-signal.png",
     "x": 320,
     "y": 832,
     "size": 64
   },
   {
-    "name": "quality/rare.png",
+    "name": "item/rail-support.png",
     "x": 384,
     "y": 832,
     "size": 64
   },
   {
-    "name": "quality/uncommon.png",
+    "name": "item/rail.png",
     "x": 448,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/acid-neutralisation.png",
+    "name": "item/railgun-ammo.png",
     "x": 512,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/advanced-carbonic-asteroid-crushing.png",
+    "name": "item/railgun-turret.png",
     "x": 576,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/advanced-metallic-asteroid-crushing.png",
+    "name": "item/railgun.png",
     "x": 640,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/advanced-oil-processing.png",
+    "name": "item/raw-fish.png",
     "x": 704,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/advanced-oxide-asteroid-crushing.png",
+    "name": "item/recycler.png",
     "x": 768,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/advanced-thruster-fuel.png",
+    "name": "item/refined-concrete.png",
     "x": 832,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/advanced-thruster-oxidizer.png",
+    "name": "item/refined-hazard-concrete.png",
     "x": 896,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/ammonia-rocket-fuel.png",
+    "name": "item/repair-pack.png",
     "x": 960,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/ammoniacal-solution-separation.png",
+    "name": "item/requester-chest.png",
     "x": 1024,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/artificial-jellynut-soil.png",
+    "name": "item/roboport.png",
     "x": 1088,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/artificial-yumako-soil.png",
+    "name": "item/rocket-fuel.png",
     "x": 1152,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/basic-oil-processing.png",
+    "name": "item/rocket-launcher.png",
     "x": 1216,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/bioflux.png",
+    "name": "item/rocket-part.png",
     "x": 1280,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/biolubricant.png",
+    "name": "item/rocket-silo.png",
     "x": 1344,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/bioplastic.png",
+    "name": "item/rocket-turret.png",
     "x": 1408,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/biosulfur.png",
+    "name": "item/rocket.png",
     "x": 1472,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/biter-egg.png",
+    "name": "item/scrap.png",
     "x": 1536,
     "y": 832,
     "size": 64
   },
   {
-    "name": "recipe/burnt-spoilage.png",
+    "name": "item/selector-combinator.png",
+    "x": 1600,
+    "y": 832,
+    "size": 64
+  },
+  {
+    "name": "item/shotgun-shell.png",
+    "x": 1664,
+    "y": 832,
+    "size": 64
+  },
+  {
+    "name": "item/shotgun.png",
     "x": 0,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/carbon.png",
+    "name": "item/slowdown-capsule.png",
     "x": 64,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/carbonic-asteroid-crushing.png",
+    "name": "item/small-electric-pole.png",
     "x": 128,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/carbonic-asteroid-reprocessing.png",
+    "name": "item/small-lamp.png",
     "x": 192,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-copper-cable.png",
+    "name": "item/solar-panel-equipment.png",
     "x": 256,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-copper.png",
+    "name": "item/solar-panel.png",
     "x": 320,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-iron-gear-wheel.png",
+    "name": "item/solid-fuel.png",
     "x": 384,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-iron-stick.png",
+    "name": "item/space-platform-foundation.png",
     "x": 448,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-iron.png",
+    "name": "item/space-platform-starter-pack.png",
     "x": 512,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-low-density-structure.png",
+    "name": "item/space-science-pack.png",
     "x": 576,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-pipe-to-ground.png",
+    "name": "item/speed-module-2.png",
     "x": 640,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-pipe.png",
+    "name": "item/speed-module-3.png",
     "x": 704,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/casting-steel.png",
+    "name": "item/speed-module.png",
     "x": 768,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/coal-liquefaction.png",
+    "name": "item/spidertron-remote.png",
     "x": 832,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/coal-synthesis.png",
+    "name": "item/spidertron.png",
     "x": 896,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/concrete-from-molten-iron.png",
+    "name": "item/splitter.png",
     "x": 960,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/copper-bacteria-cultivation.png",
+    "name": "item/spoilage.png",
     "x": 1024,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/copper-bacteria.png",
+    "name": "item/stack-inserter.png",
     "x": 1088,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/crude-oil-barrel.png",
+    "name": "item/steam-engine.png",
     "x": 1152,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-crude-oil-barrel.png",
+    "name": "item/steam-turbine.png",
     "x": 1216,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-fluoroketone-cold-barrel.png",
+    "name": "item/steel-chest.png",
     "x": 1280,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-fluoroketone-hot-barrel.png",
+    "name": "item/steel-furnace.png",
     "x": 1344,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-heavy-oil-barrel.png",
+    "name": "item/steel-plate.png",
     "x": 1408,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-light-oil-barrel.png",
+    "name": "item/stone-brick.png",
     "x": 1472,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-lubricant-barrel.png",
+    "name": "item/stone-furnace.png",
     "x": 1536,
     "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-petroleum-gas-barrel.png",
-    "x": 0,
-    "y": 960,
+    "name": "item/stone-wall.png",
+    "x": 1600,
+    "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-sulfuric-acid-barrel.png",
-    "x": 64,
-    "y": 960,
+    "name": "item/stone.png",
+    "x": 1664,
+    "y": 896,
     "size": 64
   },
   {
-    "name": "recipe/empty-water-barrel.png",
-    "x": 128,
+    "name": "item/storage-chest.png",
+    "x": 0,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/fish-breeding.png",
+    "name": "item/storage-tank.png",
+    "x": 64,
+    "y": 960,
+    "size": 64
+  },
+  {
+    "name": "item/submachine-gun.png",
+    "x": 128,
+    "y": 960,
+    "size": 64
+  },
+  {
+    "name": "item/substation.png",
     "x": 192,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/fluoroketone-cold-barrel.png",
+    "name": "item/sulfur.png",
     "x": 256,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/fluoroketone-cooling.png",
+    "name": "item/sulfuric-acid-barrel.png",
     "x": 320,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/fluoroketone-hot-barrel.png",
+    "name": "item/supercapacitor.png",
     "x": 384,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/heavy-oil-barrel.png",
+    "name": "item/superconductor.png",
     "x": 448,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/heavy-oil-cracking.png",
+    "name": "item/tank.png",
     "x": 512,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/ice-melting.png",
+    "name": "item/tesla-ammo.png",
     "x": 576,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/iron-bacteria-cultivation.png",
+    "name": "item/tesla-turret.png",
     "x": 640,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/iron-bacteria.png",
+    "name": "item/teslagun.png",
     "x": 704,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/jellynut-processing.png",
+    "name": "item/thruster.png",
     "x": 768,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/kovarex-enrichment-process.png",
+    "name": "item/toolbelt-equipment.png",
     "x": 832,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/light-oil-barrel.png",
+    "name": "item/train-stop.png",
     "x": 896,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/light-oil-cracking.png",
+    "name": "item/transport-belt.png",
     "x": 960,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/lubricant-barrel.png",
+    "name": "item/tree-seed.png",
     "x": 1024,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/metallic-asteroid-crushing.png",
+    "name": "item/tungsten-carbide.png",
     "x": 1088,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/metallic-asteroid-reprocessing.png",
+    "name": "item/tungsten-ore.png",
     "x": 1152,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/molten-copper-from-lava.png",
+    "name": "item/tungsten-plate.png",
     "x": 1216,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/molten-copper.png",
+    "name": "item/turbo-splitter.png",
     "x": 1280,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/molten-iron-from-lava.png",
+    "name": "item/turbo-transport-belt.png",
     "x": 1344,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/molten-iron.png",
+    "name": "item/turbo-underground-belt.png",
     "x": 1408,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/nuclear-fuel-reprocessing.png",
+    "name": "item/underground-belt.png",
     "x": 1472,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/nutrients-from-bioflux.png",
+    "name": "item/upgrade-planner.png",
     "x": 1536,
     "y": 960,
     "size": 64
   },
   {
-    "name": "recipe/nutrients-from-biter-egg.png",
+    "name": "item/uranium-235.png",
+    "x": 1600,
+    "y": 960,
+    "size": 64
+  },
+  {
+    "name": "item/uranium-238.png",
+    "x": 1664,
+    "y": 960,
+    "size": 64
+  },
+  {
+    "name": "item/uranium-cannon-shell.png",
     "x": 0,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/nutrients-from-fish.png",
+    "name": "item/uranium-fuel-cell.png",
     "x": 64,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/nutrients-from-spoilage.png",
+    "name": "item/uranium-ore.png",
     "x": 128,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/nutrients-from-yumako-mash.png",
+    "name": "item/uranium-rounds-magazine.png",
     "x": 192,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/overgrowth-jellynut-soil.png",
+    "name": "item/utility-science-pack.png",
     "x": 256,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/overgrowth-yumako-soil.png",
+    "name": "item/water-barrel.png",
     "x": 320,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/oxide-asteroid-crushing.png",
+    "name": "item/wood.png",
     "x": 384,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/oxide-asteroid-reprocessing.png",
+    "name": "item/wooden-chest.png",
     "x": 448,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-0.png",
+    "name": "item/yumako-mash.png",
     "x": 512,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-1.png",
+    "name": "item/yumako-seed.png",
     "x": 576,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-2.png",
+    "name": "item/yumako.png",
     "x": 640,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-3.png",
+    "name": "quality/epic.png",
     "x": 704,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-4.png",
+    "name": "quality/legendary.png",
     "x": 768,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-5.png",
+    "name": "quality/normal.png",
     "x": 832,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-6.png",
+    "name": "quality/rare.png",
     "x": 896,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-7.png",
+    "name": "quality/uncommon.png",
     "x": 960,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-8.png",
+    "name": "recipe/acid-neutralisation.png",
     "x": 1024,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/parameter-9.png",
+    "name": "recipe/advanced-carbonic-asteroid-crushing.png",
     "x": 1088,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/pentapod-egg.png",
+    "name": "recipe/advanced-metallic-asteroid-crushing.png",
     "x": 1152,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/petroleum-gas-barrel.png",
+    "name": "recipe/advanced-oil-processing.png",
     "x": 1216,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/rocket-fuel-from-jelly.png",
+    "name": "recipe/advanced-oxide-asteroid-crushing.png",
     "x": 1280,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/scrap-recycling.png",
+    "name": "recipe/advanced-thruster-fuel.png",
     "x": 1344,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/simple-coal-liquefaction.png",
+    "name": "recipe/advanced-thruster-oxidizer.png",
     "x": 1408,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/solid-fuel-from-ammonia.png",
+    "name": "recipe/ammonia-rocket-fuel.png",
     "x": 1472,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/solid-fuel-from-heavy-oil.png",
+    "name": "recipe/ammoniacal-solution-separation.png",
     "x": 1536,
     "y": 1024,
     "size": 64
   },
   {
-    "name": "recipe/solid-fuel-from-light-oil.png",
+    "name": "recipe/artificial-jellynut-soil.png",
+    "x": 1600,
+    "y": 1024,
+    "size": 64
+  },
+  {
+    "name": "recipe/artificial-yumako-soil.png",
+    "x": 1664,
+    "y": 1024,
+    "size": 64
+  },
+  {
+    "name": "recipe/basic-oil-processing.png",
     "x": 0,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/solid-fuel-from-petroleum-gas.png",
+    "name": "recipe/bioflux.png",
     "x": 64,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/space-science-pack.png",
+    "name": "recipe/biolubricant.png",
     "x": 128,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/steam-condensation.png",
+    "name": "recipe/bioplastic.png",
     "x": 192,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/sulfuric-acid-barrel.png",
+    "name": "recipe/biosulfur.png",
     "x": 256,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/uranium-processing.png",
+    "name": "recipe/biter-egg.png",
     "x": 320,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/water-barrel.png",
+    "name": "recipe/burnt-spoilage.png",
     "x": 384,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/wood-processing.png",
+    "name": "recipe/carbon.png",
     "x": 448,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "recipe/yumako-processing.png",
+    "name": "recipe/carbonic-asteroid-crushing.png",
     "x": 512,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/down-arrow.png",
+    "name": "recipe/carbonic-asteroid-reprocessing.png",
     "x": 576,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/down-left-arrow.png",
+    "name": "recipe/casting-copper-cable.png",
     "x": 640,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/down-right-arrow.png",
+    "name": "recipe/casting-copper.png",
     "x": 704,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/left-arrow.png",
+    "name": "recipe/casting-iron-gear-wheel.png",
     "x": 768,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/right-arrow.png",
+    "name": "recipe/casting-iron-stick.png",
     "x": 832,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-circle.png",
+    "name": "recipe/casting-iron.png",
     "x": 896,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-corner-2.png",
+    "name": "recipe/casting-low-density-structure.png",
     "x": 960,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-corner-3.png",
+    "name": "recipe/casting-pipe-to-ground.png",
     "x": 1024,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-corner-4.png",
+    "name": "recipe/casting-pipe.png",
     "x": 1088,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-corner.png",
+    "name": "recipe/casting-steel.png",
     "x": 1152,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-cross.png",
+    "name": "recipe/coal-liquefaction.png",
     "x": 1216,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-curve-2.png",
+    "name": "recipe/coal-synthesis.png",
     "x": 1280,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-curve-3.png",
+    "name": "recipe/concrete-from-molten-iron.png",
     "x": 1344,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-curve-4.png",
+    "name": "recipe/copper-bacteria-cultivation.png",
     "x": 1408,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-curve.png",
+    "name": "recipe/copper-bacteria.png",
     "x": 1472,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-diagonal-2.png",
+    "name": "recipe/crude-oil-barrel.png",
     "x": 1536,
     "y": 1088,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-diagonal-cross.png",
+    "name": "recipe/empty-crude-oil-barrel.png",
+    "x": 1600,
+    "y": 1088,
+    "size": 64
+  },
+  {
+    "name": "recipe/empty-fluoroketone-cold-barrel.png",
+    "x": 1664,
+    "y": 1088,
+    "size": 64
+  },
+  {
+    "name": "recipe/empty-fluoroketone-hot-barrel.png",
     "x": 0,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-diagonal.png",
+    "name": "recipe/empty-heavy-oil-barrel.png",
     "x": 64,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-horizontal.png",
+    "name": "recipe/empty-light-oil-barrel.png",
     "x": 128,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-t-2.png",
+    "name": "recipe/empty-lubricant-barrel.png",
     "x": 192,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-t-3.png",
+    "name": "recipe/empty-petroleum-gas-barrel.png",
     "x": 256,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-t-4.png",
+    "name": "recipe/empty-sulfuric-acid-barrel.png",
     "x": 320,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-t.png",
+    "name": "recipe/empty-water-barrel.png",
     "x": 384,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/shape-vertical.png",
+    "name": "recipe/fish-breeding.png",
     "x": 448,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-0.png",
+    "name": "recipe/fluoroketone-cold-barrel.png",
     "x": 512,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-1.png",
+    "name": "recipe/fluoroketone-cooling.png",
     "x": 576,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-2.png",
+    "name": "recipe/fluoroketone-hot-barrel.png",
     "x": 640,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-3.png",
+    "name": "recipe/heavy-oil-barrel.png",
     "x": 704,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-4.png",
+    "name": "recipe/heavy-oil-cracking.png",
     "x": 768,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-5.png",
+    "name": "recipe/ice-melting.png",
     "x": 832,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-6.png",
+    "name": "recipe/iron-bacteria-cultivation.png",
     "x": 896,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-7.png",
+    "name": "recipe/iron-bacteria.png",
     "x": 960,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-8.png",
+    "name": "recipe/jellynut-processing.png",
     "x": 1024,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-9.png",
+    "name": "recipe/kovarex-enrichment-process.png",
     "x": 1088,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-A.png",
+    "name": "recipe/light-oil-barrel.png",
     "x": 1152,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-B.png",
+    "name": "recipe/light-oil-cracking.png",
     "x": 1216,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-C.png",
+    "name": "recipe/lubricant-barrel.png",
     "x": 1280,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-D.png",
+    "name": "recipe/metallic-asteroid-crushing.png",
     "x": 1344,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-E.png",
+    "name": "recipe/metallic-asteroid-reprocessing.png",
     "x": 1408,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-F.png",
+    "name": "recipe/molten-copper-from-lava.png",
     "x": 1472,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-G.png",
+    "name": "recipe/molten-copper.png",
     "x": 1536,
     "y": 1152,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-H.png",
+    "name": "recipe/molten-iron-from-lava.png",
+    "x": 1600,
+    "y": 1152,
+    "size": 64
+  },
+  {
+    "name": "recipe/molten-iron.png",
+    "x": 1664,
+    "y": 1152,
+    "size": 64
+  },
+  {
+    "name": "recipe/nuclear-fuel-reprocessing.png",
     "x": 0,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-I.png",
+    "name": "recipe/nutrients-from-bioflux.png",
     "x": 64,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-J.png",
+    "name": "recipe/nutrients-from-biter-egg.png",
     "x": 128,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-K.png",
+    "name": "recipe/nutrients-from-fish.png",
     "x": 192,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-L.png",
+    "name": "recipe/nutrients-from-spoilage.png",
     "x": 256,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-M.png",
+    "name": "recipe/nutrients-from-yumako-mash.png",
     "x": 320,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-N.png",
+    "name": "recipe/overgrowth-jellynut-soil.png",
     "x": 384,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-O.png",
+    "name": "recipe/overgrowth-yumako-soil.png",
     "x": 448,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-P.png",
+    "name": "recipe/oxide-asteroid-crushing.png",
     "x": 512,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-Q.png",
+    "name": "recipe/oxide-asteroid-reprocessing.png",
     "x": 576,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-R.png",
+    "name": "recipe/parameter-0.png",
     "x": 640,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-S.png",
+    "name": "recipe/parameter-1.png",
     "x": 704,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-T.png",
+    "name": "recipe/parameter-2.png",
     "x": 768,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-U.png",
+    "name": "recipe/parameter-3.png",
     "x": 832,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-V.png",
+    "name": "recipe/parameter-4.png",
     "x": 896,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-W.png",
+    "name": "recipe/parameter-5.png",
     "x": 960,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-X.png",
+    "name": "recipe/parameter-6.png",
     "x": 1024,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-Y.png",
+    "name": "recipe/parameter-7.png",
     "x": 1088,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-Z.png",
+    "name": "recipe/parameter-8.png",
     "x": 1152,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-alarm.png",
+    "name": "recipe/parameter-9.png",
     "x": 1216,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-alert.png",
+    "name": "recipe/pentapod-egg.png",
     "x": 1280,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-ampersand.png",
+    "name": "recipe/petroleum-gas-barrel.png",
     "x": 1344,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-anticlockwise-circle-arrow.png",
+    "name": "recipe/rocket-fuel-from-jelly.png",
     "x": 1408,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-any-quality.png",
+    "name": "recipe/scrap-recycling.png",
     "x": 1472,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-anything.png",
+    "name": "recipe/simple-coal-liquefaction.png",
     "x": 1536,
     "y": 1216,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-apostrophe.png",
+    "name": "recipe/solid-fuel-from-ammonia.png",
+    "x": 1600,
+    "y": 1216,
+    "size": 64
+  },
+  {
+    "name": "recipe/solid-fuel-from-heavy-oil.png",
+    "x": 1664,
+    "y": 1216,
+    "size": 64
+  },
+  {
+    "name": "recipe/solid-fuel-from-light-oil.png",
     "x": 0,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-battery-full.png",
+    "name": "recipe/solid-fuel-from-petroleum-gas.png",
     "x": 64,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-battery-low.png",
+    "name": "recipe/space-science-pack.png",
     "x": 128,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-battery-mid-level.png",
+    "name": "recipe/steam-condensation.png",
     "x": 192,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-black.png",
+    "name": "recipe/sulfuric-acid-barrel.png",
     "x": 256,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-blue.png",
+    "name": "recipe/uranium-processing.png",
     "x": 320,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-check.png",
+    "name": "recipe/water-barrel.png",
     "x": 384,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-circumflex-accent.png",
+    "name": "recipe/wood-processing.png",
     "x": 448,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-clock.png",
+    "name": "recipe/yumako-processing.png",
     "x": 512,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-clockwise-circle-arrow.png",
+    "name": "space-location/aquilo.png",
     "x": 576,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-colon.png",
+    "name": "space-location/fulgora.png",
     "x": 640,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-comma.png",
+    "name": "space-location/gleba.png",
     "x": 704,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-cyan.png",
+    "name": "space-location/nauvis.png",
     "x": 768,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-damage.png",
+    "name": "space-location/shattered-planet.png",
     "x": 832,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-deny.png",
+    "name": "space-location/solar-system-edge.png",
     "x": 896,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-division.png",
+    "name": "space-location/vulcanus.png",
     "x": 960,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-dot.png",
+    "name": "virtual-signal/down-arrow.png",
     "x": 1024,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-each.png",
+    "name": "virtual-signal/down-left-arrow.png",
     "x": 1088,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-equal.png",
+    "name": "virtual-signal/down-right-arrow.png",
     "x": 1152,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-everything.png",
+    "name": "virtual-signal/left-arrow.png",
     "x": 1216,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-exclamation-mark.png",
+    "name": "virtual-signal/right-arrow.png",
     "x": 1280,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-explosion.png",
+    "name": "virtual-signal/shape-circle.png",
     "x": 1344,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-fire.png",
+    "name": "virtual-signal/shape-corner-2.png",
     "x": 1408,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-fluid-parameter.png",
+    "name": "virtual-signal/shape-corner-3.png",
     "x": 1472,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-fuel-parameter.png",
+    "name": "virtual-signal/shape-corner-4.png",
     "x": 1536,
     "y": 1280,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-fuel.png",
+    "name": "virtual-signal/shape-corner.png",
+    "x": 1600,
+    "y": 1280,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/shape-cross.png",
+    "x": 1664,
+    "y": 1280,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/shape-curve-2.png",
     "x": 0,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-ghost.png",
+    "name": "virtual-signal/shape-curve-3.png",
     "x": 64,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-greater-than-or-equal-to.png",
+    "name": "virtual-signal/shape-curve-4.png",
     "x": 128,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-greater-than.png",
+    "name": "virtual-signal/shape-curve.png",
     "x": 192,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-green.png",
+    "name": "virtual-signal/shape-diagonal-2.png",
     "x": 256,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-grey.png",
+    "name": "virtual-signal/shape-diagonal-cross.png",
     "x": 320,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-heart.png",
+    "name": "virtual-signal/shape-diagonal.png",
     "x": 384,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-hourglass.png",
+    "name": "virtual-signal/shape-horizontal.png",
     "x": 448,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-info.png",
+    "name": "virtual-signal/shape-t-2.png",
     "x": 512,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-input.png",
+    "name": "virtual-signal/shape-t-3.png",
     "x": 576,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-item-parameter.png",
+    "name": "virtual-signal/shape-t-4.png",
     "x": 640,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-left-parenthesis.png",
+    "name": "virtual-signal/shape-t.png",
     "x": 704,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-left-right-arrow.png",
+    "name": "virtual-signal/shape-vertical.png",
     "x": 768,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-left-square-bracket.png",
+    "name": "virtual-signal/signal-0.png",
     "x": 832,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-less-than-or-equal-to.png",
+    "name": "virtual-signal/signal-1.png",
     "x": 896,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-less-than.png",
+    "name": "virtual-signal/signal-2.png",
     "x": 960,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-letter-dot.png",
+    "name": "virtual-signal/signal-3.png",
     "x": 1024,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-lightning.png",
+    "name": "virtual-signal/signal-4.png",
     "x": 1088,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-liquid.png",
+    "name": "virtual-signal/signal-5.png",
     "x": 1152,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-lock.png",
+    "name": "virtual-signal/signal-6.png",
     "x": 1216,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-map-marker.png",
+    "name": "virtual-signal/signal-7.png",
     "x": 1280,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-mining.png",
+    "name": "virtual-signal/signal-8.png",
     "x": 1344,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-minus.png",
+    "name": "virtual-signal/signal-9.png",
     "x": 1408,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-moon.png",
+    "name": "virtual-signal/signal-A.png",
     "x": 1472,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-multiplication.png",
+    "name": "virtual-signal/signal-B.png",
     "x": 1536,
     "y": 1344,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-no-entry.png",
+    "name": "virtual-signal/signal-C.png",
+    "x": 1600,
+    "y": 1344,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-D.png",
+    "x": 1664,
+    "y": 1344,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-E.png",
     "x": 0,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-not-equal.png",
+    "name": "virtual-signal/signal-F.png",
     "x": 64,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-number-sign.png",
+    "name": "virtual-signal/signal-G.png",
     "x": 128,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-output.png",
+    "name": "virtual-signal/signal-H.png",
     "x": 192,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-percent.png",
+    "name": "virtual-signal/signal-I.png",
     "x": 256,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-pink.png",
+    "name": "virtual-signal/signal-J.png",
     "x": 320,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-plus.png",
+    "name": "virtual-signal/signal-K.png",
     "x": 384,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-question-mark.png",
+    "name": "virtual-signal/signal-L.png",
     "x": 448,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-quotation-mark.png",
+    "name": "virtual-signal/signal-M.png",
     "x": 512,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-radioactivity.png",
+    "name": "virtual-signal/signal-N.png",
     "x": 576,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-recycle.png",
+    "name": "virtual-signal/signal-O.png",
     "x": 640,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-red.png",
+    "name": "virtual-signal/signal-P.png",
     "x": 704,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-right-parenthesis.png",
+    "name": "virtual-signal/signal-Q.png",
     "x": 768,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-right-square-bracket.png",
+    "name": "virtual-signal/signal-R.png",
     "x": 832,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-rightwards-leftwards-arrow.png",
+    "name": "virtual-signal/signal-S.png",
     "x": 896,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-science-pack.png",
+    "name": "virtual-signal/signal-T.png",
     "x": 960,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-shuffle.png",
+    "name": "virtual-signal/signal-U.png",
     "x": 1024,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-signal-parameter.png",
+    "name": "virtual-signal/signal-V.png",
     "x": 1088,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-skull.png",
+    "name": "virtual-signal/signal-W.png",
     "x": 1152,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-slash.png",
+    "name": "virtual-signal/signal-X.png",
     "x": 1216,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-snowflake.png",
+    "name": "virtual-signal/signal-Y.png",
     "x": 1280,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-speed.png",
+    "name": "virtual-signal/signal-Z.png",
     "x": 1344,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-stack-size.png",
+    "name": "virtual-signal/signal-alarm.png",
     "x": 1408,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-star.png",
+    "name": "virtual-signal/signal-alert.png",
     "x": 1472,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-sun.png",
+    "name": "virtual-signal/signal-ampersand.png",
     "x": 1536,
     "y": 1408,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-thermometer-blue.png",
+    "name": "virtual-signal/signal-anticlockwise-circle-arrow.png",
+    "x": 1600,
+    "y": 1408,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-any-quality.png",
+    "x": 1664,
+    "y": 1408,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-anything.png",
     "x": 0,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-thermometer-red.png",
+    "name": "virtual-signal/signal-apostrophe.png",
     "x": 64,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-trash-bin.png",
+    "name": "virtual-signal/signal-battery-full.png",
     "x": 128,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-unlock.png",
+    "name": "virtual-signal/signal-battery-low.png",
     "x": 192,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-up-down-arrow.png",
+    "name": "virtual-signal/signal-battery-mid-level.png",
     "x": 256,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-upwards-downwards-arrow.png",
+    "name": "virtual-signal/signal-black.png",
     "x": 320,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-weapon.png",
+    "name": "virtual-signal/signal-blue.png",
     "x": 384,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-white-flag.png",
+    "name": "virtual-signal/signal-check.png",
     "x": 448,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-white.png",
+    "name": "virtual-signal/signal-circumflex-accent.png",
     "x": 512,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/signal-yellow.png",
+    "name": "virtual-signal/signal-clock.png",
     "x": 576,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/up-arrow.png",
+    "name": "virtual-signal/signal-clockwise-circle-arrow.png",
     "x": 640,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/up-left-arrow.png",
+    "name": "virtual-signal/signal-colon.png",
     "x": 704,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "virtual-signal/up-right-arrow.png",
+    "name": "virtual-signal/signal-comma.png",
     "x": 768,
     "y": 1472,
     "size": 64
   },
   {
-    "name": "item-group/combat.png",
-    "x": 0,
-    "y": 0,
-    "size": 128
-  },
-  {
-    "name": "item-group/fluids.png",
+    "name": "virtual-signal/signal-cyan.png",
+    "x": 832,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-damage.png",
+    "x": 896,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-deny.png",
+    "x": 960,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-division.png",
+    "x": 1024,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-dot.png",
+    "x": 1088,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-each.png",
+    "x": 1152,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-equal.png",
+    "x": 1216,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-everything.png",
+    "x": 1280,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-exclamation-mark.png",
+    "x": 1344,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-explosion.png",
+    "x": 1408,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-fire.png",
+    "x": 1472,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-fluid-parameter.png",
+    "x": 1536,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-fuel-parameter.png",
+    "x": 1600,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-fuel.png",
+    "x": 1664,
+    "y": 1472,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-ghost.png",
+    "x": 0,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-greater-than-or-equal-to.png",
+    "x": 64,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-greater-than.png",
+    "x": 128,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-green.png",
+    "x": 192,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-grey.png",
+    "x": 256,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-heart.png",
+    "x": 320,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-hourglass.png",
+    "x": 384,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-info.png",
+    "x": 448,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-input.png",
+    "x": 512,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-item-parameter.png",
+    "x": 576,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-left-parenthesis.png",
+    "x": 640,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-left-right-arrow.png",
+    "x": 704,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-left-square-bracket.png",
+    "x": 768,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-less-than-or-equal-to.png",
+    "x": 832,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-less-than.png",
+    "x": 896,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-letter-dot.png",
+    "x": 960,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-lightning.png",
+    "x": 1024,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-liquid.png",
+    "x": 1088,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-lock.png",
+    "x": 1152,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-map-marker.png",
+    "x": 1216,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-mining.png",
+    "x": 1280,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-minus.png",
+    "x": 1344,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-moon.png",
+    "x": 1408,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-multiplication.png",
+    "x": 1472,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-no-entry.png",
+    "x": 1536,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-not-equal.png",
+    "x": 1600,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-number-sign.png",
+    "x": 1664,
+    "y": 1536,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-output.png",
+    "x": 0,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-percent.png",
+    "x": 64,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-pink.png",
     "x": 128,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-plus.png",
+    "x": 192,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-question-mark.png",
+    "x": 256,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-quotation-mark.png",
+    "x": 320,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-radioactivity.png",
+    "x": 384,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-recycle.png",
+    "x": 448,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-red.png",
+    "x": 512,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-right-parenthesis.png",
+    "x": 576,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-right-square-bracket.png",
+    "x": 640,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-rightwards-leftwards-arrow.png",
+    "x": 704,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-science-pack.png",
+    "x": 768,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-shuffle.png",
+    "x": 832,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-signal-parameter.png",
+    "x": 896,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-skull.png",
+    "x": 960,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-slash.png",
+    "x": 1024,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-snowflake.png",
+    "x": 1088,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-speed.png",
+    "x": 1152,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-stack-size.png",
+    "x": 1216,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-star.png",
+    "x": 1280,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-sun.png",
+    "x": 1344,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-thermometer-blue.png",
+    "x": 1408,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-thermometer-red.png",
+    "x": 1472,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-trash-bin.png",
+    "x": 1536,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-unlock.png",
+    "x": 1600,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-up-down-arrow.png",
+    "x": 1664,
+    "y": 1600,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-upwards-downwards-arrow.png",
+    "x": 0,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-weapon.png",
+    "x": 64,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-white-flag.png",
+    "x": 128,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-white.png",
+    "x": 192,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/signal-yellow.png",
+    "x": 256,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/up-arrow.png",
+    "x": 320,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/up-left-arrow.png",
+    "x": 384,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "virtual-signal/up-right-arrow.png",
+    "x": 448,
+    "y": 1664,
+    "size": 64
+  },
+  {
+    "name": "item-group/combat.png",
+    "x": 0,
     "y": 0,
     "size": 128
   },
   {
-    "name": "item-group/intermediate-products.png",
+    "name": "item-group/enemies.png",
+    "x": 128,
+    "y": 0,
+    "size": 128
+  },
+  {
+    "name": "item-group/environment.png",
     "x": 256,
     "y": 0,
     "size": 128
   },
   {
-    "name": "item-group/logistics.png",
+    "name": "item-group/fluids.png",
+    "x": 384,
+    "y": 0,
+    "size": 128
+  },
+  {
+    "name": "item-group/intermediate-products.png",
     "x": 0,
     "y": 128,
     "size": 128
   },
   {
-    "name": "item-group/other.png",
+    "name": "item-group/logistics.png",
     "x": 128,
     "y": 128,
     "size": 128
   },
   {
-    "name": "item-group/production.png",
+    "name": "item-group/other.png",
     "x": 256,
     "y": 128,
     "size": 128
   },
+  {
+    "name": "item-group/production.png",
+    "x": 384,
+    "y": 128,
+    "size": 128
+  },
   {
     "name": "item-group/signals.png",
     "x": 0,

+ 50 - 42
src/assets/icon.tsx

@@ -1,42 +1,50 @@
-import { useMemo, type CSSProperties } from "react";
-import iconMap from "./data/2.0/iconMap.json";
-
-type IconProps = {
-  style?: CSSProperties;
-  className?: string;
-  iconName: string;
-  size: number;
-};
-const iconCountPerSize = iconMap.reduce((acc, icon) => {
-  acc.set(icon.size, (acc.get(icon.size) ?? 0) + 1);
-  return acc;
-}, new Map<number, number>());
-const mapSizePerSize = new Map<number, number>();
-for (let [size, count] of iconCountPerSize.entries()) {
-  mapSizePerSize.set(size, Math.floor(Math.sqrt(count)) + 1);
-}
-
-function Icon({ style, className, iconName, size = 64 }: IconProps) {
-  const icon = useMemo(() => iconMap.find((o) => o.name == iconName), [iconName]);
-  const backgroundMap = `/data/2.0/icon_${icon?.size}.webp`;
-  const bgsize = useMemo(() => {
-    if (icon) return (mapSizePerSize.get(icon.size) ?? 1) * size;
-    return size;
-  }, [icon]);
-  return (
-    <i
-      className={className}
-      style={{
-        ...style,
-        backgroundImage: `url("${backgroundMap}")`,
-        backgroundPositionX: (-(icon?.x ?? 0) * size) / (icon?.size ?? 1),
-        backgroundPositionY: (-(icon?.y ?? 0) * size) / (icon?.size ?? 1),
-        backgroundSize: `${bgsize}px`,
-        height: size,
-        width: size,
-        display: "inline-block",
-      }}
-    ></i>
-  );
-}
-export default Icon;
+import { useMemo, type CSSProperties } from "react";
+import iconMap from "./data/2.0/iconMap.json";
+import data from "./data/2.0/data.json";
+import styles from "./Icon.module.css";
+import type { Quality } from "../../scripts/factorio-dump/process-data.models";
+
+const qualityLevels = data.qualityLevels as Quality[];
+type IconProps = {
+  style?: CSSProperties;
+  className?: string;
+  iconName: string;
+  size?: number;
+  qualityLevel?: number;
+};
+
+const iconCountPerSize = iconMap.reduce((acc, icon) => {
+  acc.set(icon.size, (acc.get(icon.size) ?? 0) + 1);
+  return acc;
+}, new Map<number, number>());
+
+const mapSizePerSize = new Map<number, number>();
+for (let [size, count] of iconCountPerSize.entries()) {
+  mapSizePerSize.set(size, Math.floor(Math.sqrt(count)) + 1);
+}
+
+function Icon({ style, className, iconName, size = 64, qualityLevel = 0 }: IconProps) {
+  const icon = useMemo(() => iconMap.find((o) => o.name == iconName), [iconName]);
+  const iconStyle = useMemo(() => {
+    if (icon) {
+      const bgSizePx = (mapSizePerSize.get(icon.size) ?? 1) * size;
+      const offsetX = (-(icon.x ?? 0) * size) / (icon.size ?? 1);
+      const offsetY = (-(icon.y ?? 0) * size) / (icon.size ?? 1);
+      return {
+        backgroundImage: `url("/data/2.0/icon_${icon?.size}.webp")`,
+        backgroundPosition: `${offsetX}px ${offsetY}px`,
+        backgroundSize: `${bgSizePx}px`,
+        width: size,
+        height: size,
+        ...style,
+      };
+    }
+  }, [icon, size, style]);
+  const qualityIcon = useMemo(() => qualityLevels.find((q) => q.level == qualityLevel)?.icon, [qualityLevel]);
+  return (
+    <i className={styles.factorioIcon + " " + className} style={iconStyle}>
+      {qualityLevel > 0 && qualityIcon != undefined && <Icon iconName={qualityIcon} size={size / 2}></Icon>}
+    </i>
+  );
+}
+export default Icon;

+ 78 - 0
src/assets/theme.ts

@@ -0,0 +1,78 @@
+import { createTheme } from "@mui/material/styles";
+
+export const factorioColors = {
+  bgDarkest: "#1a1a1a",
+  bgPanel: "#242324",
+  bgPanelLight: "#313031",
+  border: "#646464",
+  textCream: "#ffe6c0",
+  amber: "#f1be64",
+  amberHover: "#e39827",
+  amberGlow: "#f9b44b",
+  green: "#5eb663",
+  greenHover: "#92e897",
+  red: "#fe5a5a",
+  redHover: "#ff9b9b",
+  grey: "#8e8e8e",
+};
+
+export const theme = createTheme({
+  palette: {
+    mode: "dark",
+    primary: { main: factorioColors.amber, contrastText: "#1a1300" },
+    secondary: { main: factorioColors.green, contrastText: "#0a1f0b" },
+    error: { main: factorioColors.red, contrastText: "#2a0a0a" },
+    background: { default: factorioColors.bgPanel, paper: factorioColors.bgPanelLight },
+    text: { primary: factorioColors.textCream, secondary: "#999999" },
+    divider: factorioColors.border,
+  },
+  shape: { borderRadius: 4 },
+  typography: {
+    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+    button: { textTransform: "none", fontWeight: 600 },
+  },
+  components: {
+    MuiCssBaseline: {
+      styleOverrides: {
+        body: { backgroundColor: factorioColors.bgPanel },
+      },
+    },
+    MuiAppBar: {
+      styleOverrides: {
+        root: {
+          backgroundColor: factorioColors.bgDarkest,
+          borderBottom: `1px solid ${factorioColors.border}`,
+          boxShadow: "none",
+        },
+      },
+    },
+    MuiTabs: {
+      styleOverrides: {
+        indicator: { backgroundColor: factorioColors.amber, height: 3 },
+      },
+    },
+    MuiTab: {
+      styleOverrides: {
+        root: {
+          color: "#999999",
+          fontWeight: 600,
+          minHeight: 48,
+          "&.Mui-selected": { color: factorioColors.amber },
+        },
+      },
+    },
+    MuiButton: {
+      styleOverrides: {
+        root: { borderRadius: 4 },
+        containedPrimary: {
+          backgroundColor: factorioColors.amber,
+          color: "#1a1300",
+          "&:hover": { backgroundColor: factorioColors.amberHover },
+        },
+      },
+    },
+    MuiPopper: {
+      defaultProps: { style: { zIndex: 1300 } },
+    },
+  },
+});

+ 139 - 0
src/assets/types.ts

@@ -0,0 +1,139 @@
+// Shared types for the combinator clock timeline editor.
+
+import type { Signal } from "./SelectSignal";
+
+export type ActionPreset = {
+  id: string;
+  label: string;
+  ticks: number;
+  fromItem: string | null; // item name for icon, or null for "custom"
+  toItem: string | null;
+};
+
+export const ACTION_PRESETS: ActionPreset[] = [
+  { id: "belt_to_chest", label: "Belt → Chest", ticks: 11, fromItem: "turbo-transport-belt", toItem: "steel-chest" },
+  { id: "chest_to_belt", label: "Chest → Belt", ticks: 12, fromItem: "steel-chest", toItem: "turbo-transport-belt" },
+  { id: "chest_to_chest", label: "Chest → Chest", ticks: 8, fromItem: "steel-chest", toItem: "steel-chest" },
+  { id: "chest_to_splitter", label: "Chest → Splitter", ticks: 11, fromItem: "steel-chest", toItem: "turbo-splitter" },
+  { id: "custom", label: "Custom", ticks: 10, fromItem: null, toItem: null },
+];
+
+export function getPreset(id: string): ActionPreset {
+  return ACTION_PRESETS.find((p) => p.id === id) ?? ACTION_PRESETS[ACTION_PRESETS.length - 1];
+}
+
+/** A single activation Clock, placed freely on the timeline. */
+export type ClockBlock = {
+  id: string;
+  rowId: string;
+  presetId: string; // one of ACTION_PRESETS, or "custom"
+  start: number; // tick where the enable window opens (>= start)
+  duration: number; // ticks the window stays open (< start + duration)
+  count: number; // items moved per activation — independent from any "stack size"
+  label?: string; // optional override, otherwise derived from preset
+  repeat?: number; //  number of back-to-back repetitions, spaced by `duration`
+};
+
+/** A lane on the timeline — one activation signal, one or more blocks.
+ * Should correspond to 1 combinator with multiple rules
+ */
+export type ClockRow = {
+  id: string;
+  name: string;
+  signals: Signal[];
+  stackSize: number; // items per inserter grab
+  inserterCount: number; // parallel inserters running this row's pattern
+};
+
+/** One repeat instance of a block — shared by the timeline (visual) and the
+ * blueprint builder (logic) so the two never disagree about where repeats land. */
+export type ExpandedInstance = {
+  id: string; // `${block.id}#${i}`
+  blockId: string;
+  start: number;
+  duration: number;
+};
+
+export function expandBlockInstances(block: ClockBlock): ExpandedInstance[] {
+  const repeat = Math.max(1, block.repeat ?? 1);
+  return Array.from({ length: repeat }, (_, i) => ({
+    id: `${block.id}#${i}`,
+    blockId: block.id,
+    start: block.start + i * block.duration,
+    duration: block.duration,
+  }));
+}
+
+export function blockDescription(block: ClockBlock, index: number): string {
+  const preset = getPreset(block.presetId);
+  const custom = block.label;
+  const itemLine =
+    preset.fromItem && preset.toItem
+      ? `\n- ${block.count} [item=${preset.fromItem}] to [item=${preset.toItem}]`
+      : custom
+        ? `\n- ${custom}`
+        : "";
+  return `Activation ${index + 1}${itemLine}`;
+}
+
+type Entity = {
+  entity_number: number;
+  name: string;
+  position: {
+    x: number;
+    y: number;
+  };
+  direction: 1 | 2 | 3 | 4;
+};
+
+export type BlueprintSignal = {
+  type?: string;
+  name: string;
+  quality?: string;
+};
+type NetworkState = {
+  red: boolean;
+  green: boolean;
+};
+export type DeciderCondition = {
+  first_signal: BlueprintSignal;
+  comparator: "<" | ">" | "=" | "≥" | "≤" | "≠";
+  compare_type?: "and" | "or";
+  first_signal_networks?: NetworkState;
+} & ({ second_signal: BlueprintSignal; second_signal_networks?: NetworkState } | { constant?: number });
+
+type DeciderOutput = {
+  signal: BlueprintSignal;
+} & ({ copy_count_from_input: false; constant?: number } | { networks?: { red: boolean; green: boolean } });
+
+export type DeciderCombinator = Entity & {
+  name: "decider-combinator";
+  control_behavior: {
+    decider_conditions: {
+      conditions?: DeciderCondition[];
+      outputs?: DeciderOutput[];
+      else_outputs?: DeciderOutput[];
+    };
+  };
+  player_description: string;
+};
+export type Blueprint = {
+  blueprint: {
+    item: "blueprint";
+    version: 562954249109505;
+    icons: Array<{ signal: BlueprintSignal; index: 1 | 2 | 3 | 4 }>;
+    entities: [];
+    /**Each wires is represented by 4 number
+     * [a, b, c, d]
+     * a : entity_number of first entity
+     * b : wire connector (1-4) of first entity
+     * a : entity_number of second entity
+     * b : wire connector (1-4) of second entity
+     * Most entity have only 2 connector (red and green circuit)
+     * Decider Combinator, Arithmetic Combinator and Selector Combinator have 4 connectors (2 inputs, 2 outputs)
+     * 1 & 3 red
+     * 2 & 4 green
+     */
+    wires: Array<Array<number>>;
+  };
+};

+ 141 - 0
src/blueprint/Blueprintbuilder.ts

@@ -0,0 +1,141 @@
+import type { Signal } from "../assets/SelectSignal";
+import {
+  expandBlockInstances,
+  type BlueprintSignal,
+  type ClockBlock,
+  type ClockRow,
+  type DeciderCombinator,
+  type DeciderCondition,
+} from "../assets/types";
+
+const BP_VERSION = 562954249109505; // version stamp reused from a real 2.0 export
+
+function sig(type: string, name: string): BlueprintSignal {
+  return type === "item" ? { name } : type === "virtual-signal" ? { type: "virtual", name } : { type, name };
+}
+
+export type ClockConfig = {
+  duration: number; // full cycle length in ticks
+  clockSignal: Signal;
+  rows: ClockRow[];
+  blocks: ClockBlock[];
+};
+
+type Instance = { start: number; duration: number; count: number; presetId: string };
+
+/** Merge instances that are contiguous in time AND share the same output
+ * count — those two conditions are exactly what let them collapse into one
+ * combinator's [start, end) window without changing behavior. Different
+ * counts can't merge because each merged run only gets one output constant. */
+function mergeAdjacent(instances: Instance[]): Instance[][] {
+  const sorted = [...instances].sort((a, b) => a.start - b.start);
+  const runs: Instance[][] = [];
+  for (const inst of sorted) {
+    const run = runs[runs.length - 1];
+    const last = run?.[run.length - 1];
+    if (last && last.start + last.duration === inst.start && last.count === inst.count) {
+      run.push(inst);
+    } else {
+      runs.push([inst]);
+    }
+  }
+  return runs;
+}
+
+export function extractConfig(blueprint: { blueprint: { entities: DeciderCombinator[] } }): ClockConfig | null {
+  const master = blueprint.blueprint.entities.find((e) => e.player_description?.startsWith("Master Clock"));
+  if (!master) return null;
+  try {
+    return JSON.parse(master.player_description.split("\n").slice(1).join("\n"));
+  } catch {
+    return null;
+  }
+}
+/**
+ * Builds the blueprint object. Every row block becomes its own decider
+ * combinator with a two-condition AND window:
+ *   clock >= start   AND   clock < start + duration
+ * This is what lets a block sit anywhere on the timeline instead of only
+ * counting down from tick 0. NOTE: the "compare_type": "and" field on the
+ * second condition is the 2.0 schema for chaining conditions as far as I
+ * know it — verify against a live export before relying on it, since I
+ * don't have a fresh in-game sample showing a multi-condition combinator.
+ */
+export function buildBlueprint(config: ClockConfig) {
+  const clockSig = sig(config.clockSignal.type, config.clockSignal.name);
+  const entities: DeciderCombinator[] = [];
+  let id = 1;
+
+  entities.push({
+    entity_number: id,
+    name: "decider-combinator",
+    position: { x: 0, y: 0 },
+    direction: 4,
+    control_behavior: {
+      decider_conditions: {
+        conditions: [{ first_signal: clockSig, constant: config.duration - 1, comparator: "<" }],
+        outputs: [{ signal: clockSig, copy_count_from_input: false }, { signal: clockSig }],
+        else_outputs: [],
+      },
+    },
+    player_description: `Master Clock`,
+  });
+  id++;
+
+  // Group blocks by row so numbering ("Activation 1", "Activation 2"...)
+  // follows each row's own timeline order, left to right.
+  for (const row of config.rows) {
+    const rowBlocks = config.blocks.filter((b) => b.rowId === row.id);
+    const instances: Instance[] = rowBlocks.flatMap((block) =>
+      expandBlockInstances(block).map((inst) => ({
+        start: inst.start,
+        duration: inst.duration,
+        count: block.count,
+        presetId: block.presetId,
+      })),
+    );
+    const runs = mergeAdjacent(instances);
+    runs.forEach((run, index) => {
+      const start = run[0].start;
+      const end = run[run.length - 1].start + run[run.length - 1].duration;
+      const conditions: DeciderCondition[] = [
+        { first_signal: clockSig, constant: start, comparator: "≥" },
+        { first_signal: clockSig, constant: end, comparator: "<", compare_type: "and" },
+      ];
+      entities.push({
+        entity_number: id,
+        name: "decider-combinator",
+        position: { x: 0, y: id - 1 },
+        direction: 4,
+        control_behavior: {
+          decider_conditions: {
+            conditions,
+            outputs: row.signals.map((s) => ({
+              signal: sig(s.type, s.name),
+              copy_count_from_input: false,
+              constant: run[0].count,
+            })),
+            else_outputs: [],
+          },
+        },
+        player_description: `Activation ${index + 1}\n` + run.map((r) => `- ${r.count}x ${r.presetId}`).join("\n"),
+      });
+      id++;
+    });
+  }
+
+  const wires: number[][] = [[1, 2, 1, 4]];
+  for (let i = 1; i < entities.length; i++) {
+    wires.push([entities[i - 1].entity_number, 2, entities[i].entity_number, 2]);
+  }
+
+  return {
+    blueprint: {
+      icons: [{ signal: { name: "decider-combinator" }, index: 1 }],
+      entities,
+      wires,
+      item: "blueprint",
+      version: BP_VERSION,
+    },
+  };
+}

+ 38 - 0
src/blueprint/parser.ts

@@ -0,0 +1,38 @@
+import pako from "pako";
+
+/**
+ * Encode a blueprint file to a Factorio-compatible string (Browser version).
+ * Uses pako for compression, compatible with browser environments.
+ */
+export function encodeBlueprintFileBrowser(blueprint: any): string {
+  const jsonString = JSON.stringify(blueprint);
+
+  const deflated = pako.deflate(new TextEncoder().encode(jsonString));
+
+  // Convert Uint8Array to base64
+  const base64 = btoa(String.fromCharCode(...deflated));
+
+  return "0" + base64;
+}
+
+/**
+ * Decode a Factorio blueprint string to a blueprint file (Browser version).
+ * Uses pako for decompression, compatible with browser environments.
+ */
+export function decodeBlueprintFileBrowser(blueprintString: string): any {
+  // Remove the version prefix ("0")
+  const base64 = blueprintString.slice(1);
+
+  // Decode base64 to Uint8Array
+  const binaryString = atob(base64);
+  const bytes = new Uint8Array(binaryString.length);
+  for (let i = 0; i < binaryString.length; i++) {
+    bytes[i] = binaryString.charCodeAt(i);
+  }
+
+  // Inflate and decode
+  const inflated = pako.inflate(bytes);
+  const jsonString = new TextDecoder().decode(inflated);
+
+  return JSON.parse(jsonString);
+}

+ 610 - 21
src/index.css

@@ -1,21 +1,610 @@
-:root {
-  font-family: "Titillium Web", sans-serif;
-  line-height: 1.5;
-  font-weight: 400;
-
-  color-scheme: light dark;
-  color: rgba(255, 255, 255, 0.87);
-}
-
-body {
-  margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
-}
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
+.panel-button {
+  border: none;
+  cursor: pointer;
+  position: relative;
+  background-color: #313031;
+  height: 24px;
+  aspect-ratio: 1;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px 8px 4px -8px #fff,
+    inset 0px -8px 2px -8px #432400,
+    0px 0px 4px 0px #000;
+}
+.panel-button:active,
+.panel-button.active {
+  box-shadow:
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px -9px 2px -8px #fff,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+  filter: none;
+  outline: 0;
+}
+
+.panel-button.hover,
+.panel-button:focus,
+.panel-button:hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px -8px 4px -8px hsl(36, 94%, 20%),
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+input:focus {
+  border: none;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.button {
+  background-color: #8e8e8e;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #e3e3e3,
+    inset 0px 10px 2px -8px #282828,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: left;
+}
+.button.icon {
+  height: fit-content;
+  min-width: auto;
+  padding: 0;
+  margin: 0;
+}
+.flex-column .button {
+  margin-right: 0;
+}
+.button i {
+  margin-right: 0px;
+}
+.button:hover,
+.button.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.button:active,
+.button.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px 9px 2px -8px #563a10,
+    inset 0px -9px 2px -8px #563a10,
+    inset 0px -8.5px 0px -8px #563a10,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+  filter: none;
+  outline: 0;
+}
+.button.disabled,
+.button.disabled:hover,
+.button.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #3d3d3d;
+  color: #818181;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #818181,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+.button-green {
+  background-color: #5eb663;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #95df99,
+    inset 0px 10px 2px -8px #163218,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: left;
+}
+.flex-column .button-green {
+  margin-right: 0;
+}
+.button-green i {
+  margin-right: 0px;
+}
+.button-green:hover,
+.button-green:focus,
+.button-green.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #cdf1cf,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #34be3c;
+  background-color: #92e897;
+  filter: drop-shadow(0 0 2px #34be3c);
+}
+.button-green:active,
+.button-green.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #3f5024,
+    inset 8px 0px 4px -8px #3f5024,
+    inset -8px 0px 4px -8px #3f5024,
+    inset -8px 0px 4px -8px #3f5024,
+    inset 0px 9px 2px -8px #3f5024,
+    inset 0px -9px 2px -8px #3f5024,
+    inset 0px -8.5px 0px -8px #3f5024,
+    0px 0px 4px 0px #000;
+  background-color: #cfdf93;
+  filter: none;
+  outline: 0;
+}
+.button-green.disabled,
+.button-green.disabled:hover,
+.button-green.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #002b02;
+  color: #376d3b;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #376d3b,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+.button-green-right {
+  background-color: #5eb663;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #95df99,
+    inset 0px 10px 2px -8px #163218,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: right;
+}
+.button-green-right::after {
+  content: "";
+  position: absolute;
+  height: 100%;
+  width: 24px;
+  right: -10px;
+  top: 0;
+  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAABACAMAAADCiszgAAABDlBMVEUAAAASMBURLxJJgUkAKxNKiU1Ih01msGpMjVFNjlFLik9ToFdLik9Lik9Lik5Yq1wIHgoOKhGAyoVNjVFipmY3cDpQkVVrsHB2vHlXmFyK1Y5/yoRvuHNcs2E7eT8bQB1doGFAe0IJIQxNjFJDgkhjrGgGHgxetmNcs2FasF9RllVVpllXqlxRnlVQm1RYrV0cQh5SoVcUNRdQk1VLllA8e0AXOhpQkFM0bDcqXC4jTiVRmFVFiUkIHgpTpFhNmVJEh0hNj1EtYTCI1I16w35utnJXn1tNk1FJkk1HjUs/fkI3czsyajYpWSsZPhxrsW9ZnV1XmVo6dz41bzgmUygkUCYiTSUTMRUPKxEOKRFdNbmJAAAAJ3RSTlMAVisODG41Bvb75Zfc1dGXlwb98/7y8fDS0JeXl5eXl25sVjU1MSucyRWzAAABMElEQVQ4y23M51LCUBiE4QWRKFFB7L37EUJTithI6FXs7f5vxDgok3Oy+/OdZxbGdrOZ3DyDOmMn2WptHM5odbfdflzdOtGq2ek8Pa/Pqtg0nVK3eLUUVWrcvsil08UVFaeszG/WsKQsT+tYJJW3s7caFpl8FOf8WKZ40YeFYqFYKBaKhWLx44V/LBQLxaLg+T8sFAvFouFYdFIZFoqFYqFYArjuYQngeuwUEsB19wii4lqu2x+s6dXO9QavDa1atuMOG2+Bh1658VFQq5XJutX3cQjaQal8XbgMQ6N31ZtKKAJKQSkoBaWgFJSCUlAKSkEpKAWloBSUglJQCkpBKSgFpdiz8rUARTxec9yRRmGYTv/lQaMw9ocjL6oUywfjyufXdxhqTdx7O46QmjhX4w9Fq5Q/daB+bwAAAABJRU5ErkJggg==");
+  background-repeat: no-repeat;
+  background-position: 100% 0%;
+  background-size: contain;
+}
+.button-green-right i {
+  margin-right: 0px;
+}
+.button-green-right:hover,
+.button-green-right:focus,
+.button-green-right.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  filter: brightness(1.3) drop-shadow(0 0 2px #34be3c);
+}
+.button-green-right:active,
+.button-green-right.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #3f5024,
+    inset 8px 0px 4px -8px #3f5024,
+    inset 0px 9px 2px -8px #3f5024,
+    inset 0px -9px 2px -8px #3f5024,
+    inset 0px -8.5px 0px -8px #3f5024,
+    0px 0px 4px 0px #000;
+  background-color: #cfdf93;
+  filter: none;
+  outline: 0;
+}
+.button-green-right:active::after,
+.button-green-right.active::after {
+  content: "";
+  position: absolute;
+  height: 100%;
+  width: 24px;
+  right: -10px;
+  top: 0;
+  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAABACAMAAADCiszgAAACBFBMVEUAAACWq1clPgwnPRChwmOhv18iOQ4iOhEdLwwhNBAkOA8jOQ4iNBAkPBSJolSUs1t1ij4kNA4MHwAhLwwhMw0hMQ+Fn0+Jp16Xt14aLA0kOxQlPBR5jkAnOBMqOhYVJQYOIgMcLgsLHQIoPRgZKQoiOBEkOxF4jj6SsFkLGwMlORdOYCRleS4/UB9ohU8xRR3P35PD5ovO3pHM3ZDK243A1IXG2Iq4zX7N4JLM4ZHL4pDJ4o+ev2nH44/F5I6guGoxRxitw3WBmlNxiUZOZS294YW12H0+VCKqzXOcvGWSqmBedjmevmSRqV9geDoQIQIJGgLJ346v0nquxHUmMSMEEQTF4Y3B44mtzom+3IegwoSVt3+etWlwj2KKpld+llJ/mVFuhUQ6TiLH2oq704nA24e21Ya73oW73oOz1X210n2oy3Klxm6lvW6Lq22YuWWaumSUs2KYt2CQpl6Rr1yLqFuNplqBnFF0jEN2kEJeci9MYi1LYCtBVCU9UyItQBguQxccKBLK3Y3C2onD1YfC2IWrzYS22YC6z32bvny1ynu10HigwnaoyHOpx22brmmjv2WWtWGMq2GPr196lV2AnFprh1iSrlZ4k1ZuilaLp1F4kEpyikhmfUF1jEBdcD5acjdUZzdtgjZacDNNYi5HWy0zQyg4TCYoNx4pOxsmMRogMA46/ea2AAAAMHRSTlMADhT7YTLolBhR/O2XTTk1Ev779vKcbWxlVk5MPv76+Ovpp6CclpRzZ15aVU5JRxpOaUmtAAAB4klEQVQ4y23Q5VuTcRTG8VtRBKQEAxS7++BgM6YOwYBJbOo6AQnp7rK7u5v6J3keHsbO2fW7X36vz4tzHVRryzmIhNVoy9mVArkb2mp25ENuZm5qqnr7ngT849e/6dn5namyvugJf/trS0tfKWqxpa0/OjqZnCFq0d3HgbB9PGmfwCeKiksqS8tsyVt4JZPZcv6C/XLSGo5JiUmJKY4zWFViUmJSYlrCFRpeu5lXA1cxTEpMAu9dwiRxarzGcezPtIyvVXwts603MAk8npaeYlSGS0cmDUwcR+y16zbomDiujI5O5ObrleNA+KeBieOSnujIRO4hrTJcZwlEqsa27j6uV3HGlz+1246CeK572NY/NDx2WNbb9568/Px9+Iio5Lh56/mHod8Fop7UamtocH+2qKcc3mbrwGABwOlpl9fZFdmYDXB6xtX4rG8gEwCnZ90PrCGdgtNzbmdXSKfgtLCptffACr1yWtjc0ZsFfZxebHmVt0jB6SVrt0HB6ZXOVQaFoOWrYYzT8hgFp1djFEoKJYWSQkmhpFBSKCmUFCaTw+VuaukUFGbzHW+js6NbUNTXNzQ4rW/yBIXH47n/9HVfFsR8Pt+j9nebJIXf729/+zETcsFg8P2nRIrr2v4fg9wCPnjQTyTwM1IAAAAASUVORK5CYII=");
+  background-repeat: no-repeat;
+  background-position: 100% 0%;
+  background-size: contain;
+}
+.button-green-right.disabled,
+.button-green-right.disabled:hover,
+.button-green-right.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #002b02;
+  color: #376d3b;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #376d3b,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+.button-red {
+  background-color: #fe5a5a;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #fda1a1,
+    inset 0px 10px 2px -8px #8b0101,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: left;
+}
+.flex-column .button-red {
+  margin-right: 0;
+}
+.button-red i {
+  margin-right: 0px;
+}
+.button-red:hover,
+.button-red:focus,
+.button-red.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #f8eaea,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #c35353;
+  background-color: #ff9b9b;
+  filter: drop-shadow(0 0 2px #c35353);
+}
+.button-red:active,
+.button-red.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #642323,
+    inset 8px 0px 4px -8px #642323,
+    inset -8px 0px 4px -8px #642323,
+    inset -8px 0px 4px -8px #642323,
+    inset 0px 9px 2px -8px #642323,
+    inset 0px -9px 2px -8px #642323,
+    inset 0px -8.5px 0px -8px #642323,
+    0px 0px 4px 0px #000;
+  background-color: #fca8a8;
+  filter: none;
+  outline: 0;
+}
+.button-red.disabled,
+.button-red.disabled:hover,
+.button-red.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #571f1f;
+  color: #8c4e4e;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #8c4e4e,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+input[type="text"],
+input[type="password"],
+input[type="email"],
+input[type="url"],
+input[type="search"],
+textarea {
+  vertical-align: baseline;
+  font-family: inherit;
+  line-height: 1.2;
+  font-size: 105%;
+  height: 36px;
+  max-width: 100%;
+  background: #8e8e8e;
+  border-radius: 4px;
+  padding: 6px;
+  border: none;
+  box-shadow:
+    inset 0px 4px 1px -2px #000,
+    inset 0px -4px 1px -2px #c5c5c5,
+    inset 2px 0px 1px 0px #5f5f5f,
+    inset -2px 0px 1px 0px #5f5f5f,
+    inset 0px -2px 2px 0px #5f5f5f,
+    0px 0px 4px 1px #2e2521;
+}
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus,
+input[type="search"]:focus,
+textarea:focus {
+  outline: none;
+  color: #000;
+  background: #f0dab4;
+  box-shadow:
+    inset 0px 4px 2px -2px #000,
+    inset 0px -1px 1px 0px #74624b,
+    inset 0px -4px 2px -2px #e0e0e0,
+    inset 2px 0px 2px 0px #a6885c,
+    inset -2px 0px 2px 0px #a6885c,
+    0px 0px 4px 1px #2e2521;
+}
+textarea {
+  width: 100%;
+  height: 8.1em;
+}
+
+.checkbox-label {
+  margin-top: 8px;
+  margin-bottom: 8px;
+  display: flex;
+  align-content: center;
+  position: relative;
+  cursor: pointer;
+  line-height: 1.2;
+  break-inside: avoid;
+}
+.checkbox-label > div {
+  width: 100%;
+  padding-left: 8px;
+}
+.checkbox-label input {
+  position: relative;
+  top: -1px;
+  left: 0px;
+  width: 16px;
+  height: 15px;
+  padding: 6px;
+  position: absolute;
+  opacity: 0;
+  cursor: pointer;
+}
+.checkbox-label .checkbox {
+  margin: auto;
+  position: relative;
+  top: -1px;
+  left: 0px;
+  width: 16px;
+  height: 15px;
+  padding: 6px;
+  background: #8e8e8e;
+  box-shadow:
+    inset 0px 4px 2.5px -2.5px #111,
+    inset 2px 0px 2px 0px #323232,
+    inset -2px 0px 2px 0px #323232,
+    inset 0px -1px 1px 0px #fff,
+    inset 0px -4px 2px -2px #8e8e8e,
+    0px 0px 4px 1px #2e2521;
+}
+.checkbox-label .checkbox:hover,
+.checkbox-label .checkbox.hover,
+.checkbox-label .checkbox:active,
+.checkbox-label .checkbox.active,
+.checkbox-label input:focus ~ .checkbox {
+  background: #e39827;
+  box-shadow:
+    inset 0px 4px 2px -2px #412a07,
+    inset 2px 0px 2px 0px #5a3c10,
+    inset -2px 0px 2px 0px #5a3c10,
+    inset 0px -2px 2px 0px #d0ae79,
+    inset 0px -2px 4px 0px #c78627,
+    0px 0px 4px 1px #786b4f;
+}
+.checkbox-label .checkbox:active,
+.checkbox-label .checkbox.active {
+  background: #f3c98e;
+  box-shadow:
+    inset 0px 4px 2px -2px #412a07,
+    inset 2px 0px 2px 0px #5a3c10,
+    inset -2px 0px 2px 0px #5a3c10,
+    inset 0px -2px 2px 0px #d0ae79,
+    inset 0px -2px 4px 0px #c78627,
+    inset 0px 1px 2.5px 4.5px #e39827,
+    0px 0px 4px 1px #786b4f;
+}
+.checkbox-label .checkbox:disabled,
+.checkbox-label .checkbox.disabled {
+  background: #313031;
+  box-shadow:
+    inset 0px 4px 2.5px -2.5px #111,
+    inset 2px 0px 2px 0px #323232,
+    inset -2px 0px 2px 0px #323232,
+    inset 0px -1px 1px 0px #666,
+    inset 0px -4px 2px -2px #8e8e8e,
+    0px 0px 4px 1px #2e2521;
+}
+.checkbox-label input:checked ~ .checkbox {
+  background: #e39827;
+}
+.checkbox-label input:checked ~ .checkbox:active,
+.checkbox-label input:checked ~ .checkbox.active {
+  background: #f3c98e;
+}
+.checkbox-label .checkbox::after {
+  position: absolute;
+  content: "";
+}
+.checkbox-label input:checked ~ .checkbox::after {
+  -webkit-transform: rotate(45deg) scale(1);
+  -ms-transform: rotate(45deg) scale(1);
+  transform: rotate(45deg) scale(1);
+  opacity: 1;
+  left: 6px;
+  top: 3px;
+  width: 2px;
+  height: 6px;
+  border: solid #282728;
+  border-width: 0 2.5px 2.5px 0;
+  background-color: transparent;
+  border-radius: 0;
+}
+.disable-checkbox-label input[type="checkbox"] {
+  position: relative;
+  opacity: 1;
+  -webkit-appearance: none;
+  appearance: none;
+  margin: 0;
+  margin-top: 2px;
+  width: 1.2em;
+  height: 1.2em;
+  cursor: pointer;
+  background-color: transparent;
+  background-repeat: no-repeat;
+  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNhNmE2YTZ9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
+}
+.disable-checkbox-label input[type="checkbox"]:hover {
+  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNmZmI1MzN9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
+}
+.disable-checkbox-label input[type="checkbox"]:checked {
+  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNmZmEyMDB9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
+}
+
+.simplebar-scrollbar::before {
+  background-color: #e39827;
+}

+ 24 - 10
src/main.tsx

@@ -1,10 +1,24 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-
-createRoot(document.getElementById('root')!).render(
-  <StrictMode>
-    <App />
-  </StrictMode>,
-)
+import ReactDOM from "react-dom/client";
+import { BrowserRouter, Route, Routes } from "react-router";
+import { ThemeProvider, CssBaseline } from "@mui/material";
+import { theme } from "./assets/theme";
+import "./index.css";
+import ComponentTests from "./ComponentTests.tsx";
+import Layout from "./Layout.tsx";
+import ClockBuilder from "./ClockBuilder.tsx";
+
+const root = document.getElementById("root");
+
+ReactDOM.createRoot(root as HTMLElement).render(
+  <ThemeProvider theme={theme}>
+    <CssBaseline />
+    <BrowserRouter>
+      <Routes>
+        <Route element={<Layout />}>
+          <Route index element={<ClockBuilder />} />
+          <Route path="tests" element={<ComponentTests />} />
+        </Route>
+      </Routes>
+    </BrowserRouter>
+  </ThemeProvider>,
+);

+ 30 - 30
tsconfig.app.json

@@ -1,31 +1,31 @@
-{
-    "compilerOptions": {
-        "target": "ES2022",
-        "useDefineForClassFields": true,
-        "lib": [
-            "ES2022",
-            "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,
-        "noFallthroughCasesInSwitch": true
-    },
-    "include": [
-        "src"
-    ]
+{
+    "compilerOptions": {
+        "target": "ES2022",
+        "useDefineForClassFields": true,
+        "lib": [
+            "ES2022",
+            "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,
+        "noFallthroughCasesInSwitch": true
+    },
+    "include": [
+        "src"
+    ]
 }

+ 7 - 7
tsconfig.json

@@ -1,7 +1,7 @@
-{
-  "files": [],
-  "references": [
-    { "path": "./tsconfig.app.json" },
-    { "path": "./tsconfig.node.json" }
-  ]
-}
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 29 - 20
tsconfig.node.json

@@ -1,20 +1,29 @@
-{
-  "compilerOptions": {
-    "target": "ES2022",
-    "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,
-    "noFallthroughCasesInSwitch": true
-  },
-  "include": ["scripts/*", "vite.config.ts"]
-}
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "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,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": [
+    "scripts/*",
+    "vite.config.ts",
+    "scripts/factorio-dump/factorio-api.models.d.ts",
+    "scripts/factorio-dump/factorio-api.ts",
+    "scripts/factorio-dump/factorio-dump.models.ts",
+    "scripts/factorio-dump/factorio-process-data.models.ts",
+    "scripts/factorio-dump/factorio-process-data.ts",
+    "scripts/factorio-dump/index.ts"
+  ]
+}

+ 14 - 14
vite.config.ts

@@ -1,14 +1,14 @@
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react";
-
-// https://vite.dev/config/
-export default defineConfig({
-  plugins: [
-    react({
-      babel: {
-        plugins: [["babel-plugin-react-compiler"]],
-      },
-    }),
-  ],
-  css: { modules: { localsConvention: "camelCase" } },
-});
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [
+    react({
+      babel: {
+        plugins: [["babel-plugin-react-compiler"]],
+      },
+    }),
+  ],
+  css: { modules: { localsConvention: "camelCaseOnly" } },
+});

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio