diff --git a/electron/main.cjs b/electron/main.cjs index 6fd09bb7..df8e8316 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -27,21 +27,63 @@ if (!gotLock) { process.exit(0); } +// Centralized server cleanup +function stopServer() { + if (serverProcess) { + try { + serverProcess.kill('SIGTERM'); + } catch (e) { + console.warn('Error stopping server:', e.message); + } + serverProcess = null; + } +} + // Wait until server is ready -function waitForServer(url) { - return new Promise((resolve) => { +function waitForServer(url, maxRetries = 20, delay = 500) { + return new Promise((resolve, reject) => { + let retries = 0; + const check = () => { - http - .get(url, () => resolve()) - .on('error', () => setTimeout(check, 500)); + const req = http + .get(url, (res) => { + res.resume(); + + if (res.statusCode === 200) { + console.log('Server is ready'); + resolve(); + } else { + retry(); + } + }) + .on('error', retry); + + // Add timeout to prevent hanging + req.setTimeout(delay, () => { + req.destroy(new Error('Request timeout')); + }); }; + + const retry = () => { + retries++; + console.log(`Waiting for server... (${retries}/${maxRetries})`); + + if (retries >= maxRetries) { + return reject( + new Error(`Server failed to start after ${maxRetries} attempts`) + ); + } + + setTimeout(check, delay); + }; + check(); }); } -// Start Nitro server (production) +// Start Nitro server function startServer() { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const serverPath = path.join( process.resourcesPath, 'app.asar.unpacked', @@ -50,11 +92,11 @@ function startServer() { 'index.mjs' ); - console.log("Starting server from:", serverPath); + console.log('Starting server from:', serverPath); serverProcess = spawn('node', [serverPath], { - stdio: 'ignore', // no terminal - windowsHide: true, // hide CMD + stdio: 'ignore', + windowsHide: true, env: { ...process.env, HOST: serverHost, @@ -62,7 +104,37 @@ function startServer() { }, }); - waitForServer(`http://localhost:${serverPort}`).then(resolve); + // Robust startup handling + let settled = false; + + const fail = (err) => { + if (!settled) { + settled = true; + stopServer(); + reject(err); + } + }; + + // Handle spawn errors + serverProcess.once('error', (err) => { + fail(new Error(`Failed to start Nitro server: ${err.message}`)); + }); + + // Handle early exit + serverProcess.once('exit', (code) => { + if (!settled) { + fail(new Error(`Nitro server exited early with code ${code}`)); + } + }); + + waitForServer(`http://localhost:${serverPort}`) + .then(() => { + if (!settled) { + settled = true; + resolve(); + } + }) + .catch(fail); }); } @@ -78,25 +150,40 @@ function createWindow() { mainWindow.loadURL(`http://localhost:${serverPort}`); - // Show when ready mainWindow.once('ready-to-show', () => { mainWindow.show(); }); - // Debug only if needed mainWindow.webContents.on('did-fail-load', (e, code, desc) => { - console.log("LOAD FAILED:", code, desc); + console.log('LOAD FAILED:', code, desc); }); } -// App start +// Graceful shutdown +function shutdown() { + console.log('Shutting down...'); + stopServer(); + app.quit(); +} + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); + +// App starts app.whenReady().then(async () => { - await startServer(); - createWindow(); + try { + await startServer(); + createWindow(); + } catch (err) { + console.error('Failed to start server:', err.message); + stopServer(); + app.quit(); + } }); -// Cleanup +// Cleanup on window close app.on('window-all-closed', () => { - if (serverProcess) serverProcess.kill(); + stopServer(); + if (process.platform !== 'darwin') app.quit(); }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5c6b977d..6c0da887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "app", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "app", + "version": "1.0.0", "dependencies": { "@nut-tree-fork/nut-js": "^4.2.6", - "@tailwindcss/postcss": "^4.1.18", - "@tanstack/react-devtools": "^0.7.0", "@tanstack/react-router": "^1.132.0", - "@tanstack/react-router-devtools": "^1.132.0", "@tanstack/react-router-ssr-query": "^1.131.7", "@tanstack/react-start": "^1.132.0", "@tanstack/router-plugin": "^1.132.0", @@ -20,14 +19,16 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-icons": "^5.5.0", - "vite-tsconfig-paths": "^6.0.2", "winston": "^3.19.0", "ws": "^8.18.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", + "@tailwindcss/postcss": "^4.1.18", "@tailwindcss/typography": "^0.5.19", "@tanstack/devtools-vite": "^0.3.11", + "@tanstack/react-devtools": "^0.7.0", + "@tanstack/react-router-devtools": "^1.132.0", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.2.0", "@types/node": "^22.10.2", @@ -47,6 +48,7 @@ "tsx": "^4.19.0", "typescript": "^5.7.2", "vite": "^7.1.7", + "vite-tsconfig-paths": "^6.0.2", "vitest": "^3.0.5", "wait-on": "^9.0.4" } @@ -62,6 +64,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -153,6 +156,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -691,6 +695,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -731,6 +736,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1288,7 +1294,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1310,7 +1315,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1327,7 +1331,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1342,7 +1345,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2016,6 +2018,7 @@ "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz", "integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/core": "^0.22.12" } @@ -2052,6 +2055,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz", "integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -2064,6 +2068,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz", "integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -2088,6 +2093,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz", "integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12", "tinycolor2": "^1.6.0" @@ -2131,6 +2137,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz", "integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -2254,6 +2261,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz", "integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -2266,6 +2274,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz", "integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -2281,6 +2290,7 @@ "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz", "integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==", "license": "MIT", + "peer": true, "dependencies": { "@jimp/utils": "^0.22.12" }, @@ -3865,6 +3875,7 @@ "version": "2.4.3", "resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz", "integrity": "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==", + "dev": true, "license": "MIT", "dependencies": { "@solid-primitives/utils": "^6.3.2" @@ -3877,6 +3888,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.3.3.tgz", "integrity": "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==", + "dev": true, "license": "MIT", "dependencies": { "@solid-primitives/event-listener": "^2.4.3", @@ -3891,6 +3903,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@solid-primitives/resize-observer/-/resize-observer-2.1.3.tgz", "integrity": "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==", + "dev": true, "license": "MIT", "dependencies": { "@solid-primitives/event-listener": "^2.4.3", @@ -3906,6 +3919,7 @@ "version": "1.5.2", "resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.5.2.tgz", "integrity": "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==", + "dev": true, "license": "MIT", "dependencies": { "@solid-primitives/utils": "^6.3.2" @@ -3918,6 +3932,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz", "integrity": "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==", + "dev": true, "license": "MIT", "dependencies": { "@solid-primitives/utils": "^6.3.2" @@ -3930,6 +3945,7 @@ "version": "6.3.2", "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.2.tgz", "integrity": "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==", + "dev": true, "license": "MIT", "peerDependencies": { "solid-js": "^1.6.12" @@ -3959,6 +3975,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -3974,6 +3991,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -4000,6 +4018,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4016,6 +4035,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4032,6 +4052,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4048,6 +4069,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4064,6 +4086,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4080,6 +4103,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4096,6 +4120,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4112,6 +4137,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4128,6 +4154,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4152,6 +4179,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4173,6 +4201,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4189,6 +4218,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4202,6 +4232,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -4228,6 +4259,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.7.0.tgz", "integrity": "sha512-AlAoCqJhWLg9GBEaoV1g/j+X/WA1aJSWOsekxeuZpYeS2hdVuKAjj04KQLUMJhtLfNl2s2E+TCj7ZRtWyY3U4w==", + "dev": true, "license": "MIT", "dependencies": { "@solid-primitives/event-listener": "^2.4.3", @@ -4272,6 +4304,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-bus/-/devtools-event-bus-0.3.3.tgz", "integrity": "sha512-lWl88uLAz7ZhwNdLH6A3tBOSEuBCrvnY9Fzr5JPdzJRFdM5ZFdyNWz1Bf5l/F3GU57VodrN0KCFi9OA26H5Kpg==", + "dev": true, "license": "MIT", "dependencies": { "ws": "^8.18.3" @@ -4302,6 +4335,7 @@ "version": "0.4.4", "resolved": "https://registry.npmjs.org/@tanstack/devtools-ui/-/devtools-ui-0.4.4.tgz", "integrity": "sha512-5xHXFyX3nom0UaNfiOM92o6ziaHjGo3mcSGe2HD5Xs8dWRZNpdZ0Smd0B9ddEhy0oB+gXyMzZgUJb9DmrZV0Mg==", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -4352,6 +4386,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/@tanstack/devtools-client/-/devtools-client-0.0.3.tgz", "integrity": "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw==", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" @@ -4368,6 +4403,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.3.5.tgz", "integrity": "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4405,6 +4441,7 @@ "version": "0.7.11", "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.7.11.tgz", "integrity": "sha512-a2Lmz8x+JoDrsU6f7uKRcyyY+k8mA/n5mb9h7XJ3Fz/y3+sPV9t7vAW1s5lyNkQyyDt6V1Oim99faLthoJSxMw==", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/devtools": "0.7.0" @@ -4445,6 +4482,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.157.16.tgz", "integrity": "sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", @@ -4469,6 +4507,7 @@ "version": "1.157.16", "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.157.16.tgz", "integrity": "sha512-g6ekyzumfLBX6T5e+Vu2r37Z2CFJKrWRFqIy3vZ6A3x7OcuPV8uXNjyrLSiT/IsGTiF8YzwI4nWJa4fyd7NlCw==", + "dev": true, "license": "MIT", "dependencies": { "@tanstack/router-devtools-core": "1.157.16" @@ -4614,6 +4653,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.157.16.tgz", "integrity": "sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", @@ -4635,6 +4675,7 @@ "version": "1.157.16", "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.157.16.tgz", "integrity": "sha512-XBJTs/kMZYK6J2zhbGucHNuypwDB1t2vi8K5To+V6dUnLGBEyfQTf01fegiF4rpL1yXgomdGnP6aTiOFgldbVg==", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -4926,6 +4967,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5153,7 +5195,9 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5162,7 +5206,9 @@ "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", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5422,6 +5468,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6040,6 +6087,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6719,6 +6767,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7026,8 +7075,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "6.0.6", @@ -7059,6 +7107,7 @@ "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.4.4.tgz", "integrity": "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg==", "license": "MIT", + "peer": true, "peerDependencies": { "srvx": ">=0.7.1" }, @@ -7153,7 +7202,9 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.5.14", @@ -7194,6 +7245,7 @@ "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz", "integrity": "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==", "license": "MIT", + "peer": true, "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", @@ -7380,6 +7432,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7472,6 +7525,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -8023,7 +8077,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8044,7 +8097,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8083,16 +8135,6 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -8119,6 +8161,7 @@ "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -8986,12 +9029,14 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, "license": "MIT" }, "node_modules/goober": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "dev": true, "license": "MIT", "peerDependencies": { "csstype": "^3.0.10" @@ -9040,6 +9085,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/h3": { @@ -9807,6 +9853,7 @@ "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -10216,6 +10263,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -10524,7 +10572,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -10627,41 +10674,12 @@ } } }, - "node_modules/nitro/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/nitro/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "optional": true, - "peer": true, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/nitro/node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 20.19.0" }, @@ -11023,7 +11041,8 @@ "version": "2.0.0-alpha.3", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-2.0.0-alpha.3.tgz", "integrity": "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ohash": { "version": "2.0.11", @@ -11588,6 +11607,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11625,7 +11645,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -11643,7 +11662,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -11804,6 +11822,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11813,6 +11832,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12073,7 +12093,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -12105,6 +12124,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -12272,6 +12292,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -12445,7 +12466,9 @@ "version": "1.9.11", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", @@ -12708,12 +12731,15 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12756,7 +12782,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -13052,6 +13077,7 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" @@ -13111,8 +13137,9 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13295,6 +13322,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13391,6 +13419,7 @@ "version": "6.0.5", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.0.5.tgz", "integrity": "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", diff --git a/src/hooks/useTrackpadGesture.ts b/src/hooks/useTrackpadGesture.ts index 66f801d8..c5cbc255 100644 --- a/src/hooks/useTrackpadGesture.ts +++ b/src/hooks/useTrackpadGesture.ts @@ -36,7 +36,6 @@ export const useTrackpadGesture = ( ) => { const [isTracking, setIsTracking] = useState(false) - // Refs for tracking state (avoids re-renders during rapid movement) const ongoingTouches = useRef>(new Map()) const moved = useRef(false) const startTimeStamp = useRef(0) @@ -46,8 +45,23 @@ export const useTrackpadGesture = ( const lastPinchDist = useRef(null) const pinching = useRef(false) + const resetGestureState = (preserveDraggingTimeout = false) => { + ongoingTouches.current.clear() + moved.current = false + releasedCount.current = 0 + dragging.current = false + lastPinchDist.current = null + pinching.current = false + + if (!preserveDraggingTimeout && draggingTimeout.current) { + clearTimeout(draggingTimeout.current) + draggingTimeout.current = null + } + } + const processMovement = (sumX: number, sumY: number) => { const touchCount = ongoingTouches.current.size + if (dragging.current) { send({ type: "move", @@ -56,18 +70,27 @@ export const useTrackpadGesture = ( }) return } + const invertMult = invertScroll ? -1 : 1 + if (!scrollMode && touchCount === 2) { const touches = Array.from(ongoingTouches.current.values()) const dist = getTouchDistance(touches[0], touches[1]) + const delta = lastPinchDist.current !== null ? dist - lastPinchDist.current : 0 + if (pinching.current || Math.abs(delta) > PINCH_THRESHOLD) { pinching.current = true lastPinchDist.current = dist - send({ type: "zoom", delta: delta * sensitivity * invertMult }) + + send({ + type: "zoom", + delta: delta * sensitivity * invertMult, + }) } else { lastPinchDist.current = dist + send({ type: "scroll", dx: -sumX * sensitivity * invertMult, @@ -77,15 +100,18 @@ export const useTrackpadGesture = ( } else if (scrollMode || touchCount === 2) { let scrollDx = sumX let scrollDy = sumY + if (scrollMode) { const absDx = Math.abs(scrollDx) const absDy = Math.abs(scrollDy) + if (absDx > absDy * axisThreshold) { scrollDy = 0 } else if (absDy > absDx * axisThreshold) { scrollDx = 0 } } + send({ type: "scroll", dx: Math.round(-scrollDx * sensitivity * 10 * invertMult) / 10, @@ -112,8 +138,10 @@ export const useTrackpadGesture = ( } const touches = e.changedTouches + for (let i = 0; i < touches.length; i++) { const touch = touches[i] + ongoingTouches.current.set(touch.identifier, { identifier: touch.identifier, pageX: touch.pageX, @@ -126,13 +154,14 @@ export const useTrackpadGesture = ( if (ongoingTouches.current.size === 2) { const touches = Array.from(ongoingTouches.current.values()) + lastPinchDist.current = getTouchDistance(touches[0], touches[1]) + pinching.current = false } setIsTracking(true) - // If we're in dragging timeout, convert to actual drag if (draggingTimeout.current) { clearTimeout(draggingTimeout.current) draggingTimeout.current = null @@ -142,27 +171,31 @@ export const useTrackpadGesture = ( const handleTouchMove = (e: React.TouchEvent) => { const touches = e.changedTouches + let sumX = 0 let sumY = 0 let movedTouchesCount = 0 + const touchCount = ongoingTouches.current.size for (let i = 0; i < touches.length; i++) { const touch = touches[i] const tracked = ongoingTouches.current.get(touch.identifier) + if (!tracked) continue movedTouchesCount++ - // Check if we've moved enough to consider this a "move" gesture if (!moved.current) { const distSq = (touch.pageX - tracked.pageXStart) ** 2 + (touch.pageY - tracked.pageYStart) ** 2 + const thresholdIndex = Math.min( touchCount - 1, TOUCH_MOVE_THRESHOLD.length - 1, ) + const threshold = TOUCH_MOVE_THRESHOLD[thresholdIndex] const thresholdSq = threshold * threshold @@ -174,25 +207,24 @@ export const useTrackpadGesture = ( } } - // Calculate delta with acceleration const dx = touch.pageX - tracked.pageX const dy = touch.pageY - tracked.pageY + const timeDelta = e.timeStamp - tracked.timeStamp if (timeDelta > 0) { const speedX = (Math.abs(dx) / timeDelta) * 1000 const speedY = (Math.abs(dy) / timeDelta) * 1000 + sumX += dx * calculateAccelerationMult(speedX) sumY += dy * calculateAccelerationMult(speedY) } - // Update tracked position tracked.pageX = touch.pageX tracked.pageY = touch.pageY tracked.timeStamp = e.timeStamp } - // Normalize movement by number of touches that actually moved to prevent sensitivity doubling if (moved.current && movedTouchesCount > 0) { processMovement(sumX / movedTouchesCount, sumY / movedTouchesCount) } @@ -213,22 +245,17 @@ export const useTrackpadGesture = ( pinching.current = false } - // Mark as moved if too many fingers if (releasedCount.current > TOUCH_MOVE_THRESHOLD.length) { moved.current = true } - // All fingers lifted if (ongoingTouches.current.size === 0 && releasedCount.current >= 1) { setIsTracking(false) - // Release drag if active if (dragging.current) { - dragging.current = false send({ type: "click", button: "left", press: false }) } - // Handle tap/click if not moved and within timeout if ( !moved.current && e.timeStamp - startTimeStamp.current < TOUCH_TIMEOUT @@ -238,7 +265,6 @@ export const useTrackpadGesture = ( if (button) { send({ type: "click", button, press: true }) - // For left click, set up drag timeout if (button === "left") { draggingTimeout.current = setTimeout( handleDraggingTimeout, @@ -250,7 +276,7 @@ export const useTrackpadGesture = ( } } - releasedCount.current = 0 + resetGestureState(draggingTimeout.current !== null) } }