initial migration to TS

This commit is contained in:
2026-03-31 22:16:52 -07:00
parent 75167454d0
commit cef5eafa9f
61 changed files with 831 additions and 85 deletions

View File

@@ -1,11 +1,13 @@
import js from '@eslint/js'; import js from '@eslint/js';
import react from 'eslint-plugin-react'; import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks';
import tseslint from 'typescript-eslint';
export default [ export default [
js.configs.recommended, js.configs.recommended,
...tseslint.configs.recommended,
{ {
files: ['src/**/*.{js,jsx}'], files: ['src/**/*.{js,jsx,ts,tsx}'],
plugins: { plugins: {
react, react,
'react-hooks': reactHooks, 'react-hooks': reactHooks,
@@ -13,6 +15,7 @@ export default [
languageOptions: { languageOptions: {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
parser: tseslint.parser,
parserOptions: { ecmaFeatures: { jsx: true } }, parserOptions: { ecmaFeatures: { jsx: true } },
globals: { globals: {
// Browser APIs // Browser APIs
@@ -77,7 +80,10 @@ export default [
'react/react-in-jsx-scope': 'off', // not needed with React 17+ JSX transform 'react/react-in-jsx-scope': 'off', // not needed with React 17+ JSX transform
'react/prop-types': 'off', // no PropTypes in this codebase 'react/prop-types': 'off', // no PropTypes in this codebase
'react/no-unknown-property': 'off', // false positives with Three.js / custom attrs 'react/no-unknown-property': 'off', // false positives with Three.js / custom attrs
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-empty': 'off', 'no-empty': 'off',
'no-prototype-builtins': 'off', 'no-prototype-builtins': 'off',
}, },

View File

@@ -7,6 +7,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -15,11 +15,16 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.183.1",
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"c8": "^10.1.3", "c8": "^10.1.3",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^5.4.0" "vite": "^5.4.0"
}, },
"engines": { "engines": {
@@ -319,6 +324,13 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -1414,6 +1426,13 @@
"win32" "win32"
] ]
}, },
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1529,6 +1548,312 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.183.1",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@dimforge/rapier3d-compat": "~0.12.0",
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": ">=0.5.17",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~1.0.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
"integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.58.0",
"@typescript-eslint/type-utils": "8.58.0",
"@typescript-eslint/utils": "8.58.0",
"@typescript-eslint/visitor-keys": "8.58.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.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.58.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz",
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.58.0",
"@typescript-eslint/types": "8.58.0",
"@typescript-eslint/typescript-estree": "8.58.0",
"@typescript-eslint/visitor-keys": "8.58.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz",
"integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.58.0",
"@typescript-eslint/types": "^8.58.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz",
"integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.58.0",
"@typescript-eslint/visitor-keys": "8.58.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz",
"integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz",
"integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.58.0",
"@typescript-eslint/typescript-estree": "8.58.0",
"@typescript-eslint/utils": "8.58.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
"integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz",
"integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.58.0",
"@typescript-eslint/tsconfig-utils": "8.58.0",
"@typescript-eslint/types": "8.58.0",
"@typescript-eslint/visitor-keys": "8.58.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.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.1.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz",
"integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.58.0",
"@typescript-eslint/types": "8.58.0",
"@typescript-eslint/typescript-estree": "8.58.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz",
"integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.58.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -1550,6 +1875,13 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
} }
}, },
"node_modules/@webgpu/types": {
"version": "0.1.69",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xyflow/react": { "node_modules/@xyflow/react": {
"version": "12.10.1", "version": "12.10.1",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz", "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz",
@@ -2186,6 +2518,13 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-color": { "node_modules/d3-color": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
@@ -2978,6 +3317,31 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4156,6 +4520,13 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/meshoptimizer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
"dev": true,
"license": "MIT"
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.2.4", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
@@ -4495,6 +4866,19 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5262,6 +5646,36 @@
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -5353,6 +5767,44 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-eslint": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz",
"integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
"@typescript-eslint/typescript-estree": "8.58.0",
"@typescript-eslint/utils": "8.58.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@@ -24,11 +24,16 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.4", "@eslint/js": "^9.39.4",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.183.1",
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"c8": "^10.1.3", "c8": "^10.1.3",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.0",
"vite": "^5.4.0" "vite": "^5.4.0"
} }
} }

View File

