Add oxlint, oxfmt, typescript-go
- oxlint with react, react-hooks, typescript, import, unicorn plugins - oxfmt (no trailing commas, single quotes, sorted imports) - tsgo (typescript-go native preview) for fast typechecking - Fix all lint warnings: non-null assertions, unused expressions, stale closures - Scripts: lint, format, typecheck, check
This commit is contained in:
parent
68b54ad6b7
commit
c123dad50c
20
.oxfmtrc.json
Normal file
20
.oxfmtrc.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always",
|
||||
"experimentalSortImports": {
|
||||
"order": "asc",
|
||||
"ignoreCase": true,
|
||||
"newlinesBetween": true,
|
||||
"groups": [
|
||||
["builtin", "external"],
|
||||
"internal",
|
||||
["parent", "sibling", "index"],
|
||||
"type"
|
||||
]
|
||||
}
|
||||
}
|
||||
101
bun.lock
101
bun.lock
|
|
@ -15,7 +15,10 @@
|
|||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260227.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"oxfmt": "^0.35.0",
|
||||
"oxlint": "^1.50.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
},
|
||||
|
|
@ -122,6 +125,82 @@
|
|||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA=="],
|
||||
|
||||
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.35.0", "", { "os": "android", "cpu": "arm64" }, "sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ=="],
|
||||
|
||||
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.35.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pGqRtqlNdn9d4VrmGUWVyQjkw79ryhI6je9y2jfqNUIZCfqceob+R97YYAoG7C5TFyt8ILdLVoN+L2vw/hSFyA=="],
|
||||
|
||||
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.35.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8GmsDcSozTPjrCJeGpp+sCmS9+9V5yRrdEZ1p/sTWxPG5nYeAfSLuS0nuEYjXSO+CtdSbStIW6dxa+4NM58yRw=="],
|
||||
|
||||
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.35.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QyfKfTe0ytHpFKHAcHCGQEzN45QSqq1AHJOYYxQMgLM3KY4xu8OsXHpCnINjDsV4XGnQzczJDU9e04Zmd8XqIQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u+kv3JD6P3J38oOyUaiCqgY5TNESzBRZJ5lyZQ6c2czUW2v5SIN9E/KWWa9vxoc+P8AFXQFUVrdzGy1tK+nbPQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-1NiZroCiV57I7Pf8kOH4XGR366kW5zir3VfSMBU2D0V14GpYjiYmPYFAoJboZvp8ACnZKUReWyMkNKSa5ad58A=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7Q0Xeg7ZnW2nxnZ4R7aF6DEbCFls4skgHZg+I63XitpNvJCbVIU8MFOTZlvZGRsY9+rPgWPQGeUpLHlyx0wvMA=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Okqi+uhYFxwKz8hcnUftNNwdm8BCkf6GSCbcz9xJxYMm87k1E4p7PEmAAbhLTk7cjSdDre6TDL0pDzNX+Y22Q=="],
|
||||
|
||||
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.35.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9k66pbZQXM/lBJWys3Xbc5yhl4JexyfqkEf/tvtq8976VIJnLAAL3M127xHA3ifYSqxdVHfVGTg84eiBHCGcNw=="],
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.35.0", "", { "os": "linux", "cpu": "none" }, "sha512-aUcY9ofKPtjO52idT6t0SAQvEF6ctjzUQa1lLp7GDsRpSBvuTrBQGeq0rYKz3gN8dMIQ7mtMdGD9tT4LhR8jAQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.35.0", "", { "os": "linux", "cpu": "none" }, "sha512-C6yhY5Hvc2sGM+mCPek9ZLe5xRUOC/BvhAt2qIWFAeXMn4il04EYIjl3DsWiJr0xDMTJhvMOmD55xTRPlNp39w=="],
|
||||
|
||||
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.35.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RG2hlvOMK4OMZpO3mt8MpxLQ0AAezlFqhn5mI/g5YrVbPFyoCv9a34AAvbSJS501ocOxlFIRcKEuw5hFvddf9g=="],
|
||||
|
||||
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wzmh90Pwvqj9xOKHJjkQYBpydRkaXG77ZvDz+iFDRRQpnqIEqGm5gmim2s6vnZIkDGsvKCuTdtxm0GFmBjM1+w=="],
|
||||
|
||||
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+HCqYCJPCUy5I+b2cf+gUVaApfgtoQT3HdnSg/l7NIcLHOhKstlYaGyrFZLmUpQt4WkFbpGKZZayG6zjRU0KFA=="],
|
||||
|
||||
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.35.0", "", { "os": "none", "cpu": "arm64" }, "sha512-kFYmWfR9YL78XyO5ws+1dsxNvZoD973qfVMNFOS4e9bcHXGF7DvGC2tY5UDFwyMCeB33t3sDIuGONKggnVNSJA=="],
|
||||
|
||||
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.35.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uD/NGdM65eKNCDGyTGdO8e9n3IHX+wwuorBvEYrPJXhDXL9qz6gzddmXH8EN04ejUXUujlq4FsoSeCfbg0Y+Jg=="],
|
||||
|
||||
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.35.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-oSRD2k8J2uxYDEKR2nAE/YTY9PobOEnhZgCmspHu0+yBQ665yH8lFErQVSTE7fcGJmJp/cC6322/gc8VFuQf7g=="],
|
||||
|
||||
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg=="],
|
||||
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw=="],
|
||||
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw=="],
|
||||
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ=="],
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA=="],
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ=="],
|
||||
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw=="],
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ=="],
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w=="],
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
|
||||
|
|
@ -216,6 +295,22 @@
|
|||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260227.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260227.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260227.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260227.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260227.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260227.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260227.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260227.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-MCQZuIj7mk152lHB0kcDpHZrPjfoEijPhhigE62cHtrxukheNy+beYt5rW44wxbiapsfJWsFbRPt+cB90M/A/A=="],
|
||||
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260227.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IXHoygCPooHUX/n5LyAI98Ig8vTtgxUcKaR2odN9FLWhHSv3X6adsl7glmpdD5EqVHRUva9GqFjsO0qr7hJQag=="],
|
||||
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260227.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pH7vB05JUO+638PCApWNcHEn/ZCB4ABy5HmqAfdjntYJ18K+/HL4P7cjxQfwA/t6LcuLd9miY9DoVh5c8yElVw=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260227.1", "", { "os": "linux", "cpu": "arm" }, "sha512-vTs7ICDzfK4v4R1Kh2zg9J0WH088jvkC2lTUS9Co8nFe2DaJ2I0QRTCg7m25GF8V+GskwpLH/k2/rGB9IA1QRA=="],
|
||||
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260227.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-BMgz0ZrSCjYpA8mAW5mMeepwfUFKnq/KKgBoyFWb/AcjiOwOhUJjC/222pSaBs5tkwgwajgIyDXaUDpiDW+5/w=="],
|
||||
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260227.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XrwuolZ0X9WoMiz3dGDXVH0bH+jHERdV6h1z2N0nAYod8Q5/jmpLLATvbq5A0fa0NQbTpZRzr7uXOVtGrcorkQ=="],
|
||||
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260227.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-XuTvdN8u1Sl+HG7WlwlQlM54gSrPlp3PicT8Ih/AnjiduQmNp0g1Cjt0kBrNMHUbXWmoJJfpCaweAY+PCT/glA=="],
|
||||
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260227.1", "", { "os": "win32", "cpu": "x64" }, "sha512-T26qE0bTzw+rS17OVE+vnHdBBMeOo5veWg04rYRRtiTAXvGZyscq8/C7v1GfnVotpxWi6ckjJ112wn0gJspjnQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"@webgpu/types": ["@webgpu/types@0.1.21", "", {}, "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow=="],
|
||||
|
|
@ -260,6 +355,10 @@
|
|||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"oxfmt": ["oxfmt@0.35.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.35.0", "@oxfmt/binding-android-arm64": "0.35.0", "@oxfmt/binding-darwin-arm64": "0.35.0", "@oxfmt/binding-darwin-x64": "0.35.0", "@oxfmt/binding-freebsd-x64": "0.35.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.35.0", "@oxfmt/binding-linux-arm-musleabihf": "0.35.0", "@oxfmt/binding-linux-arm64-gnu": "0.35.0", "@oxfmt/binding-linux-arm64-musl": "0.35.0", "@oxfmt/binding-linux-ppc64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-musl": "0.35.0", "@oxfmt/binding-linux-s390x-gnu": "0.35.0", "@oxfmt/binding-linux-x64-gnu": "0.35.0", "@oxfmt/binding-linux-x64-musl": "0.35.0", "@oxfmt/binding-openharmony-arm64": "0.35.0", "@oxfmt/binding-win32-arm64-msvc": "0.35.0", "@oxfmt/binding-win32-ia32-msvc": "0.35.0", "@oxfmt/binding-win32-x64-msvc": "0.35.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q=="],
|
||||
|
||||
"oxlint": ["oxlint@1.50.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.50.0", "@oxlint/binding-android-arm64": "1.50.0", "@oxlint/binding-darwin-arm64": "1.50.0", "@oxlint/binding-darwin-x64": "1.50.0", "@oxlint/binding-freebsd-x64": "1.50.0", "@oxlint/binding-linux-arm-gnueabihf": "1.50.0", "@oxlint/binding-linux-arm-musleabihf": "1.50.0", "@oxlint/binding-linux-arm64-gnu": "1.50.0", "@oxlint/binding-linux-arm64-musl": "1.50.0", "@oxlint/binding-linux-ppc64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-musl": "1.50.0", "@oxlint/binding-linux-s390x-gnu": "1.50.0", "@oxlint/binding-linux-x64-gnu": "1.50.0", "@oxlint/binding-linux-x64-musl": "1.50.0", "@oxlint/binding-openharmony-arm64": "1.50.0", "@oxlint/binding-win32-arm64-msvc": "1.50.0", "@oxlint/binding-win32-ia32-msvc": "1.50.0", "@oxlint/binding-win32-x64-msvc": "1.50.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
|
@ -282,6 +381,8 @@
|
|||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
|
|
|||
43
oxlint.json
Normal file
43
oxlint.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "react-hooks", "typescript", "import", "unicorn"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2024": true
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "warn",
|
||||
"no-console": "off",
|
||||
|
||||
"react/jsx-key": "error",
|
||||
"react/jsx-no-duplicate-props": "error",
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/no-children-prop": "error",
|
||||
"react/no-danger-with-children": "error",
|
||||
"react/no-direct-mutation-state": "error",
|
||||
"react/no-string-refs": "error",
|
||||
"react/require-render-return": "error",
|
||||
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
"typescript/no-explicit-any": "warn",
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
|
||||
"import/no-cycle": "error",
|
||||
"import/no-self-import": "error",
|
||||
|
||||
"unicorn/no-instanceof-array": "error",
|
||||
"unicorn/no-typeof-undefined": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts", "**/*.test.tsx"],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["node_modules", "dist", "src-tauri", "*.config.*"]
|
||||
}
|
||||
13
package.json
13
package.json
|
|
@ -7,7 +7,11 @@
|
|||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"lint": "oxlint -c oxlint.json src/",
|
||||
"format": "oxfmt --write src/",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"check": "bun run lint && bun run typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
|
|
@ -17,11 +21,14 @@
|
|||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260227.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"oxfmt": "^0.35.0",
|
||||
"oxlint": "^1.50.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4",
|
||||
"@tauri-apps/cli": "^2"
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
297
src/App.tsx
297
src/App.tsx
|
|
@ -1,8 +1,10 @@
|
|||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
|
||||
import { getCanvasKit } from './engine/canvaskit'
|
||||
import { SceneGraph } from './engine/scene-graph'
|
||||
import { SkiaRenderer } from './engine/renderer'
|
||||
import { SceneGraph } from './engine/scene-graph'
|
||||
import { UndoManager } from './engine/undo'
|
||||
|
||||
import type { NodeType, Fill } from './engine/scene-graph'
|
||||
|
||||
type Tool = 'SELECT' | 'FRAME' | 'RECTANGLE' | 'ELLIPSE' | 'LINE'
|
||||
|
|
@ -12,14 +14,14 @@ const TOOL_SHORTCUTS: Record<string, Tool> = {
|
|||
f: 'FRAME',
|
||||
r: 'RECTANGLE',
|
||||
o: 'ELLIPSE',
|
||||
l: 'LINE',
|
||||
l: 'LINE'
|
||||
}
|
||||
|
||||
const TOOL_COLORS: Record<string, { r: number; g: number; b: number; a: number }> = {
|
||||
FRAME: { r: 1, g: 1, b: 1, a: 1 },
|
||||
RECTANGLE: { r: 0.83, g: 0.83, b: 0.83, a: 1 },
|
||||
ELLIPSE: { r: 0.83, g: 0.83, b: 0.83, a: 1 },
|
||||
LINE: { r: 0, g: 0, b: 0, a: 1 },
|
||||
LINE: { r: 0, g: 0, b: 0, a: 1 }
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
|
@ -35,8 +37,14 @@ function App() {
|
|||
startY: number
|
||||
nodeId: string
|
||||
} | null>(null)
|
||||
const panningRef = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>(null)
|
||||
const movingRef = useRef<{ startX: number; startY: number; originals: Map<string, { x: number; y: number }> } | null>(null)
|
||||
const panningRef = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>(
|
||||
null
|
||||
)
|
||||
const movingRef = useRef<{
|
||||
startX: number
|
||||
startY: number
|
||||
originals: Map<string, { x: number; y: number }>
|
||||
} | null>(null)
|
||||
|
||||
const requestRender = useCallback(() => {
|
||||
const renderer = rendererRef.current
|
||||
|
|
@ -86,7 +94,15 @@ function App() {
|
|||
width: 800,
|
||||
height: 500,
|
||||
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }],
|
||||
strokes: [{ color: { r: 0.87, g: 0.87, b: 0.87, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'INSIDE' }],
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 0.87, g: 0.87, b: 0.87, a: 1 },
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
align: 'INSIDE'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
graph.createNode('RECTANGLE', graph.rootId, {
|
||||
|
|
@ -96,8 +112,19 @@ function App() {
|
|||
width: 240,
|
||||
height: 160,
|
||||
cornerRadius: 12,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, opacity: 1, visible: true }],
|
||||
effects: [{ type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 0.15 }, offset: { x: 0, y: 4 }, radius: 12, spread: 0, visible: true }],
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, opacity: 1, visible: true }
|
||||
],
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.15 },
|
||||
offset: { x: 0, y: 4 },
|
||||
radius: 12,
|
||||
spread: 0,
|
||||
visible: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
graph.createNode('ELLIPSE', graph.rootId, {
|
||||
|
|
@ -106,7 +133,9 @@ function App() {
|
|||
y: 160,
|
||||
width: 120,
|
||||
height: 120,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.13, g: 0.77, b: 0.42, a: 1 }, opacity: 1, visible: true }],
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.13, g: 0.77, b: 0.42, a: 1 }, opacity: 1, visible: true }
|
||||
]
|
||||
})
|
||||
|
||||
graph.createNode('RECTANGLE', graph.rootId, {
|
||||
|
|
@ -116,7 +145,9 @@ function App() {
|
|||
width: 200,
|
||||
height: 100,
|
||||
cornerRadius: 8,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.96, g: 0.52, b: 0.13, a: 1 }, opacity: 1, visible: true }],
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.96, g: 0.52, b: 0.13, a: 1 }, opacity: 1, visible: true }
|
||||
]
|
||||
})
|
||||
|
||||
graph.createNode('RECTANGLE', graph.rootId, {
|
||||
|
|
@ -126,11 +157,13 @@ function App() {
|
|||
width: 300,
|
||||
height: 56,
|
||||
cornerRadius: 28,
|
||||
fills: [{ type: 'SOLID', color: { r: 0.55, g: 0.36, b: 0.96, a: 1 }, opacity: 1, visible: true }],
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.55, g: 0.36, b: 0.96, a: 1 }, opacity: 1, visible: true }
|
||||
]
|
||||
})
|
||||
|
||||
setNodeCount(graph.nodes.size - 1)
|
||||
renderer.render(graph, selectedIds)
|
||||
renderer.render(graph, new Set())
|
||||
}
|
||||
|
||||
init()
|
||||
|
|
@ -139,6 +172,7 @@ function App() {
|
|||
destroyed = true
|
||||
rendererRef.current?.destroy()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Re-render when selection changes
|
||||
|
|
@ -193,7 +227,7 @@ function App() {
|
|||
const node = graph.getNode(id)
|
||||
if (!node) continue
|
||||
const snapshot = { ...node }
|
||||
const parentId = node.parentId!
|
||||
const parentId = node.parentId ?? graphRef.current.rootId
|
||||
undo.apply({
|
||||
label: 'Delete',
|
||||
forward: () => graph.deleteNode(id),
|
||||
|
|
@ -201,7 +235,7 @@ function App() {
|
|||
const restored = graph.createNode(snapshot.type, parentId, snapshot)
|
||||
// ID won't match, but good enough for PoC
|
||||
void restored
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
undo.commitBatch()
|
||||
|
|
@ -215,16 +249,22 @@ function App() {
|
|||
|
||||
function handleMouseDown(e: React.MouseEvent<HTMLCanvasElement>) {
|
||||
const renderer = rendererRef.current
|
||||
if (!renderer) return
|
||||
const canvas = canvasRef.current
|
||||
if (!renderer || !canvas) return
|
||||
|
||||
const rect = canvasRef.current!.getBoundingClientRect()
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const sx = e.clientX - rect.left
|
||||
const sy = e.clientY - rect.top
|
||||
const { x, y } = renderer.screenToCanvas(sx, sy)
|
||||
|
||||
// Space + click = pan
|
||||
if (e.button === 1 || activeTool === 'SELECT' && e.altKey) {
|
||||
panningRef.current = { startX: e.clientX, startY: e.clientY, panX: renderer.panX, panY: renderer.panY }
|
||||
if (e.button === 1 || (activeTool === 'SELECT' && e.altKey)) {
|
||||
panningRef.current = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
panX: renderer.panX,
|
||||
panY: renderer.panY
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +274,8 @@ function App() {
|
|||
const newSelected = e.shiftKey
|
||||
? (() => {
|
||||
const s = new Set(selectedIds)
|
||||
s.has(hit.id) ? s.delete(hit.id) : s.add(hit.id)
|
||||
if (s.has(hit.id)) s.delete(hit.id)
|
||||
else s.add(hit.id)
|
||||
return s
|
||||
})()
|
||||
: new Set([hit.id])
|
||||
|
|
@ -258,7 +299,7 @@ function App() {
|
|||
FRAME: 'FRAME',
|
||||
RECTANGLE: 'RECTANGLE',
|
||||
ELLIPSE: 'ELLIPSE',
|
||||
LINE: 'LINE',
|
||||
LINE: 'LINE'
|
||||
}
|
||||
const nodeType = typeMap[activeTool]
|
||||
if (!nodeType) return
|
||||
|
|
@ -267,7 +308,7 @@ function App() {
|
|||
type: 'SOLID',
|
||||
color: TOOL_COLORS[activeTool] ?? { r: 0.83, g: 0.83, b: 0.83, a: 1 },
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
visible: true
|
||||
}
|
||||
|
||||
const node = graphRef.current.createNode(nodeType, graphRef.current.rootId, {
|
||||
|
|
@ -275,7 +316,7 @@ function App() {
|
|||
y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
fills: [fill],
|
||||
fills: [fill]
|
||||
})
|
||||
|
||||
drawingRef.current = { startX: x, startY: y, nodeId: node.id }
|
||||
|
|
@ -285,7 +326,8 @@ function App() {
|
|||
|
||||
function handleMouseMove(e: React.MouseEvent<HTMLCanvasElement>) {
|
||||
const renderer = rendererRef.current
|
||||
if (!renderer) return
|
||||
const canvas = canvasRef.current
|
||||
if (!renderer || !canvas) return
|
||||
|
||||
// Pan
|
||||
if (panningRef.current) {
|
||||
|
|
@ -297,7 +339,7 @@ function App() {
|
|||
return
|
||||
}
|
||||
|
||||
const rect = canvasRef.current!.getBoundingClientRect()
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const sx = e.clientX - rect.left
|
||||
const sy = e.clientY - rect.top
|
||||
const { x, y } = renderer.screenToCanvas(sx, sy)
|
||||
|
|
@ -322,7 +364,7 @@ function App() {
|
|||
x: w < 0 ? x : startX,
|
||||
y: h < 0 ? y : startY,
|
||||
width: Math.abs(w),
|
||||
height: Math.abs(h),
|
||||
height: Math.abs(h)
|
||||
})
|
||||
requestRender()
|
||||
}
|
||||
|
|
@ -346,7 +388,7 @@ function App() {
|
|||
},
|
||||
inverse: () => {
|
||||
for (const [id, pos] of originals) graph.updateNode(id, pos)
|
||||
},
|
||||
}
|
||||
})
|
||||
movingRef.current = null
|
||||
}
|
||||
|
|
@ -369,12 +411,13 @@ function App() {
|
|||
|
||||
function handleWheel(e: React.WheelEvent<HTMLCanvasElement>) {
|
||||
const renderer = rendererRef.current
|
||||
if (!renderer) return
|
||||
const canvas = canvasRef.current
|
||||
if (!renderer || !canvas) return
|
||||
e.preventDefault()
|
||||
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
// Zoom
|
||||
const rect = canvasRef.current!.getBoundingClientRect()
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const sx = e.clientX - rect.left
|
||||
const sy = e.clientY - rect.top
|
||||
|
||||
|
|
@ -398,20 +441,50 @@ function App() {
|
|||
{ key: 'FRAME', label: '# Frame', shortcut: 'F' },
|
||||
{ key: 'RECTANGLE', label: '□ Rect', shortcut: 'R' },
|
||||
{ key: 'ELLIPSE', label: '○ Ellipse', shortcut: 'O' },
|
||||
{ key: 'LINE', label: '/ Line', shortcut: 'L' },
|
||||
{ key: 'LINE', label: '/ Line', shortcut: 'L' }
|
||||
]
|
||||
|
||||
const selectedNode = selectedIds.size === 1
|
||||
? graphRef.current.getNode([...selectedIds][0])
|
||||
: undefined
|
||||
const selectedNode =
|
||||
selectedIds.size === 1 ? graphRef.current.getNode([...selectedIds][0]) : undefined
|
||||
|
||||
return (
|
||||
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column', background: '#1e1e1e', color: '#e0e0e0', fontFamily: 'Inter, system-ui, sans-serif', fontSize: 13, overflow: 'hidden', userSelect: 'none' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#1e1e1e',
|
||||
color: '#e0e0e0',
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fontSize: 13,
|
||||
overflow: 'hidden',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
{/* Canvas */}
|
||||
<div style={{ flex: 1, position: 'relative', display: 'flex' }}>
|
||||
{/* Left panel — Layers */}
|
||||
<div style={{ width: 240, background: '#2a2a2a', borderRight: '1px solid #3a3a3a', overflowY: 'auto', padding: '8px 0' }}>
|
||||
<div style={{ padding: '4px 12px', fontSize: 11, color: '#888', textTransform: 'uppercase', letterSpacing: 1 }}>Layers</div>
|
||||
<div
|
||||
style={{
|
||||
width: 240,
|
||||
background: '#2a2a2a',
|
||||
borderRight: '1px solid #3a3a3a',
|
||||
overflowY: 'auto',
|
||||
padding: '8px 0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
fontSize: 11,
|
||||
color: '#888',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1
|
||||
}}
|
||||
>
|
||||
Layers
|
||||
</div>
|
||||
{graphRef.current.getChildren(graphRef.current.rootId).map((node) => (
|
||||
<div
|
||||
key={node.id}
|
||||
|
|
@ -424,7 +497,7 @@ function App() {
|
|||
margin: '1px 4px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12 }}>
|
||||
|
|
@ -438,7 +511,13 @@ function App() {
|
|||
{/* Canvas */}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{ flex: 1, display: 'block', cursor: activeTool === 'SELECT' ? 'default' : 'crosshair', minWidth: 0, minHeight: 0 }}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'block',
|
||||
cursor: activeTool === 'SELECT' ? 'default' : 'crosshair',
|
||||
minWidth: 0,
|
||||
minHeight: 0
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
|
|
@ -447,39 +526,129 @@ function App() {
|
|||
/>
|
||||
|
||||
{/* Right panel — Properties */}
|
||||
<div style={{ width: 260, background: '#2a2a2a', borderLeft: '1px solid #3a3a3a', overflowY: 'auto', padding: '8px 0' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 260,
|
||||
background: '#2a2a2a',
|
||||
borderLeft: '1px solid #3a3a3a',
|
||||
overflowY: 'auto',
|
||||
padding: '8px 0'
|
||||
}}
|
||||
>
|
||||
{selectedNode ? (
|
||||
<>
|
||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid #3a3a3a' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{selectedNode.name}</div>
|
||||
<div style={{ fontSize: 11, color: '#888' }}>{selectedNode.type} · {selectedNode.id}</div>
|
||||
<div style={{ fontSize: 11, color: '#888' }}>
|
||||
{selectedNode.type} · {selectedNode.id}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '8px 12px' }}>
|
||||
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>Appearance</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, fontSize: 12 }}>
|
||||
<label>W: <input type="number" value={Math.round(selectedNode.width)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { width: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
||||
<label>H: <input type="number" value={Math.round(selectedNode.height)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { height: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
||||
<label>X: <input type="number" value={Math.round(selectedNode.x)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { x: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
||||
<label>Y: <input type="number" value={Math.round(selectedNode.y)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { y: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
||||
<label>R: <input type="number" value={Math.round(selectedNode.rotation)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { rotation: +e.target.value }); requestRender() }} style={inputStyle} />°</label>
|
||||
<label>↻: <input type="number" value={selectedNode.cornerRadius} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { cornerRadius: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
||||
<div
|
||||
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, fontSize: 12 }}
|
||||
>
|
||||
<label>
|
||||
W:{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(selectedNode.width)}
|
||||
onChange={(e) => {
|
||||
graphRef.current.updateNode(selectedNode.id, { width: +e.target.value })
|
||||
requestRender()
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
H:{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(selectedNode.height)}
|
||||
onChange={(e) => {
|
||||
graphRef.current.updateNode(selectedNode.id, { height: +e.target.value })
|
||||
requestRender()
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
X:{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(selectedNode.x)}
|
||||
onChange={(e) => {
|
||||
graphRef.current.updateNode(selectedNode.id, { x: +e.target.value })
|
||||
requestRender()
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Y:{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(selectedNode.y)}
|
||||
onChange={(e) => {
|
||||
graphRef.current.updateNode(selectedNode.id, { y: +e.target.value })
|
||||
requestRender()
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
R:{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(selectedNode.rotation)}
|
||||
onChange={(e) => {
|
||||
graphRef.current.updateNode(selectedNode.id, { rotation: +e.target.value })
|
||||
requestRender()
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
°
|
||||
</label>
|
||||
<label>
|
||||
↻:{' '}
|
||||
<input
|
||||
type="number"
|
||||
value={selectedNode.cornerRadius}
|
||||
onChange={(e) => {
|
||||
graphRef.current.updateNode(selectedNode.id, {
|
||||
cornerRadius: +e.target.value
|
||||
})
|
||||
requestRender()
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '8px 12px', borderTop: '1px solid #3a3a3a' }}>
|
||||
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>Fill</div>
|
||||
{selectedNode.fills.map((fill, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
background: `rgba(${fill.color.r * 255}, ${fill.color.g * 255}, ${fill.color.b * 255}, ${fill.color.a})`,
|
||||
border: '1px solid #555',
|
||||
}} />
|
||||
border: '1px solid #555'
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12 }}>
|
||||
#{Math.round(fill.color.r * 255).toString(16).padStart(2, '0')}
|
||||
{Math.round(fill.color.g * 255).toString(16).padStart(2, '0')}
|
||||
{Math.round(fill.color.b * 255).toString(16).padStart(2, '0')}
|
||||
#
|
||||
{Math.round(fill.color.r * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}
|
||||
{Math.round(fill.color.g * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}
|
||||
{Math.round(fill.color.b * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -500,15 +669,24 @@ function App() {
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ padding: '12px', color: '#666' }}>
|
||||
No selection
|
||||
</div>
|
||||
<div style={{ padding: '12px', color: '#666' }}>No selection</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom toolbar */}
|
||||
<div style={{ height: 48, background: '#2a2a2a', borderTop: '1px solid #3a3a3a', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2, padding: '0 16px' }}>
|
||||
<div
|
||||
style={{
|
||||
height: 48,
|
||||
background: '#2a2a2a',
|
||||
borderTop: '1px solid #3a3a3a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
padding: '0 16px'
|
||||
}}
|
||||
>
|
||||
{tools.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
|
|
@ -523,7 +701,7 @@ function App() {
|
|||
cursor: 'pointer',
|
||||
fontSize: 13,
|
||||
fontFamily: 'inherit',
|
||||
transition: 'background 0.1s',
|
||||
transition: 'background 0.1s'
|
||||
}}
|
||||
>
|
||||
{t.label}
|
||||
|
|
@ -531,7 +709,8 @@ function App() {
|
|||
))}
|
||||
<div style={{ flex: 1 }} />
|
||||
<span style={{ fontSize: 11, color: '#666' }}>
|
||||
{nodeCount} nodes · Zoom: {rendererRef.current ? `${Math.round(rendererRef.current.zoom * 100)}%` : '100%'}
|
||||
{nodeCount} nodes · Zoom:{' '}
|
||||
{rendererRef.current ? `${Math.round(rendererRef.current.zoom * 100)}%` : '100%'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -546,7 +725,7 @@ const inputStyle: React.CSSProperties = {
|
|||
color: '#e0e0e0',
|
||||
padding: '2px 4px',
|
||||
fontSize: 12,
|
||||
marginLeft: 4,
|
||||
marginLeft: 4
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ let instance: CanvasKit | null = null
|
|||
export async function getCanvasKit(): Promise<CanvasKit> {
|
||||
if (instance) return instance
|
||||
instance = await CanvasKitInit({
|
||||
locateFile: () => '/canvaskit.wasm',
|
||||
locateFile: () => '/canvaskit.wasm'
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { CanvasKit, Surface, Canvas, Paint } from 'canvaskit-wasm'
|
||||
import type { SceneNode, SceneGraph, Fill } from './scene-graph'
|
||||
import type { CanvasKit, Surface, Canvas, Paint } from 'canvaskit-wasm'
|
||||
|
||||
export class SkiaRenderer {
|
||||
private ck: CanvasKit
|
||||
|
|
@ -106,11 +106,7 @@ export class SkiaRenderer {
|
|||
}
|
||||
|
||||
if (node.rotation !== 0) {
|
||||
canvas.rotate(
|
||||
node.rotation,
|
||||
node.x + node.width / 2,
|
||||
node.y + node.height / 2
|
||||
)
|
||||
canvas.rotate(node.rotation, node.x + node.width / 2, node.y + node.height / 2)
|
||||
}
|
||||
|
||||
this.renderShape(canvas, node)
|
||||
|
|
@ -127,12 +123,7 @@ export class SkiaRenderer {
|
|||
}
|
||||
|
||||
private renderShape(canvas: Canvas, node: SceneNode): void {
|
||||
const rect = this.ck.LTRBRect(
|
||||
node.x,
|
||||
node.y,
|
||||
node.x + node.width,
|
||||
node.y + node.height
|
||||
)
|
||||
const rect = this.ck.LTRBRect(node.x, node.y, node.x + node.width, node.y + node.height)
|
||||
|
||||
const hasRadius =
|
||||
node.cornerRadius > 0 ||
|
||||
|
|
@ -161,15 +152,21 @@ export class SkiaRenderer {
|
|||
const rrect = this.ck.RRectXY(rect, node.cornerRadius, node.cornerRadius)
|
||||
// For independent corners, build a proper RRect
|
||||
const radii = [
|
||||
node.topLeftRadius, node.topLeftRadius,
|
||||
node.topRightRadius, node.topRightRadius,
|
||||
node.bottomRightRadius, node.bottomRightRadius,
|
||||
node.bottomLeftRadius, node.bottomLeftRadius,
|
||||
node.topLeftRadius,
|
||||
node.topLeftRadius,
|
||||
node.topRightRadius,
|
||||
node.topRightRadius,
|
||||
node.bottomRightRadius,
|
||||
node.bottomRightRadius,
|
||||
node.bottomLeftRadius,
|
||||
node.bottomLeftRadius
|
||||
]
|
||||
const rrectIndep = new Float32Array([
|
||||
node.x, node.y,
|
||||
node.x + node.width, node.y + node.height,
|
||||
...radii,
|
||||
node.x,
|
||||
node.y,
|
||||
node.x + node.width,
|
||||
node.y + node.height,
|
||||
...radii
|
||||
])
|
||||
canvas.drawRRect(rrectIndep, this.fillPaint)
|
||||
void rrect
|
||||
|
|
@ -182,13 +179,7 @@ export class SkiaRenderer {
|
|||
}
|
||||
break
|
||||
case 'LINE':
|
||||
canvas.drawLine(
|
||||
node.x,
|
||||
node.y,
|
||||
node.x + node.width,
|
||||
node.y + node.height,
|
||||
this.fillPaint
|
||||
)
|
||||
canvas.drawLine(node.x, node.y, node.x + node.width, node.y + node.height, this.fillPaint)
|
||||
break
|
||||
default:
|
||||
canvas.drawRect(rect, this.fillPaint)
|
||||
|
|
@ -233,7 +224,7 @@ export class SkiaRenderer {
|
|||
screenToCanvas(sx: number, sy: number): { x: number; y: number } {
|
||||
return {
|
||||
x: (sx - this.panX) / this.zoom,
|
||||
y: (sy - this.panY) / this.zoom,
|
||||
y: (sy - this.panY) / this.zoom
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,10 +82,7 @@ function generateId(): string {
|
|||
return `0:${nextLocalID++}`
|
||||
}
|
||||
|
||||
function createDefaultNode(
|
||||
type: NodeType,
|
||||
overrides: Partial<SceneNode> = {}
|
||||
): SceneNode {
|
||||
function createDefaultNode(type: NodeType, overrides: Partial<SceneNode> = {}): SceneNode {
|
||||
return {
|
||||
id: generateId(),
|
||||
type,
|
||||
|
|
@ -110,7 +107,7 @@ function createDefaultNode(
|
|||
cornerSmoothing: 0,
|
||||
visible: true,
|
||||
locked: false,
|
||||
...overrides,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +119,7 @@ export class SceneGraph {
|
|||
const root = createDefaultNode('FRAME', {
|
||||
name: 'Document',
|
||||
width: 0,
|
||||
height: 0,
|
||||
height: 0
|
||||
})
|
||||
this.rootId = root.id
|
||||
this.nodes.set(root.id, root)
|
||||
|
|
@ -140,11 +137,7 @@ export class SceneGraph {
|
|||
.filter((n): n is SceneNode => n !== undefined)
|
||||
}
|
||||
|
||||
createNode(
|
||||
type: NodeType,
|
||||
parentId: string,
|
||||
overrides: Partial<SceneNode> = {}
|
||||
): SceneNode {
|
||||
createNode(type: NodeType, parentId: string, overrides: Partial<SceneNode> = {}): SceneNode {
|
||||
const node = createDefaultNode(type, overrides)
|
||||
node.parentId = parentId
|
||||
this.nodes.set(node.id, node)
|
||||
|
|
@ -176,7 +169,7 @@ export class SceneGraph {
|
|||
}
|
||||
|
||||
// Delete children recursively
|
||||
for (const childId of [...node.childIds]) {
|
||||
for (const childId of Array.from(node.childIds)) {
|
||||
this.deleteNode(childId)
|
||||
}
|
||||
|
||||
|
|
@ -185,9 +178,7 @@ export class SceneGraph {
|
|||
|
||||
hitTest(px: number, py: number): SceneNode | null {
|
||||
// Reverse order = topmost first
|
||||
const allNodes = [...this.nodes.values()].filter(
|
||||
(n) => n.id !== this.rootId && n.visible
|
||||
)
|
||||
const allNodes = [...this.nodes.values()].filter((n) => n.id !== this.rootId && n.visible)
|
||||
|
||||
for (let i = allNodes.length - 1; i >= 0; i--) {
|
||||
const n = allNodes[i]
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class UndoManager {
|
|||
this.undoStack.push({
|
||||
label,
|
||||
forward: () => entries.forEach((e) => e.forward()),
|
||||
inverse: () => [...entries].reverse().forEach((e) => e.inverse()),
|
||||
inverse: () => [...entries].reverse().forEach((e) => e.inverse())
|
||||
})
|
||||
this.redoStack = []
|
||||
}
|
||||
|
|
@ -90,6 +90,6 @@ export function createPropertyChange(
|
|||
return {
|
||||
label,
|
||||
forward: () => Object.assign(node, changes),
|
||||
inverse: () => Object.assign(node, previous),
|
||||
inverse: () => Object.assign(node, previous)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
|
|
|
|||
Loading…
Reference in a new issue