@@ -8,7 +8,7 @@ import {
measureAngleDegrees, measureAngleDegrees,
moveAngleWidget, moveAngleWidget,
round3, round3,
} from './angleMeasureGeometry.js'; } from './angleMeasureGeometry';
function clamp01(value) { function clamp01(value) {
return Math.max(0, Math.min(1, Number(value) || 0)); return Math.max(0, Math.min(1, Number(value) || 0));

View File

@@ -20,7 +20,7 @@ import { hydrateWorkflowState } from './workflowHydration';
import useUndoRedo from './useUndoRedo'; import useUndoRedo from './useUndoRedo';
import { packWorkflow, unpackWorkflow } from './workflowPacking'; import { packWorkflow, unpackWorkflow } from './workflowPacking';
import { serializeWorkflowState } from './workflowSerialization'; import { serializeWorkflowState } from './workflowSerialization';
import { sortNodesForParentOrder } from './nodeHierarchy.js'; import { sortNodesForParentOrder } from './nodeHierarchy';
import { import {
buildNodeClipboardPayload, buildNodeClipboardPayload,
buildNodeClipboardPayloadForIds, buildNodeClipboardPayloadForIds,
@@ -38,8 +38,8 @@ import {
beginTrackedNodeRequest, beginTrackedNodeRequest,
isTrackedNodeRequestCurrent, isTrackedNodeRequestCurrent,
resolveLoadNodeChannelPath, resolveLoadNodeChannelPath,
} from './loadNodeOutputs.js'; } from './loadNodeOutputs';
import { buildDefaultWidgetValues } from './nodeWidgetDefaults.js'; import { buildDefaultWidgetValues } from './nodeWidgetDefaults';
import { import {
getHandleType, getHandleType,
getInputName, getInputName,
@@ -52,7 +52,7 @@ import {
outputTypeCanConnectToTarget, outputTypeCanConnectToTarget,
resolveOutputTypeForTarget, resolveOutputTypeForTarget,
checkConnectionValid, checkConnectionValid,
} from './connectionUtils.js'; } from './connectionUtils';
import { import {
getSpecTypeAndOptions, getSpecTypeAndOptions,
@@ -2624,7 +2624,7 @@ function Flow() {
let nextNodes = currentNodes; let nextNodes = currentNodes;
let changed = false; let changed = false;
let structureChanged = false; const structureChanged = false;
nextNodes = nextNodes.map((candidate) => { nextNodes = nextNodes.map((candidate) => {
const candidateId = String(candidate.id); const candidateId = String(candidate.id);

View File

@@ -18,9 +18,9 @@ import TextNoteNode from './TextNoteNode';
import { import {
getSpecTypeAndOptions, isDataSocketSpec, SOCKET_WIDGET_TYPES, TYPE_COLORS, CAT_COLORS, getSpecTypeAndOptions, isDataSocketSpec, SOCKET_WIDGET_TYPES, TYPE_COLORS, CAT_COLORS,
} from './constants'; } from './constants';
import { getGroupMinimumSize } from './groupSizing.js'; import { getGroupMinimumSize } from './groupSizing';
import { buildCombinedInputNameByWidgetName, formatUiLabel } from './nodeWidgetLayout.js'; import { buildCombinedInputNameByWidgetName, formatUiLabel } from './nodeWidgetLayout';
import { applySIPrefix, formatNumericCell, formatTableRowCell, getTableColumns, parseNumberWithUnit } from './valueFormatting.js'; import { applySIPrefix, formatNumericCell, formatTableRowCell, getTableColumns, parseNumberWithUnit } from './valueFormatting';
// ── Context (provided by App) ───────────────────────────────────────── // ── Context (provided by App) ─────────────────────────────────────────

View File

@@ -9,7 +9,7 @@ import {
parseMarkupShapes, parseMarkupShapes,
sanitizeMarkupColor, sanitizeMarkupColor,
sanitizeMarkupShape, sanitizeMarkupShape,
} from './markupShapeGeometry.js'; } from './markupShapeGeometry';
function clampFraction(value) { function clampFraction(value) {
const numeric = Number(value); const numeric = Number(value);

View File

@@ -1,31 +1,32 @@
const EXCLUDED_CANVAS_TARGETS = '.context-menu, .react-flow__node, .react-flow__edge, .react-flow__controls, .react-flow__minimap, .surface-view-container'; const EXCLUDED_CANVAS_TARGETS = '.context-menu, .react-flow__node, .react-flow__edge, .react-flow__controls, .react-flow__minimap, .surface-view-container';
const CANVAS_AREA_TARGETS = '.react-flow, .react-flow__renderer, .react-flow__viewport, .react-flow__pane, .react-flow__background, .react-flow__selectionpane'; const CANVAS_AREA_TARGETS = '.react-flow, .react-flow__renderer, .react-flow__viewport, .react-flow__pane, .react-flow__background, .react-flow__selectionpane';
function getTargetElement(target) { function getTargetElement(target: EventTarget | null): Element | null {
if (!target) return null; if (!target) return null;
if (typeof target.closest === 'function') return target; if (typeof (target as Element).closest === 'function') return target as Element;
if (target.parentElement && typeof target.parentElement.closest === 'function') { const parent = (target as Node).parentElement;
return target.parentElement; if (parent && typeof parent.closest === 'function') {
return parent;
} }
return null; return null;
} }
function supportsClosest(target) { function supportsClosest(target: EventTarget | null): boolean {
return !!getTargetElement(target); return !!getTargetElement(target);
} }
function matchesClosest(target, selector) { function matchesClosest(target: EventTarget | null, selector: string): boolean {
const element = getTargetElement(target); const element = getTargetElement(target);
return !!element && element.closest(selector) !== null; return !!element && element.closest(selector) !== null;
} }
export function isEditableInteractionTarget(target) { export function isEditableInteractionTarget(target: EventTarget | null): boolean {
if (!supportsClosest(target)) return false; if (!supportsClosest(target)) return false;
if (matchesClosest(target, 'input, textarea, select')) return true; if (matchesClosest(target, 'input, textarea, select')) return true;
return matchesClosest(target, '[contenteditable="true"]'); return matchesClosest(target, '[contenteditable="true"]');
} }
export function canStartCanvasRightDragZoomTarget(target) { export function canStartCanvasRightDragZoomTarget(target: EventTarget | null): boolean {
if (!supportsClosest(target)) return false; if (!supportsClosest(target)) return false;
if (isEditableInteractionTarget(target)) return false; if (isEditableInteractionTarget(target)) return false;
if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) { if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
@@ -34,7 +35,7 @@ export function canStartCanvasRightDragZoomTarget(target) {
return matchesClosest(target, CANVAS_AREA_TARGETS); return matchesClosest(target, CANVAS_AREA_TARGETS);
} }
export function canOpenCanvasContextMenuTarget(target) { export function canOpenCanvasContextMenuTarget(target: EventTarget | null): boolean {
if (!supportsClosest(target)) return false; if (!supportsClosest(target)) return false;
if (isEditableInteractionTarget(target)) return false; if (isEditableInteractionTarget(target)) return false;
if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) { if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
@@ -43,7 +44,7 @@ export function canOpenCanvasContextMenuTarget(target) {
return matchesClosest(target, CANVAS_AREA_TARGETS); return matchesClosest(target, CANVAS_AREA_TARGETS);
} }
export function isSecondaryCanvasContextEvent(event) { export function isSecondaryCanvasContextEvent(event: MouseEvent | null): boolean {
if (!event || typeof event.button !== 'number') return false; if (!event || typeof event.button !== 'number') return false;
return event.button === 2 || (event.button === 0 && !!event.ctrlKey); return event.button === 2 || (event.button === 0 && !!event.ctrlKey);
} }

View File

@@ -1,7 +1,7 @@
// ── Connection utility functions ─────────────────────────────────────── // ── Connection utility functions ───────────────────────────────────────
// Pure functions extracted from App.jsx so they can be independently tested. // Pure functions extracted from App.jsx so they can be independently tested.
import { socketSpecAcceptsType } from './constants.js'; import { socketSpecAcceptsType } from './constants.ts';
// ── Handle ID helpers ───────────────────────────────────────────────── // ── Handle ID helpers ─────────────────────────────────────────────────

View File

@@ -1,3 +1,5 @@
import type { InputSpec, InputOptions } from './types.ts';
// ── Shared type & color constants ───────────────────────────────────── // ── Shared type & color constants ─────────────────────────────────────
export const DATA_TYPES = new Set([ export const DATA_TYPES = new Set([
@@ -8,7 +10,7 @@ export const DATA_TYPES = new Set([
export const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']); export const SOCKET_WIDGET_TYPES = new Set(['FLOAT', 'INT']);
export const TYPE_COLORS = { export const TYPE_COLORS: Record<string, string> = {
DATA_FIELD: '#3a7abf', DATA_FIELD: '#3a7abf',
IMAGE: '#00ff08a0', IMAGE: '#00ff08a0',
LINE: '#ffbe5c', LINE: '#ffbe5c',
@@ -26,7 +28,7 @@ export const TYPE_COLORS = {
DIRECTORY: '#f97316', DIRECTORY: '#f97316',
}; };
export const CAT_COLORS = { export const CAT_COLORS: Record<string, string> = {
Input: '#37474f', Input: '#37474f',
Display: '#212121', Display: '#212121',
Overlay: '#0f766e', Overlay: '#0f766e',
@@ -39,34 +41,34 @@ export const CAT_COLORS = {
Grains: '#bf360c', Grains: '#bf360c',
}; };
export const SOCKET_COMPATIBILITY = { export const SOCKET_COMPATIBILITY: Record<string, Set<string>> = {
FLOAT: new Set(['INT']), FLOAT: new Set(['INT']),
INT: new Set(['FLOAT']), INT: new Set(['FLOAT']),
LINE: new Set(['COORDPAIR']), LINE: new Set(['COORDPAIR']),
}; };
const EMPTY_SOCKET_TYPE_SET = new Set(); const EMPTY_SOCKET_TYPE_SET: Set<string> = new Set();
export function getSpecTypeAndOptions(spec) { export function getSpecTypeAndOptions(spec: InputSpec): [string | string[], InputOptions] {
if (Array.isArray(spec)) { if (Array.isArray(spec)) {
return [spec[0], spec[1] || {}]; return [spec[0], (spec[1] || {}) as InputOptions];
} }
return [spec, {}]; return [spec, {}];
} }
export function isDataSocketType(type) { export function isDataSocketType(type: unknown): boolean {
return typeof type === 'string' && DATA_TYPES.has(type); return typeof type === 'string' && DATA_TYPES.has(type);
} }
export function isDataSocketSpec(spec) { export function isDataSocketSpec(spec: InputSpec): boolean {
const [type] = getSpecTypeAndOptions(spec); const [type] = getSpecTypeAndOptions(spec);
return isDataSocketType(type); return isDataSocketType(type);
} }
export function getAcceptedSocketTypes(specOrType) { export function getAcceptedSocketTypes(specOrType: InputSpec | string): Set<string> {
const [type, opts] = Array.isArray(specOrType) const [type, opts] = Array.isArray(specOrType)
? getSpecTypeAndOptions(specOrType) ? getSpecTypeAndOptions(specOrType as InputSpec)
: [specOrType, {}]; : [specOrType, {} as InputOptions];
if (typeof type !== 'string') { if (typeof type !== 'string') {
return EMPTY_SOCKET_TYPE_SET; return EMPTY_SOCKET_TYPE_SET;
} }
@@ -89,7 +91,7 @@ export function getAcceptedSocketTypes(specOrType) {
return accepted; return accepted;
} }
export function socketSpecAcceptsType(sourceType, targetSpecOrType) { export function socketSpecAcceptsType(sourceType: string, targetSpecOrType: InputSpec | string): boolean {
if (typeof sourceType !== 'string' || !sourceType) return false; if (typeof sourceType !== 'string' || !sourceType) return false;
return getAcceptedSocketTypes(targetSpecOrType).has(sourceType); return getAcceptedSocketTypes(targetSpecOrType).has(sourceType);
} }

View File

@@ -1,4 +1,4 @@
import { extractWorkflow } from './pngMetadata.js'; import { extractWorkflow } from './pngMetadata.ts';
const DEFAULT_WORKFLOW_CANDIDATES = [ const DEFAULT_WORKFLOW_CANDIDATES = [
{ path: '/default-workflow.json', type: 'json' }, { path: '/default-workflow.json', type: 'json' },

View File

@@ -1,4 +1,4 @@
import { getSpecTypeAndOptions, isDataSocketSpec } from './constants.js'; import { getSpecTypeAndOptions, isDataSocketSpec } from './constants.ts';
const OMITTED_WIDGET_INPUTS_BY_CLASS = { const OMITTED_WIDGET_INPUTS_BY_CLASS = {
View3D: new Set([ View3D: new Set([

View File

@@ -1,6 +1,18 @@
export const GROUP_DRAG_RELEASE_DISTANCE = 18; export const GROUP_DRAG_RELEASE_DISTANCE = 18;
export function getPointDistanceOutsideRect(rect, point) { interface Rect {
left: number;
right: number;
top: number;
bottom: number;
}
interface Point {
x: number;
y: number;
}
export function getPointDistanceOutsideRect(rect: Rect | null, point: Point | null): number {
if (!rect || !point) return Infinity; if (!rect || !point) return Infinity;
const dx = point.x < rect.left const dx = point.x < rect.left
@@ -13,6 +25,6 @@ export function getPointDistanceOutsideRect(rect, point) {
return Math.hypot(dx, dy); return Math.hypot(dx, dy);
} }
export function shouldReleaseFromGroup(rect, point, threshold = GROUP_DRAG_RELEASE_DISTANCE) { export function shouldReleaseFromGroup(rect: Rect | null, point: Point | null, threshold = GROUP_DRAG_RELEASE_DISTANCE): boolean {
return getPointDistanceOutsideRect(rect, point) >= threshold; return getPointDistanceOutsideRect(rect, point) >= threshold;
} }

View File

@@ -1,7 +1,15 @@
const DEFAULT_CHILD_WIDTH = 200; const DEFAULT_CHILD_WIDTH = 200;
const DEFAULT_CHILD_HEIGHT = 120; const DEFAULT_CHILD_HEIGHT = 120;
function getNodeSize(node, axis) { interface SizableNode {
position?: { x: number; y: number };
measured?: { width?: number; height?: number };
width?: number;
height?: number;
style?: Record<string, unknown>;
}
function getNodeSize(node: SizableNode | null | undefined, axis: 'width' | 'height'): number {
const fallback = axis === 'width' ? DEFAULT_CHILD_WIDTH : DEFAULT_CHILD_HEIGHT; const fallback = axis === 'width' ? DEFAULT_CHILD_WIDTH : DEFAULT_CHILD_HEIGHT;
const measured = Number(node?.measured?.[axis]); const measured = Number(node?.measured?.[axis]);
if (Number.isFinite(measured) && measured > 0) return measured; if (Number.isFinite(measured) && measured > 0) return measured;
@@ -12,7 +20,7 @@ function getNodeSize(node, axis) {
return fallback; return fallback;
} }
export function getGroupMinimumSize(memberNodes, { export function getGroupMinimumSize(memberNodes: SizableNode[] | null | undefined, {
minWidth = 260, minWidth = 260,
minHeight = 180, minHeight = 180,
paddingX = 24, paddingX = 24,

View File

@@ -1,9 +1,9 @@
export function resolveLoadNodeChannelPath({ export function resolveLoadNodeChannelPath({
explicitPath = null, explicitPath = null as string | null,
resolvedPathInput = null, resolvedPathInput = null as string | null,
className = '', className = '',
widgetValues = {}, widgetValues = {} as Record<string, unknown>,
} = {}) { } = {}): string {
if (typeof explicitPath === 'string' && explicitPath) { if (typeof explicitPath === 'string' && explicitPath) {
return explicitPath; return explicitPath;
} }
@@ -19,12 +19,12 @@ export function resolveLoadNodeChannelPath({
return ''; return '';
} }
export function beginTrackedNodeRequest(requestVersions, nodeId) { export function beginTrackedNodeRequest(requestVersions: Map<string, number>, nodeId: string): number {
const nextVersion = (requestVersions.get(nodeId) || 0) + 1; const nextVersion = (requestVersions.get(nodeId) || 0) + 1;
requestVersions.set(nodeId, nextVersion); requestVersions.set(nodeId, nextVersion);
return nextVersion; return nextVersion;
} }
export function isTrackedNodeRequestCurrent(requestVersions, nodeId, version) { export function isTrackedNodeRequestCurrent(requestVersions: Map<string, number>, nodeId: string, version: number): boolean {
return requestVersions.get(nodeId) === version; return requestVersions.get(nodeId) === version;
} }

View File

@@ -1,4 +1,4 @@
import { sortNodesForParentOrder } from './nodeHierarchy.js'; import { sortNodesForParentOrder } from './nodeHierarchy.ts';
export const NODE_CLIPBOARD_KIND = 'tono/node-selection'; export const NODE_CLIPBOARD_KIND = 'tono/node-selection';
export const NODE_CLIPBOARD_MIME = 'application/x-tono-node-selection'; export const NODE_CLIPBOARD_MIME = 'application/x-tono-node-selection';

View File

@@ -1,12 +1,18 @@
export function sortNodesForParentOrder(nodes) { interface NodeLike {
id: string | number;
parentId?: string | number;
[key: string]: unknown;
}
export function sortNodesForParentOrder<T extends NodeLike>(nodes: T[]): T[] {
const list = Array.isArray(nodes) ? nodes.filter(Boolean) : []; const list = Array.isArray(nodes) ? nodes.filter(Boolean) : [];
const entries = list.map((node) => ({ id: String(node.id), node })); const entries = list.map((node) => ({ id: String(node.id), node }));
const byId = new Map(entries.map((entry) => [entry.id, entry])); const byId = new Map(entries.map((entry) => [entry.id, entry]));
const visiting = new Set(); const visiting = new Set<string>();
const visited = new Set(); const visited = new Set<string>();
const ordered = []; const ordered: T[] = [];
function visit(entry) { function visit(entry: { id: string; node: T } | undefined) {
if (!entry) return; if (!entry) return;
const { id, node } = entry; const { id, node } = entry;
if (visited.has(id) || visiting.has(id)) return; if (visited.has(id) || visiting.has(id)) return;

View File

@@ -1,4 +1,4 @@
import { getSpecTypeAndOptions, isDataSocketSpec } from './constants.js'; import { getSpecTypeAndOptions, isDataSocketSpec } from './constants.ts';
export function getDefaultWidgetValue(spec) { export function getDefaultWidgetValue(spec) {
const [type, opts] = getSpecTypeAndOptions(spec); const [type, opts] = getSpecTypeAndOptions(spec);

View File

@@ -1,4 +1,4 @@
export function formatUiLabel(text) { export function formatUiLabel(text: unknown): string {
return String(text ?? '') return String(text ?? '')
.replace(/_/g, ' ') .replace(/_/g, ' ')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
@@ -6,7 +6,7 @@ export function formatUiLabel(text) {
.toLowerCase(); .toLowerCase();
} }
function normalizeInputNames(raw) { function normalizeInputNames(raw: unknown): string[] {
if (!raw) return []; if (!raw) return [];
return (Array.isArray(raw) ? raw : [raw]) return (Array.isArray(raw) ? raw : [raw])
.map((value) => String(value)) .map((value) => String(value))

View File

@@ -1,4 +1,4 @@
export function sanitizeRuntimeValuesForPersistence(className, runtimeValues) { export function sanitizeRuntimeValuesForPersistence(className: string | undefined, runtimeValues: Record<string, unknown> | undefined): Record<string, unknown> {
if (!runtimeValues || typeof runtimeValues !== 'object' || Array.isArray(runtimeValues)) { if (!runtimeValues || typeof runtimeValues !== 'object' || Array.isArray(runtimeValues)) {
return {}; return {};
} }

235
frontend/src/types.ts Normal file
View File

@@ -0,0 +1,235 @@
import type { Node, Edge } from '@xyflow/react';
import type { CSSProperties } from 'react';
// ── Input Specifications ─────────────────────────────────────────────
export interface InputOptions {
label?: string;
hidden?: boolean;
socket_only?: boolean;
accepted_types?: string[];
default?: unknown;
placeholder?: string;
min?: number;
max?: number;
step?: number;
slider?: boolean;
min_widget?: string;
max_widget?: string;
text_input?: boolean;
color_picker?: boolean;
colormap_stops?: boolean;
set_widgets?: Record<string, unknown>;
show_when_source_type?: Record<string, string[]>;
show_when_widget_value?: Record<string, unknown[]>;
show_when_input_visible?: string | string[];
hide_when_input_connected?: string | string[];
choices_by_source_type?: Record<string, string[]>;
choices_from_table_input?: string;
choices_from_measure_input?: string;
inline_with_input?: string | [string];
source_type_input?: string;
placement?: 'top';
}
/** An input spec is either a bare type string, or a [type, opts] tuple. */
export type InputSpec = string | [type: string | string[], opts?: InputOptions];
// ── Node Definition (from GET /nodes) ────────────────────────────────
export interface NodeDefinition {
input: {
required: Record<string, InputSpec>;
optional: Record<string, InputSpec>;
};
output: string[];
output_name: string[];
output_paths?: string[];
category: string;
manual_trigger?: boolean;
}
export type NodeDefsRegistry = Record<string, NodeDefinition>;
// ── Overlay Types ────────────────────────────────────────────────────
export interface OverlayData {
kind: string;
image?: string;
image_width?: number;
image_height?: number;
x1?: number;
y1?: number;
x2?: number;
y2?: number;
xm?: number;
ym?: number;
a_locked?: boolean;
b_locked?: boolean;
section_title?: string;
line?: number[];
shape?: string;
stroke_color?: string;
stroke_width?: number;
angle_deg?: number;
color?: string;
label_dx?: number;
label_dy?: number;
line_thickness?: number;
histogram?: unknown;
}
// ── Preview Image Types ──────────────────────────────────────────────
export interface PreviewPanel {
kind: 'image' | 'line_plot';
title?: string;
image?: string;
line?: number[];
fallback_image?: string;
}
export interface PreviewPayload {
kind: 'image' | 'line_plot' | 'layer_gallery' | 'panels';
image?: string;
line?: number[];
layers?: Array<{ name?: string; image: string }>;
fallback_image?: string;
panels?: PreviewPanel[];
}
export type PreviewImage = string | PreviewPayload;
// ── Node Data (attached to each ReactFlow node) ──────────────────────
export interface GroupProxy {
handleId: string;
type: string;
label: string;
name: string;
}
export interface NodeData extends Record<string, unknown> {
label: string;
className: string;
definition?: NodeDefinition | null;
widgetValues: Record<string, unknown>;
runtimeValues?: Record<string, unknown>;
// Execution results
previewImage?: PreviewImage | null;
tableRows?: Array<Record<string, unknown>> | null;
scalarValue?: number | { value: number | string; unit?: string } | null;
meshData?: unknown;
overlay?: OverlayData | null;
// Status
error?: string | null;
warning?: string | null;
processingTimeMs?: number | null;
// Group node fields
proxyInputs?: GroupProxy[];
proxyOutputs?: GroupProxy[];
childCount?: number;
collapsed?: boolean;
expandedSize?: { width: number; height: number };
// Serialization extras
extraData?: Record<string, unknown>;
output?: string[];
output_name?: string[];
}
// ── ReactFlow Node & Edge ────────────────────────────────────────────
export type TonoNode = Node<NodeData, 'custom'>;
export type TonoEdge = Edge<{
groupProxyOwner?: string;
groupProxyOriginal?: {
source?: string;
sourceHandle?: string;
target?: string;
targetHandle?: string;
};
}>;
// ── Serialized Workflow ──────────────────────────────────────────────
export interface SerializedNode {
id: string;
type?: string;
position: { x: number; y: number };
width?: number;
height?: number;
className?: string;
parentId?: string;
extent?: [[number, number], [number, number]];
hidden?: boolean;
style?: CSSProperties;
dragHandle?: string;
data: {
label: string;
className: string;
widgetValues: Record<string, unknown>;
runtimeValues?: Record<string, unknown>;
extraData?: Record<string, unknown>;
output?: string[];
output_name?: string[];
};
}
export interface SerializedEdge {
id: string;
source: string;
sourceHandle?: string;
target: string;
targetHandle?: string;
style?: CSSProperties;
hidden?: boolean;
data?: Record<string, unknown>;
}
export interface SerializedWorkflow {
version: number;
nodes: SerializedNode[];
edges: SerializedEdge[];
packed?: boolean;
packedFiles?: Record<string, { filename: string; data: string }>;
}
// ── WebSocket Messages ───────────────────────────────────────────────
export type WsMessage =
| { type: 'execution_start'; data: { prompt_id: string } }
| { type: 'executing'; data: { node: string; prompt_id: string } }
| { type: 'execution_complete'; data: { prompt_id: string } }
| { type: 'execution_error'; data: { node_id: string; message: string } }
| { type: 'preview'; data: { node_id: string; image: PreviewImage } }
| { type: 'table'; data: { node_id: string; rows: Array<Record<string, unknown>> } }
| { type: 'scalar'; data: { node_id: string; value: number | string; unit?: string } }
| { type: 'node_timing'; data: { node_id: string; elapsed_ms: number } }
| { type: 'mesh3d'; data: { node_id: string; mesh: unknown } }
| { type: 'overlay'; data: { node_id: string; overlay: OverlayData } }
| { type: 'node_warning'; data: { node_id: string; message: string } }
| { type: 'nodes_updated'; data: Record<string, never> };
// ── Widget description (used by nodeWidgetLayout) ────────────────────
export interface WidgetDescriptor {
name: string;
type: string | string[];
opts: InputOptions;
socketType?: string;
}
// ── Node Context (provided by App to CustomNode) ─────────────────────
export interface NodeContextValue {
executingNodeId: string | null;
onWidgetChange: (nodeId: string, name: string, value: unknown) => void;
openFileBrowser: (callback: (files: File[]) => void, options?: unknown) => void;
openHelp: (label: string) => void;
getTableColumns: (nodeId: string, inputName: string) => string[];
getMeasurementChoices: (nodeId: string, inputName: string) => string[];
}

View File

@@ -1,5 +1,5 @@
import { toBlob } from 'html-to-image'; import { toBlob } from 'html-to-image';
import { CANVAS_COLORS } from './constants.js'; import { CANVAS_COLORS } from './constants.ts';
import { CAPTURE_SELECTOR as linePlotSelector } from './LinePlotOverlay'; import { CAPTURE_SELECTOR as linePlotSelector } from './LinePlotOverlay';
import { CAPTURE_SELECTOR as thresholdSelector } from './ThresholdHistogram'; import { CAPTURE_SELECTOR as thresholdSelector } from './ThresholdHistogram';
import { CAPTURE_SELECTOR as csSelector } from './CrossSectionOverlay'; import { CAPTURE_SELECTOR as csSelector } from './CrossSectionOverlay';

View File

@@ -1,5 +1,5 @@
import { sortNodesForParentOrder } from './nodeHierarchy.js'; import { sortNodesForParentOrder } from './nodeHierarchy.ts';
import { sanitizeRuntimeValuesForPersistence } from './runtimeValuePersistence.js'; import { sanitizeRuntimeValuesForPersistence } from './runtimeValuePersistence.ts';
function mergeDefinition(nodeData, defs) { function mergeDefinition(nodeData, defs) {
const savedData = nodeData || {}; const savedData = nodeData || {};

View File

@@ -5,7 +5,7 @@
* portable across machines and sessions. * portable across machines and sessions.
*/ */
import * as api from './api'; import * as api from './api.ts';
const MAX_PACKED_BYTES = 100 * 1024 * 1024; // 100 MB const MAX_PACKED_BYTES = 100 * 1024 * 1024; // 100 MB

View File

@@ -1,4 +1,4 @@
import { sanitizeRuntimeValuesForPersistence } from './runtimeValuePersistence.js'; import { sanitizeRuntimeValuesForPersistence } from './runtimeValuePersistence.ts';
export function serializeWorkflowState(nodes, edges) { export function serializeWorkflowState(nodes, edges) {
const compactObject = (value) => { const compactObject = (value) => {

View File

@@ -6,7 +6,7 @@ import {
getAngleLabelPosition, getAngleLabelPosition,
measureAngleDegrees, measureAngleDegrees,
moveAngleWidget, moveAngleWidget,
} from '../src/angleMeasureGeometry.js'; } from '../src/angleMeasureGeometry.ts';
test('measureAngleDegrees returns the included angle', () => { test('measureAngleDegrees returns the included angle', () => {
assert.equal(measureAngleDegrees(0, 1, 0, 0, 1, 0), 90); assert.equal(measureAngleDegrees(0, 1, 0, 0, 1, 0), 90);

View File

@@ -6,7 +6,7 @@ import {
canStartCanvasRightDragZoomTarget, canStartCanvasRightDragZoomTarget,
isEditableInteractionTarget, isEditableInteractionTarget,
isSecondaryCanvasContextEvent, isSecondaryCanvasContextEvent,
} from '../src/canvasInteractionTargets.js'; } from '../src/canvasInteractionTargets.ts';
function makeTarget(activeSelectors = []) { function makeTarget(activeSelectors = []) {
const selectorSet = new Set(activeSelectors); const selectorSet = new Set(activeSelectors);

View File

@@ -14,7 +14,7 @@ import {
outputTypeCanConnectToTarget, outputTypeCanConnectToTarget,
resolveOutputTypeForTarget, resolveOutputTypeForTarget,
checkConnectionValid, checkConnectionValid,
} from '../src/connectionUtils.js'; } from '../src/connectionUtils.ts';
// ── Handle ID helpers ───────────────────────────────────────────────── // ── Handle ID helpers ─────────────────────────────────────────────────

View File

@@ -6,7 +6,7 @@ import {
getAcceptedSocketTypes, getAcceptedSocketTypes,
isDataSocketSpec, isDataSocketSpec,
socketSpecAcceptsType, socketSpecAcceptsType,
} from '../src/constants.js'; } from '../src/constants.ts';
test('intrinsic socket compatibility still allows INT to connect to FLOAT sockets', () => { test('intrinsic socket compatibility still allows INT to connect to FLOAT sockets', () => {
assert.equal(socketSpecAcceptsType('INT', 'FLOAT'), true); assert.equal(socketSpecAcceptsType('INT', 'FLOAT'), true);

View File

@@ -1,8 +1,8 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { embedWorkflow } from '../src/pngMetadata.js'; import { embedWorkflow } from '../src/pngMetadata.ts';
import { loadDefaultWorkflowAsset } from '../src/defaultWorkflow.js'; import { loadDefaultWorkflowAsset } from '../src/defaultWorkflow.ts';
const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aF9sAAAAASUVORK5CYII='; const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aF9sAAAAASUVORK5CYII=';

View File

@@ -5,7 +5,7 @@ import {
serializeExecutionGraph, serializeExecutionGraph,
getAutoRunnableNodes, getAutoRunnableNodes,
hasBlockingAutoRunInput, hasBlockingAutoRunInput,
} from '../src/executionGraph.js'; } from '../src/executionGraph.ts';
test('serializeExecutionGraph excludes isolated nodes from the backend prompt', () => { test('serializeExecutionGraph excludes isolated nodes from the backend prompt', () => {
const nodes = [ const nodes = [

View File

@@ -5,7 +5,7 @@ import {
GROUP_DRAG_RELEASE_DISTANCE, GROUP_DRAG_RELEASE_DISTANCE,
getPointDistanceOutsideRect, getPointDistanceOutsideRect,
shouldReleaseFromGroup, shouldReleaseFromGroup,
} from '../src/groupDrag.js'; } from '../src/groupDrag.ts';
test('getPointDistanceOutsideRect returns zero inside the rect', () => { test('getPointDistanceOutsideRect returns zero inside the rect', () => {
const rect = { left: 10, top: 20, right: 110, bottom: 120 }; const rect = { left: 10, top: 20, right: 110, bottom: 120 };

View File

@@ -1,7 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { getGroupMinimumSize } from '../src/groupSizing.js'; import { getGroupMinimumSize } from '../src/groupSizing.ts';
test('getGroupMinimumSize keeps the base minimum for empty groups', () => { test('getGroupMinimumSize keeps the base minimum for empty groups', () => {
assert.deepEqual(getGroupMinimumSize([]), { width: 260, height: 180 }); assert.deepEqual(getGroupMinimumSize([]), { width: 260, height: 180 });

View File

@@ -5,7 +5,7 @@ import {
beginTrackedNodeRequest, beginTrackedNodeRequest,
isTrackedNodeRequestCurrent, isTrackedNodeRequestCurrent,
resolveLoadNodeChannelPath, resolveLoadNodeChannelPath,
} from '../src/loadNodeOutputs.js'; } from '../src/loadNodeOutputs.ts';
test('resolveLoadNodeChannelPath can resolve a new ImageDemo node from its explicit selection before mount', () => { test('resolveLoadNodeChannelPath can resolve a new ImageDemo node from its explicit selection before mount', () => {
const resolvedPath = resolveLoadNodeChannelPath({ const resolvedPath = resolveLoadNodeChannelPath({

View File

@@ -8,7 +8,7 @@ import {
getMarkupPreviewStrokeWidth, getMarkupPreviewStrokeWidth,
sanitizeMarkupColor, sanitizeMarkupColor,
sanitizeMarkupShape, sanitizeMarkupShape,
} from '../src/markupShapeGeometry.js'; } from '../src/markupShapeGeometry.ts';
test('markup defaults use arrow and red', () => { test('markup defaults use arrow and red', () => {
assert.equal(MARKUP_DEFAULT_SHAPE, 'arrow'); assert.equal(MARKUP_DEFAULT_SHAPE, 'arrow');

View File

@@ -7,7 +7,7 @@ import {
instantiateNodeClipboardPayload, instantiateNodeClipboardPayload,
NODE_CLIPBOARD_KIND, NODE_CLIPBOARD_KIND,
parseNodeClipboardPayload, parseNodeClipboardPayload,
} from '../src/nodeClipboard.js'; } from '../src/nodeClipboard.ts';
test('buildNodeClipboardPayload keeps only selected nodes and internal edges', () => { test('buildNodeClipboardPayload keeps only selected nodes and internal edges', () => {
const nodes = [ const nodes = [

View File

@@ -1,9 +1,9 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { sortNodesForParentOrder } from '../src/nodeHierarchy.js'; import { sortNodesForParentOrder } from '../src/nodeHierarchy.ts';
import { hydrateWorkflowState } from '../src/workflowHydration.js'; import { hydrateWorkflowState } from '../src/workflowHydration.ts';
import { instantiateNodeClipboardPayload, NODE_CLIPBOARD_KIND } from '../src/nodeClipboard.js'; import { instantiateNodeClipboardPayload, NODE_CLIPBOARD_KIND } from '../src/nodeClipboard.ts';
test('sortNodesForParentOrder places parents before descendants', () => { test('sortNodesForParentOrder places parents before descendants', () => {
const nodes = [ const nodes = [

View File

@@ -1,7 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { buildDefaultWidgetValues, getDefaultWidgetValue } from '../src/nodeWidgetDefaults.js'; import { buildDefaultWidgetValues, getDefaultWidgetValue } from '../src/nodeWidgetDefaults.ts';
test('enum widget defaults honor opts.default instead of the first option', () => { test('enum widget defaults honor opts.default instead of the first option', () => {
assert.equal( assert.equal(

View File

@@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
import { import {
buildCombinedInputNameByWidgetName, buildCombinedInputNameByWidgetName,
getWidgetCombinedInputName, getWidgetCombinedInputName,
} from '../src/nodeWidgetLayout.js'; } from '../src/nodeWidgetLayout.ts';
test('getWidgetCombinedInputName pairs same-label hide_when_input_connected widgets with their matching input', () => { test('getWidgetCombinedInputName pairs same-label hide_when_input_connected widgets with their matching input', () => {
const dataInputByName = new Map([ const dataInputByName = new Map([

View File

@@ -1,7 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { embedWorkflow, extractWorkflow } from '../src/pngMetadata.js'; import { embedWorkflow, extractWorkflow } from '../src/pngMetadata.ts';
const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aF9sAAAAASUVORK5CYII='; const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aF9sAAAAASUVORK5CYII=';

View File

@@ -6,7 +6,7 @@ import {
formatDisplayUnit, formatDisplayUnit,
formatTableRowCell, formatTableRowCell,
getTableColumns, getTableColumns,
} from '../src/valueFormatting.js'; } from '../src/valueFormatting.ts';
test('getTableColumns hides companion record-table unit columns', () => { test('getTableColumns hides companion record-table unit columns', () => {
const columns = getTableColumns([ const columns = getTableColumns([

View File

@@ -1,7 +1,7 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { OVERLAY_CAPTURE_SELECTORS, captureViewportBlob } from '../src/workflowCapture.js'; import { OVERLAY_CAPTURE_SELECTORS, captureViewportBlob } from '../src/workflowCapture.ts';
function makeElement(name, { tagName = 'DIV', width = 160, height = 90, src = '' } = {}) { function makeElement(name, { tagName = 'DIV', width = 160, height = 90, src = '' } = {}) {
return { return {

View File

@@ -1,8 +1,8 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { hydrateWorkflowState } from '../src/workflowHydration.js'; import { hydrateWorkflowState } from '../src/workflowHydration.ts';
import { serializeWorkflowState } from '../src/workflowSerialization.js'; import { serializeWorkflowState } from '../src/workflowSerialization.ts';
test('serializeWorkflowState keeps only stable workflow fields needed for reload', () => { test('serializeWorkflowState keeps only stable workflow fields needed for reload', () => {
const nodes = [ const nodes = [

19
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"allowJs": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true
},
"include": ["src"]
}