From af41a91afc84b61f8864b8457f192ead612b9c9b Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:39:08 -0500 Subject: [PATCH 01/10] chore: replace jest with vitest, modernize tsconfig and clean up configs --- .gitignore | 2 + jest.config.cjs | 6 - package-lock.json | 414 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 13 +- tsconfig.json | 34 ++-- vite.config.ts | 31 ++-- 6 files changed, 442 insertions(+), 58 deletions(-) delete mode 100644 jest.config.cjs diff --git a/.gitignore b/.gitignore index deed335..fd7d79d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ dist/ .env +.DS_Store +*.log diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index 8f06dc1..0000000 --- a/jest.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', // Or 'jsdom' if testing browser-specific code - roots: ['/src'], // Look for tests in the src directory - testMatch: ['**/*.test.ts'], // Files ending with .test.ts -}; diff --git a/package-lock.json b/package-lock.json index adab2a4..0959bd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "devDependencies": { "typescript": "^5.8.3", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^4.1.8" } }, "node_modules/@esbuild/aix-ppc64": { @@ -438,6 +439,13 @@ "node": ">=18" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", @@ -718,6 +726,31 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -725,6 +758,153 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -766,12 +946,35 @@ "@esbuild/win32-x64": "0.25.4" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "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" }, @@ -796,6 +999,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/magic-string": { + "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" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -815,6 +1028,27 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -823,9 +1057,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "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", "peer": true, @@ -905,6 +1139,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -915,15 +1156,46 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -932,6 +1204,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -952,6 +1234,7 @@ "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -1020,6 +1303,113 @@ "optional": true } } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 325d6db..c395bbb 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { "name": "clock_website", "version": "1.0.0", - "main": "dist/app.js", + "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build" + "build": "vite build", + "test": "vitest run", + "test:watch": "vitest" }, - "keywords": [], - "author": "", "license": "ISC", - "type": "module", - "description": "", "devDependencies": { "typescript": "^5.8.3", - "vite": "^6.3.5" + "vite": "^6.3.5", + "vitest": "^4.1.8" } } diff --git a/tsconfig.json b/tsconfig.json index fb38d22..df37ce3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,19 @@ { "compilerOptions": { - "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "module": "es2020", /* Specify what module code is generated. */ - "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - "strict": true, /* Enable all strict type-checking options. */ - "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "rootDir": "./src", + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index f96563c..1d4b590 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,17 +1,16 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; -export default defineConfig(() => { - - return { - root: 'src', // Set the source directory as the root - build: { - outDir: '../dist', // Output build files to a 'dist' directory at the project root - emptyOutDir: true, // Empty the output directory on build - }, - server: { - open: true, // Automatically open the browser on server start - }, - define: { - } - }; -}); +export default defineConfig({ + root: 'src', + build: { + outDir: '../dist', + emptyOutDir: true, + }, + server: { + open: true, + }, + test: { + globals: true, + environment: 'jsdom', + }, +}); \ No newline at end of file From 180504993d0643472b2068eb3cbe00722ff631d4 Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:40:23 -0500 Subject: [PATCH 02/10] feat: add vitest with time utility tests and refactor time formatting --- package-lock.json | 536 +++++++++++++++++++++++++++++++ package.json | 1 + src/utils/__tests__/time.test.ts | 102 ++++++ src/utils/time.ts | 51 +-- 4 files changed, 656 insertions(+), 34 deletions(-) create mode 100644 src/utils/__tests__/time.test.ts diff --git a/package-lock.json b/package-lock.json index 0959bd9..08478ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,218 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "jsdom": "^29.1.1", "typescript": "^5.8.3", "vite": "^6.3.5", "vitest": "^4.1.8" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -439,6 +646,24 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -881,6 +1106,16 @@ "node": ">=12" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -898,6 +1133,54 @@ "dev": true, "license": "MIT" }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -999,6 +1282,78 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1009,6 +1364,13 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1042,6 +1404,19 @@ "node": ">=12.20.0" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1099,6 +1474,26 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.40.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", @@ -1139,6 +1534,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1170,6 +1578,13 @@ "dev": true, "license": "MIT" }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1214,6 +1629,52 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -1228,6 +1689,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", + "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/vite": { "version": "6.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", @@ -1394,6 +1865,54 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1410,6 +1929,23 @@ "engines": { "node": ">=8" } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index c395bbb..939b829 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "license": "ISC", "devDependencies": { + "jsdom": "^29.1.1", "typescript": "^5.8.3", "vite": "^6.3.5", "vitest": "^4.1.8" diff --git a/src/utils/__tests__/time.test.ts b/src/utils/__tests__/time.test.ts new file mode 100644 index 0000000..a5c637a --- /dev/null +++ b/src/utils/__tests__/time.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { formatTime, formatTimeForTitle, formatDuration, formatFinishTime } from '../time'; + +function fakeDate(year: number, month: number, day: number, h: number, m: number, s: number): Date { + return new Date(year, month - 1, day, h, m, s); +} + +describe('formatTime', () => { + it('formats midnight as 12:00:00 AM', () => { + expect(formatTime(fakeDate(2025, 1, 1, 0, 0, 0))).toBe('12:00:00 AM'); + }); + + it('formats noon as 12:00:00 PM', () => { + expect(formatTime(fakeDate(2025, 6, 15, 12, 0, 0))).toBe('12:00:00 PM'); + }); + + it('formats morning time correctly', () => { + expect(formatTime(fakeDate(2025, 3, 10, 9, 5, 3))).toBe('9:05:03 AM'); + }); + + it('formats evening time correctly', () => { + expect(formatTime(fakeDate(2025, 12, 31, 23, 59, 59))).toBe('11:59:59 PM'); + }); + + it('pads single-digit hours', () => { + // 1 AM → "1:00:00 AM" (no leading zero on hours) + expect(formatTime(fakeDate(2025, 1, 1, 1, 0, 0))).toBe('1:00:00 AM'); + }); +}); + +describe('formatTimeForTitle', () => { + it('formats midnight without seconds', () => { + expect(formatTimeForTitle(fakeDate(2025, 1, 1, 0, 0, 0))).toBe('12:00 AM'); + }); + + it('formats evening time without seconds', () => { + expect(formatTimeForTitle(fakeDate(2025, 7, 4, 17, 30, 45))).toBe('5:30 PM'); + }); +}); + +describe('formatDuration', () => { + it('formats zero seconds as 00:00', () => { + expect(formatDuration(0)).toBe('00:00'); + }); + + it('formats seconds only', () => { + expect(formatDuration(45)).toBe('00:45'); + }); + + it('formats minutes only', () => { + expect(formatDuration(300)).toBe('05:00'); + }); + + it('formats mixed minutes and seconds', () => { + expect(formatDuration(150)).toBe('02:30'); + }); + + it('handles large durations', () => { + expect(formatDuration(3661)).toBe('61:01'); + }); + + it('clamps negative values to 00:00', () => { + expect(formatDuration(-10)).toBe('00:00'); + }); +}); + +describe('formatFinishTime', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns a string matching h:mm:ss AM/PM pattern', () => { + vi.useFakeTimers(); + vi.setSystemTime(fakeDate(2025, 6, 15, 12, 0, 0)); + + const result = formatFinishTime(0); + // Should be current time formatted + expect(result).toBe('12:00:00 PM'); + + vi.useRealTimers(); + }); + + it('formats finish time 60 seconds from now', () => { + vi.useFakeTimers(); + vi.setSystemTime(fakeDate(2025, 6, 15, 12, 0, 0)); + + const result = formatFinishTime(60); + expect(result).toBe('12:01:00 PM'); + + vi.useRealTimers(); + }); + + it('wraps across noon', () => { + vi.useFakeTimers(); + vi.setSystemTime(fakeDate(2025, 6, 15, 11, 59, 50)); + + const result = formatFinishTime(15); // 15 seconds later → 12:00:05 PM + expect(result).toBe('12:00:05 PM'); + + vi.useRealTimers(); + }); +}); \ No newline at end of file diff --git a/src/utils/time.ts b/src/utils/time.ts index 24c2238..3762c77 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,43 +1,33 @@ /** - * Formats a Date object into a `h:mm:ss AM/PM` string. - * @param date The Date object to format. - * @returns The formatted time string. + * Converts a 24-hour Date into 12-hour components. */ - -export function formatTime(date: Date): string { - let hours24 = date.getHours(); +function to12Hour(date: Date): { hours: number; minutes: string; seconds: string; ampm: string } { + const hours24 = date.getHours(); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); const ampm = hours24 >= 12 ? 'PM' : 'AM'; + const hours12 = hours24 % 12 || 12; + return { hours: hours12, minutes, seconds, ampm }; +} - let hours12 = hours24 % 12; - hours12 = hours12 ? hours12 : 12; // Convert hour '0' to '12' - - const hoursStr = String(hours12); - return `${hoursStr}:${minutes}:${seconds} ${ampm}`; +/** + * Formats a Date object into a `h:mm:ss AM/PM` string. + */ +export function formatTime(date: Date): string { + const { hours, minutes, seconds, ampm } = to12Hour(date); + return `${hours}:${minutes}:${seconds} ${ampm}`; } /** * Formats a Date object into a `h:mm AM/PM` string for the document title. - * @param date The Date object to format. - * @returns The formatted time string without seconds. */ export function formatTimeForTitle(date: Date): string { - let hours24 = date.getHours(); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const ampm = hours24 >= 12 ? 'PM' : 'AM'; - - let hours12 = hours24 % 12; - hours12 = hours12 ? hours12 : 12; - - const hoursStr = String(hours12); - return `${hoursStr}:${minutes} ${ampm}`; + const { hours, minutes, ampm } = to12Hour(date); + return `${hours}:${minutes} ${ampm}`; } /** * Converts total seconds into a `MM:SS` string. - * @param totalSeconds Non-negative number of seconds. - * @returns The formatted duration string (e.g., "05:30"). */ export function formatDuration(totalSeconds: number): string { if (totalSeconds < 0) totalSeconds = 0; @@ -48,16 +38,9 @@ export function formatDuration(totalSeconds: number): string { /** * Converts a number of seconds from now into a wall-clock time string. - * @param secondsFromNow Seconds until the timer finishes. - * @returns Formatted time like "5:12 PM". */ export function formatFinishTime(secondsFromNow: number): string { const finish = new Date(Date.now() + secondsFromNow * 1000); - let hours24 = finish.getHours(); - const minutes = String(finish.getMinutes()).padStart(2, '0'); - const seconds = String(finish.getSeconds()).padStart(2, '0'); - const ampm = hours24 >= 12 ? 'PM' : 'AM'; - let hours12 = hours24 % 12; - hours12 = hours12 ? hours12 : 12; - return `${hours12}:${minutes}:${seconds} ${ampm}`; -} + const { hours, minutes, seconds, ampm } = to12Hour(finish); + return `${hours}:${minutes}:${seconds} ${ampm}`; +} \ No newline at end of file From eb26eac0c58d0e36a43fed92894b01b743637b8e Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:42:43 -0500 Subject: [PATCH 03/10] feat: refactor clock with initClock and DI, extract pure computeClockTick --- src/components/__tests__/clock.test.ts | 136 +++++++++++++++++++++++++ src/components/clock.ts | 128 ++++++++++++++++------- src/main.ts | 18 +--- src/utils/audio.ts | 18 ++-- 4 files changed, 242 insertions(+), 58 deletions(-) create mode 100644 src/components/__tests__/clock.test.ts diff --git a/src/components/__tests__/clock.test.ts b/src/components/__tests__/clock.test.ts new file mode 100644 index 0000000..9fb389b --- /dev/null +++ b/src/components/__tests__/clock.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest'; +import { computeClockTick, msToNextSecond } from '../clock'; +import type { ClockTickInput } from '../clock'; + +function fakeDate(h: number, m: number, s: number, ms = 0): Date { + return new Date(2025, 5, 15, h, m, s, ms); +} + +function baseInput(overrides: Partial = {}): ClockTickInput { + return { + now: fakeDate(10, 30, 45), + lastMinute: null, + lastSecond: null, + isTimerActive: false, + devMode: false, + ...overrides, + }; +} + +describe('computeClockTick', () => { + it('returns formatted clock text', () => { + const result = computeClockTick(baseInput({ now: fakeDate(9, 5, 3) })); + expect(result.clockText).toBe('9:05:03 AM'); + }); + + it('formats PM times correctly', () => { + const result = computeClockTick(baseInput({ now: fakeDate(14, 30, 0) })); + expect(result.clockText).toBe('2:30:00 PM'); + }); + + it('formats midnight correctly', () => { + const result = computeClockTick(baseInput({ now: fakeDate(0, 0, 0) })); + expect(result.clockText).toBe('12:00:00 AM'); + }); + + describe('title updates', () => { + it('updates title when minute changes and timer is not active', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 31, 0), + lastMinute: 30, + lastSecond: 59, + isTimerActive: false, + })); + expect(result.titleText).toBe('10:31 AM | Simple Clock'); + }); + + it('does not update title when timer is active', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 31, 0), + lastMinute: 30, + lastSecond: 59, + isTimerActive: true, + })); + expect(result.titleText).toBeNull(); + }); + + it('does not update title when minute has not changed', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 30, 15), + lastMinute: 30, + lastSecond: 14, + isTimerActive: false, + })); + expect(result.titleText).toBeNull(); + }); + + it('updates title on first tick (lastMinute is null)', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 30, 45), + lastMinute: null, + isTimerActive: false, + })); + expect(result.titleText).toBe('10:30 AM | Simple Clock'); + }); + }); + + describe('timezone updates', () => { + it('updates timezone when second changes', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 30, 45), + lastSecond: 44, + })); + expect(result.timezoneText).not.toBeNull(); + }); + + it('does not update timezone when second has not changed', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 30, 45), + lastSecond: 45, + })); + expect(result.timezoneText).toBeNull(); + }); + + it('updates timezone on first tick (lastSecond is null)', () => { + const result = computeClockTick(baseInput({ + now: fakeDate(10, 30, 45), + lastSecond: null, + })); + expect(result.timezoneText).not.toBeNull(); + }); + }); + + describe('environment marker', () => { + it('shows DEV in dev mode', () => { + const result = computeClockTick(baseInput({ devMode: true })); + expect(result.envMarkerText).toBe('DEV'); + }); + + it('shows empty string in production mode', () => { + const result = computeClockTick(baseInput({ devMode: false })); + expect(result.envMarkerText).toBe(''); + }); + }); + + describe('state tracking', () => { + it('returns current minute and second for next tick', () => { + const result = computeClockTick(baseInput({ now: fakeDate(10, 31, 45) })); + expect(result.newLastMinute).toBe(31); + expect(result.newLastSecond).toBe(45); + }); + }); +}); + +describe('msToNextSecond', () => { + it('returns 0 when milliseconds is 0', () => { + expect(msToNextSecond(fakeDate(10, 30, 45, 0))).toBe(1000); + }); + + it('returns 1 when milliseconds is 999', () => { + expect(msToNextSecond(fakeDate(10, 30, 45, 999))).toBe(1); + }); + + it('returns 500 when milliseconds is 500', () => { + expect(msToNextSecond(fakeDate(10, 30, 45, 500))).toBe(500); + }); +}); \ No newline at end of file diff --git a/src/components/clock.ts b/src/components/clock.ts index 8451280..687568f 100644 --- a/src/components/clock.ts +++ b/src/components/clock.ts @@ -1,55 +1,115 @@ import { formatTime, formatTimeForTitle } from '../utils/time'; -interface ClockElements { - clock: HTMLElement; - timezone: HTMLElement; - environmentMarker: HTMLElement; +// ---------- Pure logic (testable without DOM) ---------- + +export interface ClockTickInput { + now: Date; + lastMinute: number | null; + lastSecond: number | null; + isTimerActive: boolean; + devMode: boolean; } -export function createClock(elements: ClockElements) { - const { clock, timezone, environmentMarker } = elements; - let lastSecond: number | null = null; +export interface ClockTickResult { + clockText: string; + timezoneText: string | null; + envMarkerText: string; + titleText: string | null; + newLastMinute: number; + newLastSecond: number; +} + +const cachedTimezone = new Intl.DateTimeFormat().resolvedOptions().timeZone.replace('_', ' '); + +/** + * Pure function: given the current state, returns what the clock should display. + */ +export function computeClockTick(input: ClockTickInput): ClockTickResult { + const { now, lastMinute, lastSecond, isTimerActive, devMode } = input; + const currentSecond = now.getSeconds(); + const currentMinute = now.getMinutes(); + + let titleText: string | null = null; + let timezoneText: string | null = null; + + if (currentMinute !== lastMinute && !isTimerActive) { + titleText = `${formatTimeForTitle(now)} | Simple Clock`; + } + + if (currentSecond !== lastSecond) { + timezoneText = cachedTimezone; + } + + return { + clockText: formatTime(now), + timezoneText, + envMarkerText: devMode ? 'DEV' : '', + titleText, + newLastMinute: currentMinute, + newLastSecond: currentSecond, + }; +} + +/** + * Returns milliseconds until the next whole second boundary. + */ +export function msToNextSecond(now: Date): number { + return 1000 - now.getMilliseconds(); +} + +// ---------- DOM wiring ---------- + +export function initClock(): { start: () => void; stop: () => void } | null { + const clockEl = document.getElementById('clock'); + const timezoneEl = document.getElementById('timezone'); + const envMarkerEl = document.getElementById('environment-marker'); + + if (!clockEl || !timezoneEl || !envMarkerEl) { + console.error('Could not find all required clock elements.'); + return null; + } + + // Non-null after guard + const clock = clockEl; + const timezone = timezoneEl; + const envMarker = envMarkerEl; + let lastMinute: number | null = null; + let lastSecond: number | null = null; let intervalId: number | null = null; - - const formatter = new Intl.DateTimeFormat(); - const timeZone = formatter.resolvedOptions().timeZone.replace('_', ' '); + const isDevMode = import.meta.env.MODE === 'development'; function tick() { const now = new Date(); - const currentSecond = now.getSeconds(); - const currentMinute = now.getMinutes(); - - if (currentMinute !== lastMinute) { - lastMinute = currentMinute; - // Don't overwrite title when timer is running/paused - if (!document.body.hasAttribute('data-timer-active')) { - document.title = `${formatTimeForTitle(now)} | Simple Clock`; - } - } + const result = computeClockTick({ + now, + lastMinute, + lastSecond, + isTimerActive: document.body.hasAttribute('data-timer-active'), + devMode: isDevMode, + }); - if (currentSecond !== lastSecond) { - lastSecond = currentSecond; - clock.textContent = formatTime(now); - timezone.textContent = timeZone; + lastMinute = result.newLastMinute; + lastSecond = result.newLastSecond; + + if (result.titleText !== null) { + document.title = result.titleText; + } - if (import.meta.env.MODE === 'development') { - environmentMarker.textContent = 'DEV'; - } else { - environmentMarker.textContent = ''; - } + if (result.timezoneText !== null) { + timezone.textContent = result.timezoneText; } + + clock.textContent = result.clockText; + envMarker.textContent = result.envMarkerText; } function start() { - // Sync to the next whole second for a clean first tick - const now = new Date(); - const msToNextSecond = 1000 - now.getMilliseconds(); - + const ms = msToNextSecond(new Date()); setTimeout(() => { tick(); intervalId = window.setInterval(tick, 1000); - }, msToNextSecond); + }, ms); } function stop() { diff --git a/src/main.ts b/src/main.ts index 9bd0214..7ad6e64 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,22 +1,10 @@ import './styles/main.css'; -import { createClock } from './components/clock'; +import { initClock } from './components/clock'; import { createTimer } from './components/timer'; import type { TimerState } from './components/timer'; -const clockElement = document.getElementById('clock'); -const timezoneElement = document.getElementById('timezone'); -const environmentMarker = document.getElementById('environment-marker'); - -if (clockElement && timezoneElement && environmentMarker) { - const clock = createClock({ - clock: clockElement, - timezone: timezoneElement, - environmentMarker: environmentMarker, - }); - clock.start(); -} else { - console.error('Could not find all required clock elements.'); -} +const clock = initClock(); +clock?.start(); // Timer const timerDisplay = document.getElementById('timer-display'); diff --git a/src/utils/audio.ts b/src/utils/audio.ts index 82f5b23..bef30b7 100644 --- a/src/utils/audio.ts +++ b/src/utils/audio.ts @@ -1,12 +1,12 @@ /** * Plays a short beep using the Web Audio API. - * Returns a promise that resolves when the beep finishes. + * Accepts an optional AudioContext for testability (dependency injection). */ -export function playBeep(): Promise { +export function playBeep(audioCtx?: AudioContext): Promise { + const ctx = audioCtx ?? new AudioContext(); return new Promise((resolve) => { - const audioCtx = new AudioContext(); - const oscillator = audioCtx.createOscillator(); - const gainNode = audioCtx.createGain(); + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); oscillator.type = 'square'; oscillator.frequency.value = 880; @@ -14,17 +14,17 @@ export function playBeep(): Promise { gainNode.gain.value = 0.3; gainNode.gain.exponentialRampToValueAtTime( 0.001, - audioCtx.currentTime + 0.5 + ctx.currentTime + 0.5, ); oscillator.connect(gainNode); - gainNode.connect(audioCtx.destination); + gainNode.connect(ctx.destination); oscillator.start(); - oscillator.stop(audioCtx.currentTime + 0.5); + oscillator.stop(ctx.currentTime + 0.5); oscillator.onended = () => { - audioCtx.close(); + ctx.close(); resolve(); }; }); From 5b26c778238c6c243be536ead8c7eceac59f50ea Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:44:55 -0500 Subject: [PATCH 04/10] feat: extract pure timer state machine (timer-core.ts) with 67 tests --- src/utils/__tests__/timer-core.test.ts | 423 +++++++++++++++++++++++++ src/utils/timer-core.ts | 248 +++++++++++++++ 2 files changed, 671 insertions(+) create mode 100644 src/utils/__tests__/timer-core.test.ts create mode 100644 src/utils/timer-core.ts diff --git a/src/utils/__tests__/timer-core.test.ts b/src/utils/__tests__/timer-core.test.ts new file mode 100644 index 0000000..98f7f7c --- /dev/null +++ b/src/utils/__tests__/timer-core.test.ts @@ -0,0 +1,423 @@ +import { describe, it, expect } from 'vitest'; +import { + parseInputSeconds, + clampInputs, + secondsToInputs, + hasValidInput, + decInputs, + incInputs, + parsePresets, + formatPresetLabel, + addPreset, + removePreset, + createTimerCore, + canStart, + startTimer, + pauseTimer, + resetTimer, + tickTimer, + applyPreset, + addUserPreset, + removeUserPreset, + titlePrefix, + isResetVisible, + isPresetsHidden, + isInputsEnabled, + isAddBtnInactive, + DEFAULT_PRESETS, + DEFAULT_DURATION, + MAX_PRESETS, +} from '../timer-core'; +import type { TimerCore } from '../timer-core'; + +function makeCore(overrides: Partial = {}): TimerCore { + return { ...createTimerCore(), ...overrides }; +} + +// ---------- Input helpers ---------- + +describe('parseInputSeconds', () => { + it('parses valid inputs', () => { + expect(parseInputSeconds('5', '0')).toBe(300); + expect(parseInputSeconds('0', '30')).toBe(30); + expect(parseInputSeconds('10', '45')).toBe(645); + }); + + it('handles empty/NaN inputs as 0', () => { + expect(parseInputSeconds('', '')).toBe(0); + expect(parseInputSeconds('abc', 'xyz')).toBe(0); + }); + + it('clamps values to max ranges', () => { + expect(parseInputSeconds('99', '59')).toBe(99 * 60 + 59); + expect(parseInputSeconds('100', '60')).toBe(99 * 60 + 59); + }); +}); + +describe('clampInputs', () => { + it('clamps to valid ranges', () => { + expect(clampInputs(-1, -1)).toEqual({ mins: 0, secs: 0 }); + expect(clampInputs(100, 60)).toEqual({ mins: 99, secs: 59 }); + }); + + it('passes through valid values', () => { + expect(clampInputs(5, 30)).toEqual({ mins: 5, secs: 30 }); + }); +}); + +describe('secondsToInputs', () => { + it('converts 0 to 0:0', () => { + expect(secondsToInputs(0)).toEqual({ mins: 0, secs: 0 }); + }); + + it('converts 300 to 5:0', () => { + expect(secondsToInputs(300)).toEqual({ mins: 5, secs: 0 }); + }); + + it('converts 90 to 1:30', () => { + expect(secondsToInputs(90)).toEqual({ mins: 1, secs: 30 }); + }); +}); + +describe('hasValidInput', () => { + it('returns true for positive values', () => { + expect(hasValidInput('1', '0')).toBe(true); + expect(hasValidInput('0', '1')).toBe(true); + }); + + it('returns false for zero', () => { + expect(hasValidInput('0', '0')).toBe(false); + }); +}); + +// ---------- Inc / Dec ---------- + +describe('decInputs', () => { + it('decrements minutes when activeInput is min', () => { + expect(decInputs(5, 0, 'min')).toEqual({ mins: 4, secs: 0 }); + }); + + it('does not go below 0 minutes', () => { + expect(decInputs(0, 0, 'min')).toEqual({ mins: 0, secs: 0 }); + }); + + it('decrements seconds by 15s steps', () => { + expect(decInputs(5, 30, 'sec')).toEqual({ mins: 5, secs: 15 }); + expect(decInputs(5, 15, 'sec')).toEqual({ mins: 5, secs: 0 }); + }); + + it('wraps from 0s to 45s and decrements minute', () => { + expect(decInputs(5, 0, 'sec')).toEqual({ mins: 4, secs: 45 }); + }); + + it('does not wrap below 0 minutes', () => { + expect(decInputs(0, 0, 'sec')).toEqual({ mins: 0, secs: 0 }); + }); +}); + +describe('incInputs', () => { + it('increments minutes when activeInput is min', () => { + expect(incInputs(5, 0, 'min')).toEqual({ mins: 6, secs: 0 }); + }); + + it('does not go above 99 minutes', () => { + expect(incInputs(99, 0, 'min')).toEqual({ mins: 99, secs: 0 }); + }); + + it('increments seconds by 15s steps', () => { + expect(incInputs(5, 0, 'sec')).toEqual({ mins: 5, secs: 15 }); + expect(incInputs(5, 15, 'sec')).toEqual({ mins: 5, secs: 30 }); + }); + + it('wraps from 45s to 0s and increments minute', () => { + expect(incInputs(5, 45, 'sec')).toEqual({ mins: 6, secs: 0 }); + }); + + it('does not wrap above 99 minutes', () => { + expect(incInputs(99, 45, 'sec')).toEqual({ mins: 99, secs: 0 }); + }); +}); + +// ---------- Preset helpers ---------- + +describe('parsePresets', () => { + it('returns defaults for null', () => { + expect(parsePresets(null)).toEqual(DEFAULT_PRESETS); + }); + + it('returns defaults for invalid JSON', () => { + expect(parsePresets('not json')).toEqual(DEFAULT_PRESETS); + }); + + it('returns defaults for non-array', () => { + expect(parsePresets('{"foo": 1}')).toEqual(DEFAULT_PRESETS); + }); + + it('returns defaults for array with non-numbers', () => { + expect(parsePresets('[1, "a", 3]')).toEqual(DEFAULT_PRESETS); + }); + + it('returns defaults for array with negative numbers', () => { + expect(parsePresets('[1, -5, 3]')).toEqual(DEFAULT_PRESETS); + }); + + it('parses valid preset arrays', () => { + expect(parsePresets('[60, 300, 900]')).toEqual([60, 300, 900]); + }); +}); + +describe('formatPresetLabel', () => { + it('formats whole minutes', () => { + expect(formatPresetLabel(300)).toBe('5m'); + expect(formatPresetLabel(60)).toBe('1m'); + }); + + it('formats minutes with seconds', () => { + expect(formatPresetLabel(90)).toBe('1m 30s'); + expect(formatPresetLabel(330)).toBe('5m 30s'); + }); +}); + +describe('addPreset', () => { + it('adds a new preset', () => { + expect(addPreset([300, 900], 1500)).toEqual([300, 900, 1500]); + }); + + it('sorts presets after adding', () => { + expect(addPreset([900], 300)).toEqual([300, 900]); + }); + + it('does not add duplicates', () => { + expect(addPreset([300, 900], 300)).toEqual([300, 900]); + }); + + it('does not add zero or negative', () => { + expect(addPreset([300], 0)).toEqual([300]); + expect(addPreset([300], -1)).toEqual([300]); + }); + + it('respects max limit', () => { + const presets = [1, 2, 3]; + expect(addPreset(presets, 4, 3)).toEqual([1, 2, 3]); + }); +}); + +describe('removePreset', () => { + it('removes by index', () => { + expect(removePreset([300, 900, 1500], 1)).toEqual([300, 1500]); + }); + + it('does not mutate original array', () => { + const original = [300, 900]; + removePreset(original, 0); + expect(original).toEqual([300, 900]); + }); +}); + +// ---------- State machine ---------- + +describe('createTimerCore', () => { + it('creates with default values', () => { + const core = createTimerCore(); + expect(core.state).toBe('idle'); + expect(core.remaining).toBe(DEFAULT_DURATION); + expect(core.configuredDuration).toBe(DEFAULT_DURATION); + expect(core.presets).toEqual(DEFAULT_PRESETS); + }); + + it('accepts custom presets', () => { + const core = createTimerCore([60, 120]); + expect(core.presets).toEqual([60, 120]); + }); +}); + +describe('canStart', () => { + it('returns true for idle, paused, finished', () => { + expect(canStart(makeCore({ state: 'idle' }))).toBe(true); + expect(canStart(makeCore({ state: 'paused' }))).toBe(true); + expect(canStart(makeCore({ state: 'finished' }))).toBe(true); + }); + + it('returns false for running', () => { + expect(canStart(makeCore({ state: 'running' }))).toBe(false); + }); +}); + +describe('startTimer', () => { + const now = new Date(2025, 5, 15, 12, 0, 0); + + it('transitions idle → running', () => { + const result = startTimer(makeCore({ remaining: 300 }), now); + expect(result).not.toBeNull(); + expect(result!.state).toBe('running'); + expect(result!.finishTimestamp).toBe(now.getTime() + 300000); + }); + + it('transitions paused → running (resume)', () => { + const result = startTimer(makeCore({ state: 'paused', remaining: 120 }), now); + expect(result!.state).toBe('running'); + expect(result!.remaining).toBe(120); + }); + + it('does nothing if already running', () => { + const result = startTimer(makeCore({ state: 'running', remaining: 100 }), now); + expect(result).toBeNull(); + }); + + it('returns null if remaining is 0 from idle', () => { + const result = startTimer(makeCore({ state: 'idle', remaining: 0 }), now); + expect(result).toBeNull(); + }); +}); + +describe('pauseTimer', () => { + it('transitions running → paused', () => { + const startTime = new Date(2025, 5, 15, 12, 0, 0); + const core = startTimer(makeCore({ remaining: 300 }), startTime)!; + // Simulate 10 seconds passing + const pauseTime = new Date(2025, 5, 15, 12, 0, 10); + const result = pauseTimer(core, pauseTime); + expect(result).not.toBeNull(); + expect(result!.state).toBe('paused'); + expect(result!.remaining).toBe(290); + expect(result!.finishTimestamp).toBeNull(); + }); + + it('does nothing if not running', () => { + expect(pauseTimer(makeCore({ state: 'idle' }))).toBeNull(); + expect(pauseTimer(makeCore({ state: 'paused' }))).toBeNull(); + expect(pauseTimer(makeCore({ state: 'finished' }))).toBeNull(); + }); +}); + +describe('resetTimer', () => { + it('returns to idle with configured duration', () => { + const core = makeCore({ configuredDuration: 600, remaining: 100, state: 'running' }); + const result = resetTimer(core); + expect(result.state).toBe('idle'); + expect(result.remaining).toBe(600); + expect(result.finishTimestamp).toBeNull(); + }); +}); + +describe('tickTimer', () => { + it('decrements remaining while running', () => { + const core = makeCore({ state: 'running', remaining: 300 }); + const result = tickTimer(core); + expect(result.remaining).toBe(299); + expect(result.state).toBe('running'); + }); + + it('transitions to finished when remaining hits 0', () => { + const core = makeCore({ state: 'running', remaining: 1 }); + const result = tickTimer(core); + expect(result.state).toBe('finished'); + expect(result.remaining).toBe(0); + expect(result.finishTimestamp).toBeNull(); + }); + + it('does nothing if not running', () => { + const core = makeCore({ state: 'idle', remaining: 300 }); + expect(tickTimer(core)).toEqual(core); + }); +}); + +describe('applyPreset', () => { + it('sets duration and remaining from preset', () => { + const result = applyPreset(makeCore(), 900); + expect(result.configuredDuration).toBe(900); + expect(result.remaining).toBe(900); + }); + + it('does nothing while running', () => { + const core = makeCore({ state: 'running' }); + expect(applyPreset(core, 900)).toEqual(core); + }); + + it('transitions paused to idle', () => { + const result = applyPreset(makeCore({ state: 'paused' }), 900); + expect(result.state).toBe('idle'); + }); +}); + +describe('addUserPreset / removeUserPreset', () => { + it('adds a preset to the core', () => { + const core = addUserPreset(makeCore(), 120); + expect(core.presets).toContain(120); + }); + + it('removes a preset by index', () => { + const core = removeUserPreset(makeCore(), 0); + expect(core.presets.length).toBe(DEFAULT_PRESETS.length - 1); + }); +}); + +// ---------- Derived display values ---------- + +describe('titlePrefix', () => { + it('returns empty for idle', () => { + expect(titlePrefix('idle', 300)).toBe(''); + }); + + it('returns empty for finished', () => { + expect(titlePrefix('finished', 0)).toBe(''); + }); + + it('returns formatted prefix for running', () => { + expect(titlePrefix('running', 300)).toBe('05:00 | '); + }); + + it('returns formatted prefix for paused', () => { + expect(titlePrefix('paused', 65)).toBe('01:05 | '); + }); +}); + +describe('isResetVisible', () => { + it('is false for idle', () => { + expect(isResetVisible('idle')).toBe(false); + }); + + it('is true for all other states', () => { + expect(isResetVisible('running')).toBe(true); + expect(isResetVisible('paused')).toBe(true); + expect(isResetVisible('finished')).toBe(true); + }); +}); + +describe('isPresetsHidden', () => { + it('is true for running and paused', () => { + expect(isPresetsHidden('running')).toBe(true); + expect(isPresetsHidden('paused')).toBe(true); + }); + + it('is false for idle and finished', () => { + expect(isPresetsHidden('idle')).toBe(false); + expect(isPresetsHidden('finished')).toBe(false); + }); +}); + +describe('isInputsEnabled', () => { + it('is true for idle and finished', () => { + expect(isInputsEnabled('idle')).toBe(true); + expect(isInputsEnabled('finished')).toBe(true); + }); + + it('is false for running and paused', () => { + expect(isInputsEnabled('running')).toBe(false); + expect(isInputsEnabled('paused')).toBe(false); + }); +}); + +describe('isAddBtnInactive', () => { + it('is true when total is 0', () => { + expect(isAddBtnInactive([300], 0)).toBe(true); + }); + + it('is true when preset already exists', () => { + expect(isAddBtnInactive([300, 900], 300)).toBe(true); + }); + + it('is false when valid and unique', () => { + expect(isAddBtnInactive([300, 900], 150)).toBe(false); + }); +}); \ No newline at end of file diff --git a/src/utils/timer-core.ts b/src/utils/timer-core.ts new file mode 100644 index 0000000..e6cae6a --- /dev/null +++ b/src/utils/timer-core.ts @@ -0,0 +1,248 @@ +/** + * Pure timer state machine. + * + * Every function takes state in and returns new state out — + * zero DOM, zero side effects. Fully unit-testable. + */ + +// ---------- Types ---------- + +export type TimerState = 'idle' | 'running' | 'paused' | 'finished'; + +export interface TimerCore { + state: TimerState; + remaining: number; + configuredDuration: number; + finishTimestamp: number | null; + presets: number[]; +} + +export const DEFAULT_DURATION = 5 * 60; // 5 minutes +export const MAX_PRESETS = 10; +export const DEFAULT_PRESETS = [300, 900, 1500]; // 5m, 15m, 25m + +// ---------- Input helpers ---------- + +/** Parse min/sec string inputs into total seconds, clamped to valid ranges. */ +export function parseInputSeconds(mins: string, secs: string): number { + const m = parseInt(mins, 10) || 0; + const s = parseInt(secs, 10) || 0; + return Math.min(m, 99) * 60 + Math.min(s, 59); +} + +/** Clamp raw min/sec numbers to valid ranges. */ +export function clampInputs(mins: number, secs: number): { mins: number; secs: number } { + return { + mins: Math.max(0, Math.min(mins, 99)), + secs: Math.max(0, Math.min(secs, 59)), + }; +} + +/** Convert total seconds to { mins, secs } for display in inputs. */ +export function secondsToInputs(totalSec: number): { mins: number; secs: number } { + return { + mins: Math.floor(totalSec / 60), + secs: totalSec % 60, + }; +} + +/** Check if the parsed input is greater than zero. */ +export function hasValidInput(mins: string, secs: string): boolean { + return parseInputSeconds(mins, secs) > 0; +} + +// ---------- Inc / Dec ---------- + +/** Decrement seconds by 15-second steps, wrapping across minutes. */ +export function decInputs(mins: number, secs: number, activeInput: 'min' | 'sec'): { mins: number; secs: number } { + if (activeInput === 'sec') { + const raw = Math.ceil(secs / 15) * 15 - 15; + if (raw < 0) { + return { + mins: mins > 0 ? mins - 1 : mins, + secs: mins > 0 ? 45 : 0, + }; + } + return { mins, secs: raw }; + } + return { mins: mins > 0 ? mins - 1 : mins, secs }; +} + +/** Increment seconds by 15-second steps, wrapping across minutes. */ +export function incInputs(mins: number, secs: number, activeInput: 'min' | 'sec'): { mins: number; secs: number } { + if (activeInput === 'sec') { + const raw = Math.floor(secs / 15) * 15 + 15; + if (raw > 59) { + return { + mins: mins < 99 ? mins + 1 : mins, + secs: 0, + }; + } + return { mins, secs: raw }; + } + return { mins: mins < 99 ? mins + 1 : mins, secs }; +} + +// ---------- Preset helpers ---------- + +/** Attempt to parse presets from a raw localStorage value. Returns defaults on failure. */ +export function parsePresets(raw: string | null): number[] { + if (!raw) return [...DEFAULT_PRESETS]; + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed) && parsed.every((n: unknown) => typeof n === 'number' && n > 0)) { + return parsed; + } + } catch { + // ignore + } + return [...DEFAULT_PRESETS]; +} + +/** Format a preset duration into a human label like "5m" or "15m 30s". */ +export function formatPresetLabel(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (secs === 0) return `${mins}m`; + return `${mins}m ${secs}s`; +} + +/** Add a preset if it's valid, not a duplicate, and within limit. Returns new array. */ +export function addPreset(presets: number[], seconds: number, max = MAX_PRESETS): number[] { + if (seconds <= 0) return presets; + if (presets.includes(seconds)) return presets; + if (presets.length >= max) return presets; + return [...presets, seconds].sort((a, b) => a - b); +} + +/** Remove a preset by index. Returns new array. */ +export function removePreset(presets: number[], index: number): number[] { + return presets.filter((_, i) => i !== index); +} + +// ---------- State machine ---------- + +/** Create the initial timer core state. */ +export function createTimerCore(presets?: number[]): TimerCore { + return { + state: 'idle', + remaining: DEFAULT_DURATION, + configuredDuration: DEFAULT_DURATION, + finishTimestamp: null, + presets: presets ?? [...DEFAULT_PRESETS], + }; +} + +/** Can the timer be started from the current state? */ +export function canStart(core: TimerCore): boolean { + return core.state === 'idle' || core.state === 'paused' || core.state === 'finished'; +} + +/** Transition to running. Returns new state or null if invalid. */ +export function startTimer(core: TimerCore, now: Date = new Date()): TimerCore | null { + if (core.state === 'running') return null; + if (core.state === 'idle' || core.state === 'finished') { + if (core.remaining <= 0) return null; + } + return { + ...core, + state: 'running', + finishTimestamp: now.getTime() + core.remaining * 1000, + }; +} + +/** Transition to paused. Returns new state or null if invalid. */ +export function pauseTimer(core: TimerCore, now: Date = new Date()): TimerCore | null { + if (core.state !== 'running') return null; + if (core.finishTimestamp === null) return null; + const remaining = Math.max(0, Math.ceil((core.finishTimestamp - now.getTime()) / 1000)); + return { + ...core, + state: 'paused', + remaining, + finishTimestamp: null, + }; +} + +/** Transition to idle (reset). Returns new state. */ +export function resetTimer(core: TimerCore): TimerCore { + return { + ...core, + state: 'idle', + remaining: core.configuredDuration, + finishTimestamp: null, + }; +} + +/** Tick the countdown. Returns new state (may be 'finished'). */ +export function tickTimer(core: TimerCore): TimerCore { + if (core.state !== 'running') return core; + const remaining = core.remaining - 1; + if (remaining <= 0) { + return { + ...core, + state: 'finished', + remaining: 0, + finishTimestamp: null, + }; + } + return { ...core, remaining }; +} + +/** Apply a preset duration. Returns new state. */ +export function applyPreset(core: TimerCore, seconds: number): TimerCore { + if (core.state === 'running') return core; + return { + ...core, + state: core.state === 'paused' ? 'idle' : core.state, + configuredDuration: seconds, + remaining: seconds, + finishTimestamp: null, + }; +} + +/** Add a user-defined preset. Returns new core with updated presets. */ +export function addUserPreset(core: TimerCore, seconds: number): TimerCore { + return { + ...core, + presets: addPreset(core.presets, seconds), + }; +} + +/** Remove a preset by index. Returns new core with updated presets. */ +export function removeUserPreset(core: TimerCore, index: number): TimerCore { + return { + ...core, + presets: removePreset(core.presets, index), + }; +} + +// ---------- Derived display values ---------- + +/** Compute the document title prefix. */ +export function titlePrefix(state: TimerState, remaining: number): string { + if (state === 'idle' || state === 'finished') return ''; + const m = Math.floor(remaining / 60); + const s = remaining % 60; + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')} | `; +} + +/** Determine if the reset button should be visible. */ +export function isResetVisible(state: TimerState): boolean { + return state !== 'idle'; +} + +/** Determine if presets should be hidden. */ +export function isPresetsHidden(state: TimerState): boolean { + return state === 'running' || state === 'paused'; +} + +/** Determine if inputs should be enabled. */ +export function isInputsEnabled(state: TimerState): boolean { + return state !== 'running' && state !== 'paused'; +} + +/** Determine the add-preset button disabled state. */ +export function isAddBtnInactive(presets: number[], totalSeconds: number): boolean { + return totalSeconds <= 0 || presets.includes(totalSeconds); +} \ No newline at end of file From c6ed66e6a2466f9261ce939a4a96448c2076c487 Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:47:08 -0500 Subject: [PATCH 05/10] refactor: slim main.ts to 4 lines, timer delegates to pure timer-core --- src/components/timer.ts | 609 +++++++++++++++------------------------- src/main.ts | 68 +---- 2 files changed, 236 insertions(+), 441 deletions(-) diff --git a/src/components/timer.ts b/src/components/timer.ts index 612ada3..1194d1d 100644 --- a/src/components/timer.ts +++ b/src/components/timer.ts @@ -1,168 +1,165 @@ import { formatDuration, formatFinishTime } from '../utils/time'; import { playBeep } from '../utils/audio'; - -export type TimerState = 'idle' | 'running' | 'paused' | 'finished'; - -const STORAGE_KEY = 'timer-presets'; -const MAX_PRESETS = 10; - -export interface TimerElements { - display: HTMLElement; - finishTimeEl: HTMLElement; - minInput: HTMLInputElement; - secInput: HTMLInputElement; - startBtn: HTMLButtonElement; - resetBtn: HTMLButtonElement; - decMinBtn: HTMLButtonElement; - incMinBtn: HTMLButtonElement; - toggleBtn: HTMLButtonElement; - panel: HTMLElement; - presetsContainer: HTMLElement; - presetsBar: HTMLElement; - presetAddBtn: HTMLButtonElement; -} +import { + createTimerCore, + startTimer, + pauseTimer, + resetTimer, + tickTimer, + applyPreset, + addUserPreset, + removeUserPreset, + parseInputSeconds, + secondsToInputs, + hasValidInput, + decInputs, + incInputs, + parsePresets, + formatPresetLabel, + isResetVisible, + isPresetsHidden, + isInputsEnabled, + isAddBtnInactive, +} from '../utils/timer-core'; +import type { TimerState } from '../utils/timer-core'; + +export type { TimerState }; export interface TimerCallbacks { onStateChange?: (state: TimerState) => void; } -function loadPresets(): number[] { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (raw) { - const parsed = JSON.parse(raw); - if (Array.isArray(parsed) && parsed.every((n: unknown) => typeof n === 'number' && n > 0)) { - return parsed; - } - } - } catch { - // ignore - } - return [300, 900, 1500]; // 5m, 15m, 25m defaults -} - -function savePresets(presets: number[]) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(presets)); -} - -function formatPresetLabel(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - if (secs === 0) return `${mins}m`; - return `${mins}m ${secs}s`; -} +const STORAGE_KEY = 'timer-presets'; -export function createTimer(elements: TimerElements, callbacks: TimerCallbacks = {}) { - const { - display, - finishTimeEl, - minInput, - secInput, - startBtn, - resetBtn, - decMinBtn, - incMinBtn, - toggleBtn, - panel, - presetsContainer, - presetsBar, - presetAddBtn, - } = elements; - - // Internal state - let state: TimerState = 'idle'; - /** Total remaining seconds in the countdown */ - let remaining = 0; - /** Last configured duration in seconds (for reset) */ - let configuredDuration = 5 * 60; // default 5:00 - /** Timestamp (ms) when the timer will finish */ - let finishTimestamp: number | null = null; +export function initTimer(callbacks: TimerCallbacks = {}) { + // ---------- DOM lookups (with type narrowing guard) ---------- + + const display = document.getElementById('timer-display'); + const finishTimeEl = document.getElementById('timer-finish-time'); + const minInput = document.getElementById('timer-min') as HTMLInputElement | null; + const secInput = document.getElementById('timer-sec') as HTMLInputElement | null; + const startBtn = document.getElementById('timer-start') as HTMLButtonElement | null; + const resetBtn = document.getElementById('timer-reset') as HTMLButtonElement | null; + const decMinBtn = document.getElementById('timer-dec-min') as HTMLButtonElement | null; + const incMinBtn = document.getElementById('timer-inc-min') as HTMLButtonElement | null; + const toggleBtn = document.getElementById('timer-toggle') as HTMLButtonElement | null; + const panel = document.getElementById('timer-panel'); + const presetsContainer = document.getElementById('timer-presets'); + const presetsBar = document.getElementById('timer-presets-bar'); + const presetAddBtn = document.getElementById('timer-preset-add') as HTMLButtonElement | null; + + if (!display || !finishTimeEl || !minInput || !secInput || !startBtn || !resetBtn || !decMinBtn || !incMinBtn || !toggleBtn || !panel || !presetsContainer || !presetsBar || !presetAddBtn) { + console.error('Could not find all required timer elements.'); + return; + } + + // Narrow to non-null + const dom = { + display, finishTimeEl, minInput, secInput, startBtn, resetBtn, + decMinBtn, incMinBtn, toggleBtn, panel, presetsContainer, presetsBar, presetAddBtn, + }; + + // ---------- Core state ---------- + + const presets = parsePresets(localStorage.getItem(STORAGE_KEY)); + let core = createTimerCore(presets); let intervalId: number | null = null; let isPanelOpen = false; - let presets: number[] = loadPresets(); let activeInput: 'min' | 'sec' = 'min'; - // ---------- Helpers ---------- + // ---------- Persistence ---------- - function setState(newState: TimerState) { - const wasActive = state !== 'idle' && state !== 'finished'; - state = newState; - const isActive = state !== 'idle' && state !== 'finished'; - if (isActive !== wasActive) { - document.body.toggleAttribute('data-timer-active', isActive); - } - updateResetBtnVisibility(); - updatePresetsVisibility(); - updateTitle(); - callbacks.onStateChange?.(newState); + function savePresets() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(core.presets)); } - function updateTitle() { - const prefix = state === 'idle' || state === 'finished' ? '' : `${formatDuration(remaining)} | `; - document.title = `${prefix}Simple Clock`; - } + // ---------- Render ---------- - function getInputSeconds(): number { - const mins = parseInt(minInput.value, 10) || 0; - const secs = parseInt(secInput.value, 10) || 0; - return Math.min(mins, 99) * 60 + Math.min(secs, 59); - } + function render() { + const { display, finishTimeEl, startBtn, resetBtn, minInput, secInput, decMinBtn, incMinBtn, presetsContainer, presetsBar, presetAddBtn, toggleBtn } = dom; - function setInputsFromSeconds(totalSec: number) { - const mins = Math.floor(totalSec / 60); - const secs = totalSec % 60; - minInput.value = String(mins); - secInput.value = String(secs); - } + display.textContent = formatDuration(core.remaining); - function updateDisplay() { - display.textContent = formatDuration(remaining); - updateFinishTimeEl(); - updateToggleContent(); - updateTitle(); - } - - function updateFinishTimeEl() { - if (state === 'running' && remaining > 0 && finishTimestamp !== null) { - finishTimeEl.textContent = `→ ${formatFinishTime(remaining)}`; + // Finish time + if (core.state === 'running' && core.remaining > 0 && core.finishTimestamp !== null) { + finishTimeEl.textContent = `→ ${formatFinishTime(core.remaining)}`; finishTimeEl.classList.add('visible'); } else { finishTimeEl.textContent = ''; finishTimeEl.classList.remove('visible'); } - } - function enableInputs(enabled: boolean) { - minInput.disabled = !enabled; - secInput.disabled = !enabled; - decMinBtn.disabled = !enabled; - incMinBtn.disabled = !enabled; - } - - function updateResetBtnVisibility() { - resetBtn.style.display = state === 'idle' ? 'none' : ''; + // Toggle button + renderToggle(); + + // Title + document.title = `${core.state === 'idle' || core.state === 'finished' ? '' : formatDuration(core.remaining) + ' | '}Simple Clock`; + + // Body attribute + const isActive = core.state !== 'idle' && core.state !== 'finished'; + document.body.toggleAttribute('data-timer-active', isActive); + + // Buttons + startBtn.textContent = core.state === 'running' ? '⏸ Pause' : core.state === 'paused' ? '▶ Resume' : '▶ Start'; + resetBtn.style.display = isResetVisible(core.state) ? '' : 'none'; + + // Inputs + const inputsEnabled = isInputsEnabled(core.state); + minInput.disabled = !inputsEnabled; + secInput.disabled = !inputsEnabled; + decMinBtn.disabled = !inputsEnabled; + incMinBtn.disabled = !inputsEnabled; + + // Presets visibility + const hidePresets = isPresetsHidden(core.state); + presetsContainer.style.display = hidePresets ? 'none' : ''; + decMinBtn.style.display = hidePresets ? 'none' : ''; + incMinBtn.style.display = hidePresets ? 'none' : ''; + + // Add button + if (!hidePresets) { + const total = parseInputSeconds(minInput.value, secInput.value); + if (core.presets.length >= 10) { + presetsBar.style.display = 'none'; + } else { + presetsBar.style.display = ''; + presetAddBtn.classList.toggle('inactive', isAddBtnInactive(core.presets, total)); + } + } else { + presetsBar.style.display = 'none'; + } } - function updatePresetsVisibility() { - const hide = state === 'running' || state === 'paused'; - presetsContainer.style.display = hide ? 'none' : ''; - decMinBtn.style.display = hide ? 'none' : ''; - incMinBtn.style.display = hide ? 'none' : ''; - if (!hide) { - updateAddBtnState(); + function renderToggle() { + const { toggleBtn } = dom; + if (isPanelOpen) { + toggleBtn.textContent = '⏱'; + } else if (core.state === 'running' && core.remaining > 0 && core.finishTimestamp !== null) { + toggleBtn.textContent = formatFinishTime(core.remaining); + } else if (core.state !== 'idle') { + toggleBtn.textContent = formatDuration(core.remaining); } else { - presetsBar.style.display = 'none'; + toggleBtn.textContent = '⏱'; } } - // ---------- Preset rendering ---------- + function setInputsFromSeconds(totalSec: number) { + const { mins, secs } = secondsToInputs(totalSec); + dom.minInput.value = String(mins); + dom.secInput.value = String(secs); + } function renderPresets() { + const { presetsContainer } = dom; presetsContainer.innerHTML = ''; - presets.forEach((seconds, index) => { + core.presets.forEach((seconds, index) => { const row = document.createElement('div'); row.className = 'preset-row'; - row.addEventListener('click', () => applyPreset(seconds)); + row.addEventListener('click', () => { + core = applyPreset(core, seconds); + setInputsFromSeconds(core.remaining); + render(); + }); const label = document.createElement('span'); label.className = 'preset-row-label'; @@ -174,7 +171,10 @@ export function createTimer(elements: TimerElements, callbacks: TimerCallbacks = removeBtn.title = 'Remove preset'; removeBtn.addEventListener('click', (e) => { e.stopPropagation(); - removePreset(index); + core = removeUserPreset(core, index); + savePresets(); + renderPresets(); + render(); }); row.appendChild(label); @@ -183,131 +183,28 @@ export function createTimer(elements: TimerElements, callbacks: TimerCallbacks = }); } - function updateAddBtnState() { - const seconds = getInputSeconds(); - if (presets.length >= MAX_PRESETS) { - presetsBar.style.display = 'none'; - } else { - presetsBar.style.display = ''; - if (seconds <= 0 || presets.includes(seconds)) { - presetAddBtn.classList.add('inactive'); - } else { - presetAddBtn.classList.remove('inactive'); - } - } - } - - function applyPreset(seconds: number) { - if (state === 'running') return; // don't change while running - - // If paused, reset the timer first so we start fresh - if (state === 'paused') { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - setState('idle'); - enableInputs(true); - startBtn.textContent = '▶ Start'; - document.body.classList.remove('timer-finished'); - } - - configuredDuration = seconds; - remaining = seconds; - setInputsFromSeconds(seconds); - updateDisplay(); - updateAddBtnState(); - } - - function addPreset() { - const seconds = getInputSeconds(); - if (seconds <= 0) return; - // Don't add duplicates - if (presets.includes(seconds)) return; - if (presets.length >= MAX_PRESETS) return; - presets.push(seconds); - presets.sort((a, b) => a - b); - savePresets(presets); - renderPresets(); - updateAddBtnState(); - } - - function removePreset(index: number) { - presets.splice(index, 1); - savePresets(presets); - renderPresets(); - updateAddBtnState(); - } - - // ---------- UI button handlers ---------- - - function onDec() { - if (activeInput === 'sec') { - let secs = parseInt(secInput.value, 10) || 0; - let mins = parseInt(minInput.value, 10) || 0; - const raw = Math.ceil(secs / 15) * 15 - 15; - if (raw < 0) { - // Wrapped below 0 (e.g. 0 → -15) → 45, decrement minute - secs = 45; - if (mins > 0) mins--; - else secs = 0; - } else { - secs = raw; - } - secInput.value = String(secs); - minInput.value = String(mins); - } else { - const mins = parseInt(minInput.value, 10) || 0; - if (mins > 0) { - minInput.value = String(mins - 1); - } - } - updateAddBtnState(); - } - - function onInc() { - if (activeInput === 'sec') { - let secs = parseInt(secInput.value, 10) || 0; - let mins = parseInt(minInput.value, 10) || 0; - const raw = Math.floor(secs / 15) * 15 + 15; - if (raw > 59) { - // Wrapped past 59 (e.g. 59 → 60) → 0, increment minute - secs = 0; - if (mins < 99) mins++; - } else { - secs = raw; - } - secInput.value = String(secs); - minInput.value = String(mins); - } else { - const mins = parseInt(minInput.value, 10) || 0; - if (mins < 99) { - minInput.value = String(mins + 1); - } - } - updateAddBtnState(); - } - - function validateInputs(): boolean { - const mins = parseInt(minInput.value, 10) || 0; - const secs = parseInt(secInput.value, 10) || 0; - if (mins < 0) minInput.value = '0'; - if (secs < 0) secInput.value = '0'; - if (mins > 99) minInput.value = '99'; - if (secs > 59) secInput.value = '59'; - const total = Math.min(mins, 99) * 60 + Math.min(secs, 59); - return total > 0; - } - // ---------- Core timer actions ---------- function runCountdown() { intervalId = window.setInterval(() => { - remaining--; - updateDisplay(); - - if (remaining <= 0) { - finish(); + core = tickTimer(core); + render(); + + if (core.state === 'finished') { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + document.body.classList.add('timer-finished'); + playBeep(); + let beepCount = 0; + const beepInterval = window.setInterval(() => { + playBeep(); + beepCount++; + if (beepCount >= 2) { + clearInterval(beepInterval); + } + }, 700); } }, 1000); } @@ -316,131 +213,89 @@ export function createTimer(elements: TimerElements, callbacks: TimerCallbacks = return 1000 - new Date().getMilliseconds(); } - function start() { - if (state === 'running') return; + function onStart() { + if (core.state === 'running') { + if (intervalId === null) return; + clearInterval(intervalId); + intervalId = null; + core = pauseTimer(core)!; + render(); + return; + } - if (state === 'idle' || state === 'finished') { - if (!validateInputs()) { - return; // can't start with 00:00 - } - configuredDuration = getInputSeconds(); - remaining = configuredDuration; + if (core.state === 'idle' || core.state === 'finished') { + if (!hasValidInput(dom.minInput.value, dom.secInput.value)) return; + const duration = parseInputSeconds(dom.minInput.value, dom.secInput.value); + core = { ...core, remaining: duration, configuredDuration: duration }; } - // If paused, remaining is already set - finishTimestamp = Date.now() + remaining * 1000; - setState('running'); - enableInputs(false); - startBtn.textContent = '⏸ Pause'; - updateDisplay(); + const next = startTimer(core); + if (!next) return; + core = next; + render(); - // Sync first tick with the clock's next whole second boundary setTimeout(() => { - if (state !== 'running') return; // was paused/reset during delay + if (core.state !== 'running') return; runCountdown(); }, msToNextSecond()); } - function pause() { - if (state !== 'running' || intervalId === null) return; - clearInterval(intervalId); - intervalId = null; - // Recalculate remaining from timestamp in case of drift - remaining = Math.max(0, Math.ceil((finishTimestamp! - Date.now()) / 1000)); - finishTimestamp = null; - setState('paused'); - startBtn.textContent = '▶ Resume'; - } - - function reset() { + function onReset() { if (intervalId !== null) { clearInterval(intervalId); intervalId = null; } - - finishTimestamp = null; - setState('idle'); - remaining = configuredDuration; - setInputsFromSeconds(configuredDuration); - enableInputs(true); - startBtn.textContent = '▶ Start'; - updateDisplay(); - - // Remove finished flash + core = resetTimer(core); + setInputsFromSeconds(core.configuredDuration); document.body.classList.remove('timer-finished'); + render(); } - function finish() { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - remaining = 0; - finishTimestamp = null; - updateDisplay(); - setState('finished'); - startBtn.textContent = '▶ Start'; - enableInputs(true); - - // Flash effect - document.body.classList.add('timer-finished'); - - // Beep - playBeep(); - - // Keep beeping 3 more times every 0.7s - let beepCount = 0; - const beepInterval = window.setInterval(() => { - playBeep(); - beepCount++; - if (beepCount >= 2) { - clearInterval(beepInterval); - } - }, 700); + function onDec() { + const mins = parseInt(dom.minInput.value, 10) || 0; + const secs = parseInt(dom.secInput.value, 10) || 0; + const result = decInputs(mins, secs, activeInput); + dom.minInput.value = String(result.mins); + dom.secInput.value = String(result.secs); + render(); } - function handleStartPause() { - if (state === 'running') { - pause(); - } else { - start(); - } + function onInc() { + const mins = parseInt(dom.minInput.value, 10) || 0; + const secs = parseInt(dom.secInput.value, 10) || 0; + const result = incInputs(mins, secs, activeInput); + dom.minInput.value = String(result.mins); + dom.secInput.value = String(result.secs); + render(); + } + + function onAddPreset() { + const seconds = parseInputSeconds(dom.minInput.value, dom.secInput.value); + core = addUserPreset(core, seconds); + savePresets(); + renderPresets(); + render(); } // ---------- Panel toggle ---------- - function updateToggleContent() { - if (isPanelOpen) { - toggleBtn.textContent = '⏱'; - } else if (state === 'running' && remaining > 0 && finishTimestamp !== null) { - toggleBtn.textContent = formatFinishTime(remaining); - } else if (state !== 'idle') { - toggleBtn.textContent = formatDuration(remaining); - } else { - toggleBtn.textContent = '⏱'; - } - } - function openPanel() { isPanelOpen = true; - panel.removeAttribute('hidden'); - toggleBtn.classList.add('active'); - updateToggleContent(); + dom.panel.removeAttribute('hidden'); + dom.toggleBtn.classList.add('active'); + renderToggle(); } function closePanel() { isPanelOpen = false; - panel.setAttribute('hidden', ''); - toggleBtn.classList.remove('active'); - updateToggleContent(); + dom.panel.setAttribute('hidden', ''); + dom.toggleBtn.classList.remove('active'); + renderToggle(); } function togglePanel() { - if (isPanelOpen) { - closePanel(); - } else { - openPanel(); - } + if (isPanelOpen) closePanel(); + else openPanel(); } function onDocumentClick(e: MouseEvent) { @@ -452,44 +307,33 @@ export function createTimer(elements: TimerElements, callbacks: TimerCallbacks = } function onDocumentKeydown(e: KeyboardEvent) { - if (e.key === 'Escape' && isPanelOpen) { - closePanel(); - } + if (e.key === 'Escape' && isPanelOpen) closePanel(); } - // ---------- Lifecycle ---------- + // ---------- Init ---------- function init() { - // Set default display - remaining = configuredDuration; - setInputsFromSeconds(configuredDuration); - updateDisplay(); - enableInputs(true); - updateResetBtnVisibility(); - updatePresetsVisibility(); + setInputsFromSeconds(core.configuredDuration); + render(); renderPresets(); - - // Wire events - toggleBtn.addEventListener('click', togglePanel); - startBtn.addEventListener('click', handleStartPause); - resetBtn.addEventListener('click', reset); - decMinBtn.addEventListener('click', onDec); - incMinBtn.addEventListener('click', onInc); - minInput.addEventListener('focus', () => { activeInput = 'min'; minInput.select(); }); - secInput.addEventListener('focus', () => { activeInput = 'sec'; secInput.select(); }); - minInput.addEventListener('change', () => { validateInputs(); updateAddBtnState(); }); - secInput.addEventListener('change', () => { validateInputs(); updateAddBtnState(); }); - minInput.addEventListener('input', updateAddBtnState); - secInput.addEventListener('input', updateAddBtnState); - presetAddBtn.addEventListener('click', addPreset); + renderToggle(); + + dom.toggleBtn.addEventListener('click', togglePanel); + dom.startBtn.addEventListener('click', onStart); + dom.resetBtn.addEventListener('click', onReset); + dom.decMinBtn.addEventListener('click', onDec); + dom.incMinBtn.addEventListener('click', onInc); + dom.minInput.addEventListener('focus', () => { activeInput = 'min'; dom.minInput.select(); }); + dom.secInput.addEventListener('focus', () => { activeInput = 'sec'; dom.secInput.select(); }); + dom.minInput.addEventListener('change', () => render()); + dom.secInput.addEventListener('change', () => render()); + dom.minInput.addEventListener('input', () => render()); + dom.secInput.addEventListener('input', () => render()); + dom.presetAddBtn.addEventListener('click', onAddPreset); document.addEventListener('click', onDocumentClick); document.addEventListener('keydown', onDocumentKeydown); - updateAddBtnState(); - - // Close panel initially closePanel(); - updateToggleContent(); } function destroy() { @@ -503,5 +347,18 @@ export function createTimer(elements: TimerElements, callbacks: TimerCallbacks = init(); - return { start, pause, reset, destroy, togglePanel, getState: () => state }; + return { + start: onStart, + pause: () => { + if (core.state === 'running') { + if (intervalId !== null) { clearInterval(intervalId); intervalId = null; } + core = pauseTimer(core)!; + render(); + } + }, + reset: onReset, + destroy, + togglePanel, + getState: () => core.state, + }; } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7ad6e64..e32448b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,68 +1,6 @@ import './styles/main.css'; import { initClock } from './components/clock'; -import { createTimer } from './components/timer'; -import type { TimerState } from './components/timer'; +import { initTimer } from './components/timer'; -const clock = initClock(); -clock?.start(); - -// Timer -const timerDisplay = document.getElementById('timer-display'); -const timerFinishTime = document.getElementById('timer-finish-time'); -const timerMinInput = document.getElementById('timer-min') as HTMLInputElement | null; -const timerSecInput = document.getElementById('timer-sec') as HTMLInputElement | null; -const timerStartBtn = document.getElementById('timer-start') as HTMLButtonElement | null; -const timerResetBtn = document.getElementById('timer-reset') as HTMLButtonElement | null; -const timerDecMinBtn = document.getElementById('timer-dec-min') as HTMLButtonElement | null; -const timerIncMinBtn = document.getElementById('timer-inc-min') as HTMLButtonElement | null; -const timerToggleBtn = document.getElementById('timer-toggle') as HTMLButtonElement | null; -const timerPanel = document.getElementById('timer-panel'); -const timerPresetsContainer = document.getElementById('timer-presets'); -const timerPresetsBar = document.getElementById('timer-presets-bar'); -const timerPresetAddBtn = document.getElementById('timer-preset-add') as HTMLButtonElement | null; - -if ( - timerDisplay && - timerFinishTime && - timerMinInput && - timerSecInput && - timerStartBtn && - timerResetBtn && - timerDecMinBtn && - timerIncMinBtn && - timerToggleBtn && - timerPanel && - timerPresetsContainer && - timerPresetsBar && - timerPresetAddBtn -) { - void createTimer( - { - display: timerDisplay, - finishTimeEl: timerFinishTime, - minInput: timerMinInput, - secInput: timerSecInput, - startBtn: timerStartBtn, - resetBtn: timerResetBtn, - decMinBtn: timerDecMinBtn, - incMinBtn: timerIncMinBtn, - toggleBtn: timerToggleBtn, - panel: timerPanel, - presetsContainer: timerPresetsContainer, - presetsBar: timerPresetsBar, - presetAddBtn: timerPresetAddBtn, - }, - { - onStateChange(state: TimerState) { - // Add/remove running class on toggle button for pulse animation - if (state === 'running') { - timerToggleBtn.classList.add('running'); - } else { - timerToggleBtn.classList.remove('running'); - } - }, - }, - ); -} else { - console.error('Could not find all required timer elements.'); -} \ No newline at end of file +initClock()?.start(); +initTimer(); \ No newline at end of file From 3aa6bda8132048e00017338db4080cb584113a61 Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:47:35 -0500 Subject: [PATCH 06/10] docs: update README with current architecture and testing setup --- README.md | 117 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 6347ae3..5701164 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,67 @@ -# [SYSTEM PROMPT] Clock Website Development Assistant - -## 1. AI Directives - -**Persona:** You are an expert pair programmer specializing in modern frontend development. - -**Mission:** Your objective is to assist in developing this portfolio project by writing clean, high-quality code. Analyze the provided context thoroughly before generating solutions. - -**Rules of Engagement:** - -- **Primary Language:** Use **TypeScript** for all new application logic (`.ts` files). Use HTML and CSS for structure and styling as needed. -- **Style:** Prioritize clarity and simplicity. Adhere to the existing code style. -- **Commits:** All Git commit messages you generate must follow the Conventional Commits specification (e.g., `feat:`, `fix:`, `chore:`). -- **Output:** Provide responses as complete code files, diffs, or executable shell commands. - ---- - -## 2. Project Architecture & Codebase Context - -### **Technology Stack:** - -- **TypeScript**: Handles all clock logic and DOM manipulation. -- **Vite**: Serves as the build tool and local dev server. -- **Vercel**: Manages automated hosting and CI/CD. - -### **Core File Analysis:** - -- **`src/index.html`**: - - - Contains the basic structure for the application. - - Key elements include a `
` which holds the `#clock` and `#timezone` displays, and an `#environment-marker`. - - It loads `script.ts` as a module. - -- **`src/script.ts`**: - - - This is the main entry point for the application's logic. - - The `updateClock()` function is the core of the application. It runs every second via `setInterval`. - - **Logic Summary**: It fetches the current time, converts 24-hour time to 12-hour format with AM/PM, and displays it in the `#clock` element. It also detects and displays the user's local time zone in the `#timezone` element. - - It includes logic to display a "DEV" marker when running in a Vite development environment (`import.meta.env.MODE === 'development'`). - -- **`src/style.css`**: - - Implements a dark, centered, "digital clock" aesthetic. - - Uses Flexbox to center the clock vertically and horizontally. - - Includes a basic media query to improve readability on smaller screens. - ---- - -## 3. Development Task List - -_(Please populate this section with your desired tasks.)_ +# Simple Clock + +A clean, dark-themed digital clock with a built-in countdown timer. Built with TypeScript and Vite. + +## Tech Stack + +- **TypeScript** — Application logic and DOM manipulation +- **Vite** — Build tool and dev server +- **Vitest** — Unit testing with jsdom + +## Project Structure + +``` +src/ +├── main.ts # Entry point (4 lines — thin glue) +├── index.html # HTML shell +├── vite-env.d.ts # Vite client types +├── assets/ +│ └── favicon.svg +├── components/ +│ ├── clock.ts # Clock UI — initClock(), pure computeClockTick() +│ └── timer.ts # Timer UI — initTimer(), thin renderer for timer-core +├── styles/ +│ ├── main.css # Import hub +│ ├── variables.css # Design tokens +│ ├── base.css # Reset / body +│ ├── clock.css # Clock styles +│ ├── timer.css # Timer styles +│ ├── animations.css # Keyframes +│ └── responsive.css # Media queries +└── utils/ + ├── time.ts # Time formatting (pure functions) + ├── audio.ts # Web Audio beep (injectable AudioContext) + ├── timer-core.ts # Pure timer state machine (no DOM) + └── __tests__/ + ├── time.test.ts + ├── clock.test.ts # (in components/__tests__/) + └── timer-core.test.ts +``` + +## Architecture + +The codebase is structured for **testability**: + +- **Pure functions** (`timer-core.ts`, `time.ts`, `computeClockTick`) contain all business logic with zero DOM dependencies — fully unit-testable. +- **UI layers** (`initClock`, `initTimer`) are thin wrappers that query the DOM, bind events, and render state from the pure core. +- **Dependency injection** — `playBeep()` accepts an optional `AudioContext`, enabling test mocking. + +## Scripts + +```bash +npm run dev # Start dev server (auto-opens browser) +npm run build # Production build → dist/ +npm test # Run tests once +npm run test:watch # Run tests in watch mode +``` + +## Testing + +```bash +npm test +``` + +Tests cover: +- Time formatting (12h, AM/PM, midnight, noon, duration) +- Clock tick logic (title updates, timezone display, dev marker, timer-active guard) +- Timer state machine (start/pause/reset/tick transitions, input validation, presets) \ No newline at end of file From e48021d73aa048fe9d95be65b790b0dec467ce1f Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 07:55:26 -0500 Subject: [PATCH 07/10] ci: add Prettier, ESLint, and Husky for local code quality enforcement - Add Prettier with .prettierrc config (tabs, single quotes, trailing commas) - Add ESLint with @typescript-eslint for static analysis - Add Husky pre-commit hook with lint-staged - Add format/lint scripts to package.json - Fix lint errors (unused vars, unused imports) - Format entire codebase with Prettier --- .husky/pre-commit | 1 + .prettierrc | 10 + README.md | 3 +- eslint.config.js | 30 + package-lock.json | 5500 +++++++++++++++--------- package.json | 52 +- src/components/__tests__/clock.test.ts | 258 +- src/components/clock.ts | 196 +- src/components/timer.ts | 783 ++-- src/index.html | 88 +- src/main.ts | 2 +- src/styles/animations.css | 38 +- src/styles/base.css | 34 +- src/styles/clock.css | 26 +- src/styles/main.css | 2 +- src/styles/responsive.css | 212 +- src/styles/timer.css | 380 +- src/styles/variables.css | 112 +- src/utils/__tests__/time.test.ts | 164 +- src/utils/__tests__/timer-core.test.ts | 665 +-- src/utils/audio.ts | 39 +- src/utils/time.ts | 43 +- src/utils/timer-core.ts | 305 +- src/vite-env.d.ts | 14 +- tsconfig.json | 36 +- vite.config.ts | 26 +- 26 files changed, 5433 insertions(+), 3586 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a96efb5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 2, + "useTabs": true, + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/README.md b/README.md index 5701164..bb91b46 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ npm test ``` Tests cover: + - Time formatting (12h, AM/PM, midnight, noon, duration) - Clock tick logic (title updates, timezone display, dev marker, timer-active guard) -- Timer state machine (start/pause/reset/tick transitions, input validation, presets) \ No newline at end of file +- Timer state machine (start/pause/reset/tick transitions, input validation, presets) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b46426a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,30 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports' }, + ], + 'no-console': ['warn', { allow: ['error', 'warn'] }], + }, + }, + { + ignores: ['dist/', 'node_modules/', 'eslint.config.js', 'vite.config.ts'], + }, +); diff --git a/package-lock.json b/package-lock.json index 08478ed..e83b289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1951 +1,3553 @@ { - "name": "clock_website", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "clock_website", - "version": "1.0.0", - "license": "ISC", - "devDependencies": { - "jsdom": "^29.1.1", - "typescript": "^5.8.3", - "vite": "^6.3.5", - "vitest": "^4.1.8" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", - "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", - "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.8", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "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/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-string": { - "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" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", - "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.2" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", - "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", - "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - } - } + "name": "clock_website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clock_website", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.4.1", + "husky": "^9.1.7", + "jsdom": "^29.1.1", + "lint-staged": "^17.0.7", + "prettier": "^3.8.3", + "typescript": "^5.8.3", + "typescript-eslint": "^8.60.1", + "vite": "^6.3.5", + "vitest": "^4.1.8" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "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.60.1", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "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/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "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.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "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/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope/node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lint-staged": { + "version": "17.0.7", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", + "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "listr2": "^10.2.1", + "picomatch": "^4.0.4", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "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" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.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/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" + }, + "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/undici": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz", + "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } } diff --git a/package.json b/package.json index 939b829..eda8fda 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,38 @@ { - "name": "clock_website", - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "test": "vitest run", - "test:watch": "vitest" - }, - "license": "ISC", - "devDependencies": { - "jsdom": "^29.1.1", - "typescript": "^5.8.3", - "vite": "^6.3.5", - "vitest": "^4.1.8" - } + "name": "clock_website", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test": "vitest run", + "test:watch": "vitest", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "prepare": "husky" + }, + "lint-staged": { + "*.{ts,js}": [ + "prettier --write", + "eslint --fix" + ], + "*.{json,css,md}": [ + "prettier --write" + ] + }, + "license": "ISC", + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.4.1", + "husky": "^9.1.7", + "jsdom": "^29.1.1", + "lint-staged": "^17.0.7", + "prettier": "^3.8.3", + "typescript": "^5.8.3", + "typescript-eslint": "^8.60.1", + "vite": "^6.3.5", + "vitest": "^4.1.8" + } } diff --git a/src/components/__tests__/clock.test.ts b/src/components/__tests__/clock.test.ts index 9fb389b..e582a9a 100644 --- a/src/components/__tests__/clock.test.ts +++ b/src/components/__tests__/clock.test.ts @@ -3,134 +3,148 @@ import { computeClockTick, msToNextSecond } from '../clock'; import type { ClockTickInput } from '../clock'; function fakeDate(h: number, m: number, s: number, ms = 0): Date { - return new Date(2025, 5, 15, h, m, s, ms); + return new Date(2025, 5, 15, h, m, s, ms); } function baseInput(overrides: Partial = {}): ClockTickInput { - return { - now: fakeDate(10, 30, 45), - lastMinute: null, - lastSecond: null, - isTimerActive: false, - devMode: false, - ...overrides, - }; + return { + now: fakeDate(10, 30, 45), + lastMinute: null, + lastSecond: null, + isTimerActive: false, + devMode: false, + ...overrides, + }; } describe('computeClockTick', () => { - it('returns formatted clock text', () => { - const result = computeClockTick(baseInput({ now: fakeDate(9, 5, 3) })); - expect(result.clockText).toBe('9:05:03 AM'); - }); - - it('formats PM times correctly', () => { - const result = computeClockTick(baseInput({ now: fakeDate(14, 30, 0) })); - expect(result.clockText).toBe('2:30:00 PM'); - }); - - it('formats midnight correctly', () => { - const result = computeClockTick(baseInput({ now: fakeDate(0, 0, 0) })); - expect(result.clockText).toBe('12:00:00 AM'); - }); - - describe('title updates', () => { - it('updates title when minute changes and timer is not active', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 31, 0), - lastMinute: 30, - lastSecond: 59, - isTimerActive: false, - })); - expect(result.titleText).toBe('10:31 AM | Simple Clock'); - }); - - it('does not update title when timer is active', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 31, 0), - lastMinute: 30, - lastSecond: 59, - isTimerActive: true, - })); - expect(result.titleText).toBeNull(); - }); - - it('does not update title when minute has not changed', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 30, 15), - lastMinute: 30, - lastSecond: 14, - isTimerActive: false, - })); - expect(result.titleText).toBeNull(); - }); - - it('updates title on first tick (lastMinute is null)', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 30, 45), - lastMinute: null, - isTimerActive: false, - })); - expect(result.titleText).toBe('10:30 AM | Simple Clock'); - }); - }); - - describe('timezone updates', () => { - it('updates timezone when second changes', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 30, 45), - lastSecond: 44, - })); - expect(result.timezoneText).not.toBeNull(); - }); - - it('does not update timezone when second has not changed', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 30, 45), - lastSecond: 45, - })); - expect(result.timezoneText).toBeNull(); - }); - - it('updates timezone on first tick (lastSecond is null)', () => { - const result = computeClockTick(baseInput({ - now: fakeDate(10, 30, 45), - lastSecond: null, - })); - expect(result.timezoneText).not.toBeNull(); - }); - }); - - describe('environment marker', () => { - it('shows DEV in dev mode', () => { - const result = computeClockTick(baseInput({ devMode: true })); - expect(result.envMarkerText).toBe('DEV'); - }); - - it('shows empty string in production mode', () => { - const result = computeClockTick(baseInput({ devMode: false })); - expect(result.envMarkerText).toBe(''); - }); - }); - - describe('state tracking', () => { - it('returns current minute and second for next tick', () => { - const result = computeClockTick(baseInput({ now: fakeDate(10, 31, 45) })); - expect(result.newLastMinute).toBe(31); - expect(result.newLastSecond).toBe(45); - }); - }); + it('returns formatted clock text', () => { + const result = computeClockTick(baseInput({ now: fakeDate(9, 5, 3) })); + expect(result.clockText).toBe('9:05:03 AM'); + }); + + it('formats PM times correctly', () => { + const result = computeClockTick(baseInput({ now: fakeDate(14, 30, 0) })); + expect(result.clockText).toBe('2:30:00 PM'); + }); + + it('formats midnight correctly', () => { + const result = computeClockTick(baseInput({ now: fakeDate(0, 0, 0) })); + expect(result.clockText).toBe('12:00:00 AM'); + }); + + describe('title updates', () => { + it('updates title when minute changes and timer is not active', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 31, 0), + lastMinute: 30, + lastSecond: 59, + isTimerActive: false, + }), + ); + expect(result.titleText).toBe('10:31 AM | Simple Clock'); + }); + + it('does not update title when timer is active', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 31, 0), + lastMinute: 30, + lastSecond: 59, + isTimerActive: true, + }), + ); + expect(result.titleText).toBeNull(); + }); + + it('does not update title when minute has not changed', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 30, 15), + lastMinute: 30, + lastSecond: 14, + isTimerActive: false, + }), + ); + expect(result.titleText).toBeNull(); + }); + + it('updates title on first tick (lastMinute is null)', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 30, 45), + lastMinute: null, + isTimerActive: false, + }), + ); + expect(result.titleText).toBe('10:30 AM | Simple Clock'); + }); + }); + + describe('timezone updates', () => { + it('updates timezone when second changes', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 30, 45), + lastSecond: 44, + }), + ); + expect(result.timezoneText).not.toBeNull(); + }); + + it('does not update timezone when second has not changed', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 30, 45), + lastSecond: 45, + }), + ); + expect(result.timezoneText).toBeNull(); + }); + + it('updates timezone on first tick (lastSecond is null)', () => { + const result = computeClockTick( + baseInput({ + now: fakeDate(10, 30, 45), + lastSecond: null, + }), + ); + expect(result.timezoneText).not.toBeNull(); + }); + }); + + describe('environment marker', () => { + it('shows DEV in dev mode', () => { + const result = computeClockTick(baseInput({ devMode: true })); + expect(result.envMarkerText).toBe('DEV'); + }); + + it('shows empty string in production mode', () => { + const result = computeClockTick(baseInput({ devMode: false })); + expect(result.envMarkerText).toBe(''); + }); + }); + + describe('state tracking', () => { + it('returns current minute and second for next tick', () => { + const result = computeClockTick(baseInput({ now: fakeDate(10, 31, 45) })); + expect(result.newLastMinute).toBe(31); + expect(result.newLastSecond).toBe(45); + }); + }); }); describe('msToNextSecond', () => { - it('returns 0 when milliseconds is 0', () => { - expect(msToNextSecond(fakeDate(10, 30, 45, 0))).toBe(1000); - }); - - it('returns 1 when milliseconds is 999', () => { - expect(msToNextSecond(fakeDate(10, 30, 45, 999))).toBe(1); - }); - - it('returns 500 when milliseconds is 500', () => { - expect(msToNextSecond(fakeDate(10, 30, 45, 500))).toBe(500); - }); -}); \ No newline at end of file + it('returns 0 when milliseconds is 0', () => { + expect(msToNextSecond(fakeDate(10, 30, 45, 0))).toBe(1000); + }); + + it('returns 1 when milliseconds is 999', () => { + expect(msToNextSecond(fakeDate(10, 30, 45, 999))).toBe(1); + }); + + it('returns 500 when milliseconds is 500', () => { + expect(msToNextSecond(fakeDate(10, 30, 45, 500))).toBe(500); + }); +}); diff --git a/src/components/clock.ts b/src/components/clock.ts index 687568f..ec9102a 100644 --- a/src/components/clock.ts +++ b/src/components/clock.ts @@ -3,121 +3,123 @@ import { formatTime, formatTimeForTitle } from '../utils/time'; // ---------- Pure logic (testable without DOM) ---------- export interface ClockTickInput { - now: Date; - lastMinute: number | null; - lastSecond: number | null; - isTimerActive: boolean; - devMode: boolean; + now: Date; + lastMinute: number | null; + lastSecond: number | null; + isTimerActive: boolean; + devMode: boolean; } export interface ClockTickResult { - clockText: string; - timezoneText: string | null; - envMarkerText: string; - titleText: string | null; - newLastMinute: number; - newLastSecond: number; + clockText: string; + timezoneText: string | null; + envMarkerText: string; + titleText: string | null; + newLastMinute: number; + newLastSecond: number; } -const cachedTimezone = new Intl.DateTimeFormat().resolvedOptions().timeZone.replace('_', ' '); +const cachedTimezone = new Intl.DateTimeFormat() + .resolvedOptions() + .timeZone.replace('_', ' '); /** * Pure function: given the current state, returns what the clock should display. */ export function computeClockTick(input: ClockTickInput): ClockTickResult { - const { now, lastMinute, lastSecond, isTimerActive, devMode } = input; - const currentSecond = now.getSeconds(); - const currentMinute = now.getMinutes(); - - let titleText: string | null = null; - let timezoneText: string | null = null; - - if (currentMinute !== lastMinute && !isTimerActive) { - titleText = `${formatTimeForTitle(now)} | Simple Clock`; - } - - if (currentSecond !== lastSecond) { - timezoneText = cachedTimezone; - } - - return { - clockText: formatTime(now), - timezoneText, - envMarkerText: devMode ? 'DEV' : '', - titleText, - newLastMinute: currentMinute, - newLastSecond: currentSecond, - }; + const { now, lastMinute, lastSecond, isTimerActive, devMode } = input; + const currentSecond = now.getSeconds(); + const currentMinute = now.getMinutes(); + + let titleText: string | null = null; + let timezoneText: string | null = null; + + if (currentMinute !== lastMinute && !isTimerActive) { + titleText = `${formatTimeForTitle(now)} | Simple Clock`; + } + + if (currentSecond !== lastSecond) { + timezoneText = cachedTimezone; + } + + return { + clockText: formatTime(now), + timezoneText, + envMarkerText: devMode ? 'DEV' : '', + titleText, + newLastMinute: currentMinute, + newLastSecond: currentSecond, + }; } /** * Returns milliseconds until the next whole second boundary. */ export function msToNextSecond(now: Date): number { - return 1000 - now.getMilliseconds(); + return 1000 - now.getMilliseconds(); } // ---------- DOM wiring ---------- export function initClock(): { start: () => void; stop: () => void } | null { - const clockEl = document.getElementById('clock'); - const timezoneEl = document.getElementById('timezone'); - const envMarkerEl = document.getElementById('environment-marker'); - - if (!clockEl || !timezoneEl || !envMarkerEl) { - console.error('Could not find all required clock elements.'); - return null; - } - - // Non-null after guard - const clock = clockEl; - const timezone = timezoneEl; - const envMarker = envMarkerEl; - - let lastMinute: number | null = null; - let lastSecond: number | null = null; - let intervalId: number | null = null; - const isDevMode = import.meta.env.MODE === 'development'; - - function tick() { - const now = new Date(); - const result = computeClockTick({ - now, - lastMinute, - lastSecond, - isTimerActive: document.body.hasAttribute('data-timer-active'), - devMode: isDevMode, - }); - - lastMinute = result.newLastMinute; - lastSecond = result.newLastSecond; - - if (result.titleText !== null) { - document.title = result.titleText; - } - - if (result.timezoneText !== null) { - timezone.textContent = result.timezoneText; - } - - clock.textContent = result.clockText; - envMarker.textContent = result.envMarkerText; - } - - function start() { - const ms = msToNextSecond(new Date()); - setTimeout(() => { - tick(); - intervalId = window.setInterval(tick, 1000); - }, ms); - } - - function stop() { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - } - - return { start, stop }; -} \ No newline at end of file + const clockEl = document.getElementById('clock'); + const timezoneEl = document.getElementById('timezone'); + const envMarkerEl = document.getElementById('environment-marker'); + + if (!clockEl || !timezoneEl || !envMarkerEl) { + console.error('Could not find all required clock elements.'); + return null; + } + + // Non-null after guard + const clock = clockEl; + const timezone = timezoneEl; + const envMarker = envMarkerEl; + + let lastMinute: number | null = null; + let lastSecond: number | null = null; + let intervalId: number | null = null; + const isDevMode = import.meta.env.MODE === 'development'; + + function tick() { + const now = new Date(); + const result = computeClockTick({ + now, + lastMinute, + lastSecond, + isTimerActive: document.body.hasAttribute('data-timer-active'), + devMode: isDevMode, + }); + + lastMinute = result.newLastMinute; + lastSecond = result.newLastSecond; + + if (result.titleText !== null) { + document.title = result.titleText; + } + + if (result.timezoneText !== null) { + timezone.textContent = result.timezoneText; + } + + clock.textContent = result.clockText; + envMarker.textContent = result.envMarkerText; + } + + function start() { + const ms = msToNextSecond(new Date()); + setTimeout(() => { + tick(); + intervalId = window.setInterval(tick, 1000); + }, ms); + } + + function stop() { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + } + + return { start, stop }; +} diff --git a/src/components/timer.ts b/src/components/timer.ts index 1194d1d..9cb8499 100644 --- a/src/components/timer.ts +++ b/src/components/timer.ts @@ -1,364 +1,445 @@ import { formatDuration, formatFinishTime } from '../utils/time'; import { playBeep } from '../utils/audio'; import { - createTimerCore, - startTimer, - pauseTimer, - resetTimer, - tickTimer, - applyPreset, - addUserPreset, - removeUserPreset, - parseInputSeconds, - secondsToInputs, - hasValidInput, - decInputs, - incInputs, - parsePresets, - formatPresetLabel, - isResetVisible, - isPresetsHidden, - isInputsEnabled, - isAddBtnInactive, + createTimerCore, + startTimer, + pauseTimer, + resetTimer, + tickTimer, + applyPreset, + addUserPreset, + removeUserPreset, + parseInputSeconds, + secondsToInputs, + hasValidInput, + decInputs, + incInputs, + parsePresets, + formatPresetLabel, + isResetVisible, + isPresetsHidden, + isInputsEnabled, + isAddBtnInactive, } from '../utils/timer-core'; import type { TimerState } from '../utils/timer-core'; export type { TimerState }; export interface TimerCallbacks { - onStateChange?: (state: TimerState) => void; + onStateChange?: (state: TimerState) => void; } const STORAGE_KEY = 'timer-presets'; -export function initTimer(callbacks: TimerCallbacks = {}) { - // ---------- DOM lookups (with type narrowing guard) ---------- - - const display = document.getElementById('timer-display'); - const finishTimeEl = document.getElementById('timer-finish-time'); - const minInput = document.getElementById('timer-min') as HTMLInputElement | null; - const secInput = document.getElementById('timer-sec') as HTMLInputElement | null; - const startBtn = document.getElementById('timer-start') as HTMLButtonElement | null; - const resetBtn = document.getElementById('timer-reset') as HTMLButtonElement | null; - const decMinBtn = document.getElementById('timer-dec-min') as HTMLButtonElement | null; - const incMinBtn = document.getElementById('timer-inc-min') as HTMLButtonElement | null; - const toggleBtn = document.getElementById('timer-toggle') as HTMLButtonElement | null; - const panel = document.getElementById('timer-panel'); - const presetsContainer = document.getElementById('timer-presets'); - const presetsBar = document.getElementById('timer-presets-bar'); - const presetAddBtn = document.getElementById('timer-preset-add') as HTMLButtonElement | null; - - if (!display || !finishTimeEl || !minInput || !secInput || !startBtn || !resetBtn || !decMinBtn || !incMinBtn || !toggleBtn || !panel || !presetsContainer || !presetsBar || !presetAddBtn) { - console.error('Could not find all required timer elements.'); - return; - } - - // Narrow to non-null - const dom = { - display, finishTimeEl, minInput, secInput, startBtn, resetBtn, - decMinBtn, incMinBtn, toggleBtn, panel, presetsContainer, presetsBar, presetAddBtn, - }; - - // ---------- Core state ---------- - - const presets = parsePresets(localStorage.getItem(STORAGE_KEY)); - let core = createTimerCore(presets); - let intervalId: number | null = null; - let isPanelOpen = false; - let activeInput: 'min' | 'sec' = 'min'; - - // ---------- Persistence ---------- - - function savePresets() { - localStorage.setItem(STORAGE_KEY, JSON.stringify(core.presets)); - } - - // ---------- Render ---------- - - function render() { - const { display, finishTimeEl, startBtn, resetBtn, minInput, secInput, decMinBtn, incMinBtn, presetsContainer, presetsBar, presetAddBtn, toggleBtn } = dom; - - display.textContent = formatDuration(core.remaining); - - // Finish time - if (core.state === 'running' && core.remaining > 0 && core.finishTimestamp !== null) { - finishTimeEl.textContent = `→ ${formatFinishTime(core.remaining)}`; - finishTimeEl.classList.add('visible'); - } else { - finishTimeEl.textContent = ''; - finishTimeEl.classList.remove('visible'); - } - - // Toggle button - renderToggle(); - - // Title - document.title = `${core.state === 'idle' || core.state === 'finished' ? '' : formatDuration(core.remaining) + ' | '}Simple Clock`; - - // Body attribute - const isActive = core.state !== 'idle' && core.state !== 'finished'; - document.body.toggleAttribute('data-timer-active', isActive); - - // Buttons - startBtn.textContent = core.state === 'running' ? '⏸ Pause' : core.state === 'paused' ? '▶ Resume' : '▶ Start'; - resetBtn.style.display = isResetVisible(core.state) ? '' : 'none'; - - // Inputs - const inputsEnabled = isInputsEnabled(core.state); - minInput.disabled = !inputsEnabled; - secInput.disabled = !inputsEnabled; - decMinBtn.disabled = !inputsEnabled; - incMinBtn.disabled = !inputsEnabled; - - // Presets visibility - const hidePresets = isPresetsHidden(core.state); - presetsContainer.style.display = hidePresets ? 'none' : ''; - decMinBtn.style.display = hidePresets ? 'none' : ''; - incMinBtn.style.display = hidePresets ? 'none' : ''; - - // Add button - if (!hidePresets) { - const total = parseInputSeconds(minInput.value, secInput.value); - if (core.presets.length >= 10) { - presetsBar.style.display = 'none'; - } else { - presetsBar.style.display = ''; - presetAddBtn.classList.toggle('inactive', isAddBtnInactive(core.presets, total)); - } - } else { - presetsBar.style.display = 'none'; - } - } - - function renderToggle() { - const { toggleBtn } = dom; - if (isPanelOpen) { - toggleBtn.textContent = '⏱'; - } else if (core.state === 'running' && core.remaining > 0 && core.finishTimestamp !== null) { - toggleBtn.textContent = formatFinishTime(core.remaining); - } else if (core.state !== 'idle') { - toggleBtn.textContent = formatDuration(core.remaining); - } else { - toggleBtn.textContent = '⏱'; - } - } - - function setInputsFromSeconds(totalSec: number) { - const { mins, secs } = secondsToInputs(totalSec); - dom.minInput.value = String(mins); - dom.secInput.value = String(secs); - } - - function renderPresets() { - const { presetsContainer } = dom; - presetsContainer.innerHTML = ''; - core.presets.forEach((seconds, index) => { - const row = document.createElement('div'); - row.className = 'preset-row'; - row.addEventListener('click', () => { - core = applyPreset(core, seconds); - setInputsFromSeconds(core.remaining); - render(); - }); - - const label = document.createElement('span'); - label.className = 'preset-row-label'; - label.textContent = formatPresetLabel(seconds); - - const removeBtn = document.createElement('button'); - removeBtn.className = 'preset-row-remove'; - removeBtn.textContent = '×'; - removeBtn.title = 'Remove preset'; - removeBtn.addEventListener('click', (e) => { - e.stopPropagation(); - core = removeUserPreset(core, index); - savePresets(); - renderPresets(); - render(); - }); - - row.appendChild(label); - row.appendChild(removeBtn); - presetsContainer.appendChild(row); - }); - } - - // ---------- Core timer actions ---------- - - function runCountdown() { - intervalId = window.setInterval(() => { - core = tickTimer(core); - render(); - - if (core.state === 'finished') { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - document.body.classList.add('timer-finished'); - playBeep(); - let beepCount = 0; - const beepInterval = window.setInterval(() => { - playBeep(); - beepCount++; - if (beepCount >= 2) { - clearInterval(beepInterval); - } - }, 700); - } - }, 1000); - } - - function msToNextSecond(): number { - return 1000 - new Date().getMilliseconds(); - } - - function onStart() { - if (core.state === 'running') { - if (intervalId === null) return; - clearInterval(intervalId); - intervalId = null; - core = pauseTimer(core)!; - render(); - return; - } - - if (core.state === 'idle' || core.state === 'finished') { - if (!hasValidInput(dom.minInput.value, dom.secInput.value)) return; - const duration = parseInputSeconds(dom.minInput.value, dom.secInput.value); - core = { ...core, remaining: duration, configuredDuration: duration }; - } - - const next = startTimer(core); - if (!next) return; - core = next; - render(); - - setTimeout(() => { - if (core.state !== 'running') return; - runCountdown(); - }, msToNextSecond()); - } - - function onReset() { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - core = resetTimer(core); - setInputsFromSeconds(core.configuredDuration); - document.body.classList.remove('timer-finished'); - render(); - } - - function onDec() { - const mins = parseInt(dom.minInput.value, 10) || 0; - const secs = parseInt(dom.secInput.value, 10) || 0; - const result = decInputs(mins, secs, activeInput); - dom.minInput.value = String(result.mins); - dom.secInput.value = String(result.secs); - render(); - } - - function onInc() { - const mins = parseInt(dom.minInput.value, 10) || 0; - const secs = parseInt(dom.secInput.value, 10) || 0; - const result = incInputs(mins, secs, activeInput); - dom.minInput.value = String(result.mins); - dom.secInput.value = String(result.secs); - render(); - } - - function onAddPreset() { - const seconds = parseInputSeconds(dom.minInput.value, dom.secInput.value); - core = addUserPreset(core, seconds); - savePresets(); - renderPresets(); - render(); - } - - // ---------- Panel toggle ---------- - - function openPanel() { - isPanelOpen = true; - dom.panel.removeAttribute('hidden'); - dom.toggleBtn.classList.add('active'); - renderToggle(); - } - - function closePanel() { - isPanelOpen = false; - dom.panel.setAttribute('hidden', ''); - dom.toggleBtn.classList.remove('active'); - renderToggle(); - } - - function togglePanel() { - if (isPanelOpen) closePanel(); - else openPanel(); - } - - function onDocumentClick(e: MouseEvent) { - const target = e.target as Node; - const wrapper = document.getElementById('timer-wrapper'); - if (isPanelOpen && wrapper && !wrapper.contains(target)) { - closePanel(); - } - } - - function onDocumentKeydown(e: KeyboardEvent) { - if (e.key === 'Escape' && isPanelOpen) closePanel(); - } - - // ---------- Init ---------- - - function init() { - setInputsFromSeconds(core.configuredDuration); - render(); - renderPresets(); - renderToggle(); - - dom.toggleBtn.addEventListener('click', togglePanel); - dom.startBtn.addEventListener('click', onStart); - dom.resetBtn.addEventListener('click', onReset); - dom.decMinBtn.addEventListener('click', onDec); - dom.incMinBtn.addEventListener('click', onInc); - dom.minInput.addEventListener('focus', () => { activeInput = 'min'; dom.minInput.select(); }); - dom.secInput.addEventListener('focus', () => { activeInput = 'sec'; dom.secInput.select(); }); - dom.minInput.addEventListener('change', () => render()); - dom.secInput.addEventListener('change', () => render()); - dom.minInput.addEventListener('input', () => render()); - dom.secInput.addEventListener('input', () => render()); - dom.presetAddBtn.addEventListener('click', onAddPreset); - document.addEventListener('click', onDocumentClick); - document.addEventListener('keydown', onDocumentKeydown); - - closePanel(); - } - - function destroy() { - if (intervalId !== null) { - clearInterval(intervalId); - intervalId = null; - } - document.removeEventListener('click', onDocumentClick); - document.removeEventListener('keydown', onDocumentKeydown); - } - - init(); - - return { - start: onStart, - pause: () => { - if (core.state === 'running') { - if (intervalId !== null) { clearInterval(intervalId); intervalId = null; } - core = pauseTimer(core)!; - render(); - } - }, - reset: onReset, - destroy, - togglePanel, - getState: () => core.state, - }; -} \ No newline at end of file +export function initTimer(_callbacks: TimerCallbacks = {}) { + // ---------- DOM lookups (with type narrowing guard) ---------- + + const display = document.getElementById('timer-display'); + const finishTimeEl = document.getElementById('timer-finish-time'); + const minInput = document.getElementById( + 'timer-min', + ) as HTMLInputElement | null; + const secInput = document.getElementById( + 'timer-sec', + ) as HTMLInputElement | null; + const startBtn = document.getElementById( + 'timer-start', + ) as HTMLButtonElement | null; + const resetBtn = document.getElementById( + 'timer-reset', + ) as HTMLButtonElement | null; + const decMinBtn = document.getElementById( + 'timer-dec-min', + ) as HTMLButtonElement | null; + const incMinBtn = document.getElementById( + 'timer-inc-min', + ) as HTMLButtonElement | null; + const toggleBtn = document.getElementById( + 'timer-toggle', + ) as HTMLButtonElement | null; + const panel = document.getElementById('timer-panel'); + const presetsContainer = document.getElementById('timer-presets'); + const presetsBar = document.getElementById('timer-presets-bar'); + const presetAddBtn = document.getElementById( + 'timer-preset-add', + ) as HTMLButtonElement | null; + + if ( + !display || + !finishTimeEl || + !minInput || + !secInput || + !startBtn || + !resetBtn || + !decMinBtn || + !incMinBtn || + !toggleBtn || + !panel || + !presetsContainer || + !presetsBar || + !presetAddBtn + ) { + console.error('Could not find all required timer elements.'); + return; + } + + // Narrow to non-null + const dom = { + display, + finishTimeEl, + minInput, + secInput, + startBtn, + resetBtn, + decMinBtn, + incMinBtn, + toggleBtn, + panel, + presetsContainer, + presetsBar, + presetAddBtn, + }; + + // ---------- Core state ---------- + + const presets = parsePresets(localStorage.getItem(STORAGE_KEY)); + let core = createTimerCore(presets); + let intervalId: number | null = null; + let isPanelOpen = false; + let activeInput: 'min' | 'sec' = 'min'; + + // ---------- Persistence ---------- + + function savePresets() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(core.presets)); + } + + // ---------- Render ---------- + + function render() { + const { + display, + finishTimeEl, + startBtn, + resetBtn, + minInput, + secInput, + decMinBtn, + incMinBtn, + presetsContainer, + presetsBar, + presetAddBtn, + } = dom; + + display.textContent = formatDuration(core.remaining); + + // Finish time + if ( + core.state === 'running' && + core.remaining > 0 && + core.finishTimestamp !== null + ) { + finishTimeEl.textContent = `→ ${formatFinishTime(core.remaining)}`; + finishTimeEl.classList.add('visible'); + } else { + finishTimeEl.textContent = ''; + finishTimeEl.classList.remove('visible'); + } + + // Toggle button + renderToggle(); + + // Title + document.title = `${core.state === 'idle' || core.state === 'finished' ? '' : formatDuration(core.remaining) + ' | '}Simple Clock`; + + // Body attribute + const isActive = core.state !== 'idle' && core.state !== 'finished'; + document.body.toggleAttribute('data-timer-active', isActive); + + // Buttons + startBtn.textContent = + core.state === 'running' + ? '⏸ Pause' + : core.state === 'paused' + ? '▶ Resume' + : '▶ Start'; + resetBtn.style.display = isResetVisible(core.state) ? '' : 'none'; + + // Inputs + const inputsEnabled = isInputsEnabled(core.state); + minInput.disabled = !inputsEnabled; + secInput.disabled = !inputsEnabled; + decMinBtn.disabled = !inputsEnabled; + incMinBtn.disabled = !inputsEnabled; + + // Presets visibility + const hidePresets = isPresetsHidden(core.state); + presetsContainer.style.display = hidePresets ? 'none' : ''; + decMinBtn.style.display = hidePresets ? 'none' : ''; + incMinBtn.style.display = hidePresets ? 'none' : ''; + + // Add button + if (!hidePresets) { + const total = parseInputSeconds(minInput.value, secInput.value); + if (core.presets.length >= 10) { + presetsBar.style.display = 'none'; + } else { + presetsBar.style.display = ''; + presetAddBtn.classList.toggle( + 'inactive', + isAddBtnInactive(core.presets, total), + ); + } + } else { + presetsBar.style.display = 'none'; + } + } + + function renderToggle() { + const { toggleBtn } = dom; + if (isPanelOpen) { + toggleBtn.textContent = '⏱'; + } else if ( + core.state === 'running' && + core.remaining > 0 && + core.finishTimestamp !== null + ) { + toggleBtn.textContent = formatFinishTime(core.remaining); + } else if (core.state !== 'idle') { + toggleBtn.textContent = formatDuration(core.remaining); + } else { + toggleBtn.textContent = '⏱'; + } + } + + function setInputsFromSeconds(totalSec: number) { + const { mins, secs } = secondsToInputs(totalSec); + dom.minInput.value = String(mins); + dom.secInput.value = String(secs); + } + + function renderPresets() { + const { presetsContainer } = dom; + presetsContainer.innerHTML = ''; + core.presets.forEach((seconds, index) => { + const row = document.createElement('div'); + row.className = 'preset-row'; + row.addEventListener('click', () => { + core = applyPreset(core, seconds); + setInputsFromSeconds(core.remaining); + render(); + }); + + const label = document.createElement('span'); + label.className = 'preset-row-label'; + label.textContent = formatPresetLabel(seconds); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'preset-row-remove'; + removeBtn.textContent = '×'; + removeBtn.title = 'Remove preset'; + removeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + core = removeUserPreset(core, index); + savePresets(); + renderPresets(); + render(); + }); + + row.appendChild(label); + row.appendChild(removeBtn); + presetsContainer.appendChild(row); + }); + } + + // ---------- Core timer actions ---------- + + function runCountdown() { + intervalId = window.setInterval(() => { + core = tickTimer(core); + render(); + + if (core.state === 'finished') { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + document.body.classList.add('timer-finished'); + playBeep(); + let beepCount = 0; + const beepInterval = window.setInterval(() => { + playBeep(); + beepCount++; + if (beepCount >= 2) { + clearInterval(beepInterval); + } + }, 700); + } + }, 1000); + } + + function msToNextSecond(): number { + return 1000 - new Date().getMilliseconds(); + } + + function onStart() { + if (core.state === 'running') { + if (intervalId === null) return; + clearInterval(intervalId); + intervalId = null; + core = pauseTimer(core)!; + render(); + return; + } + + if (core.state === 'idle' || core.state === 'finished') { + if (!hasValidInput(dom.minInput.value, dom.secInput.value)) return; + const duration = parseInputSeconds( + dom.minInput.value, + dom.secInput.value, + ); + core = { ...core, remaining: duration, configuredDuration: duration }; + } + + const next = startTimer(core); + if (!next) return; + core = next; + render(); + + setTimeout(() => { + if (core.state !== 'running') return; + runCountdown(); + }, msToNextSecond()); + } + + function onReset() { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + core = resetTimer(core); + setInputsFromSeconds(core.configuredDuration); + document.body.classList.remove('timer-finished'); + render(); + } + + function onDec() { + const mins = parseInt(dom.minInput.value, 10) || 0; + const secs = parseInt(dom.secInput.value, 10) || 0; + const result = decInputs(mins, secs, activeInput); + dom.minInput.value = String(result.mins); + dom.secInput.value = String(result.secs); + render(); + } + + function onInc() { + const mins = parseInt(dom.minInput.value, 10) || 0; + const secs = parseInt(dom.secInput.value, 10) || 0; + const result = incInputs(mins, secs, activeInput); + dom.minInput.value = String(result.mins); + dom.secInput.value = String(result.secs); + render(); + } + + function onAddPreset() { + const seconds = parseInputSeconds(dom.minInput.value, dom.secInput.value); + core = addUserPreset(core, seconds); + savePresets(); + renderPresets(); + render(); + } + + // ---------- Panel toggle ---------- + + function openPanel() { + isPanelOpen = true; + dom.panel.removeAttribute('hidden'); + dom.toggleBtn.classList.add('active'); + renderToggle(); + } + + function closePanel() { + isPanelOpen = false; + dom.panel.setAttribute('hidden', ''); + dom.toggleBtn.classList.remove('active'); + renderToggle(); + } + + function togglePanel() { + if (isPanelOpen) closePanel(); + else openPanel(); + } + + function onDocumentClick(e: MouseEvent) { + const target = e.target as Node; + const wrapper = document.getElementById('timer-wrapper'); + if (isPanelOpen && wrapper && !wrapper.contains(target)) { + closePanel(); + } + } + + function onDocumentKeydown(e: KeyboardEvent) { + if (e.key === 'Escape' && isPanelOpen) closePanel(); + } + + // ---------- Init ---------- + + function init() { + setInputsFromSeconds(core.configuredDuration); + render(); + renderPresets(); + renderToggle(); + + dom.toggleBtn.addEventListener('click', togglePanel); + dom.startBtn.addEventListener('click', onStart); + dom.resetBtn.addEventListener('click', onReset); + dom.decMinBtn.addEventListener('click', onDec); + dom.incMinBtn.addEventListener('click', onInc); + dom.minInput.addEventListener('focus', () => { + activeInput = 'min'; + dom.minInput.select(); + }); + dom.secInput.addEventListener('focus', () => { + activeInput = 'sec'; + dom.secInput.select(); + }); + dom.minInput.addEventListener('change', () => render()); + dom.secInput.addEventListener('change', () => render()); + dom.minInput.addEventListener('input', () => render()); + dom.secInput.addEventListener('input', () => render()); + dom.presetAddBtn.addEventListener('click', onAddPreset); + document.addEventListener('click', onDocumentClick); + document.addEventListener('keydown', onDocumentKeydown); + + closePanel(); + } + + function destroy() { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + document.removeEventListener('click', onDocumentClick); + document.removeEventListener('keydown', onDocumentKeydown); + } + + init(); + + return { + start: onStart, + pause: () => { + if (core.state === 'running') { + if (intervalId !== null) { + clearInterval(intervalId); + intervalId = null; + } + core = pauseTimer(core)!; + render(); + } + }, + reset: onReset, + destroy, + togglePanel, + getState: () => core.state, + }; +} diff --git a/src/index.html b/src/index.html index 5914c8f..14930c0 100644 --- a/src/index.html +++ b/src/index.html @@ -1,43 +1,51 @@ - + - - - - Simple Clock - - - -
-
-
+ + + + Simple Clock + + + +
+
+
-
- - -
-
-
+
+ + +
+
+
- - - \ No newline at end of file + + + diff --git a/src/main.ts b/src/main.ts index e32448b..8bf487c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,4 +3,4 @@ import { initClock } from './components/clock'; import { initTimer } from './components/timer'; initClock()?.start(); -initTimer(); \ No newline at end of file +initTimer(); diff --git a/src/styles/animations.css b/src/styles/animations.css index e8b66b2..e3ddd7a 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -5,28 +5,38 @@ /* Timer pulse — toggle button glow when timer is running */ @keyframes timer-pulse { - 0%, 100% { box-shadow: 0 0 5px var(--color-primary); } - 50% { box-shadow: 0 0 20px var(--color-primary); } + 0%, + 100% { + box-shadow: 0 0 5px var(--color-primary); + } + 50% { + box-shadow: 0 0 20px var(--color-primary); + } } /* Panel fade-in — timer panel opens */ @keyframes timer-fade-in { - from { - opacity: 0; - transform: translateY(-6px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* Flash — clock fades in/out when timer finishes */ @keyframes timer-flash { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.2; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.2; + } } body.timer-finished #clock-container { - animation: timer-flash 0.5s ease-in-out 4; -} \ No newline at end of file + animation: timer-flash 0.5s ease-in-out 4; +} diff --git a/src/styles/base.css b/src/styles/base.css index df8da3b..9b2e703 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -4,25 +4,25 @@ ============================================================ */ body { - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - margin: 0; - background-color: var(--color-bg); - color: var(--color-primary); - font-family: var(--font-family); - overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + margin: 0; + background-color: var(--color-bg); + color: var(--color-primary); + font-family: var(--font-family); + overflow: hidden; } /* ---------- Environment Marker ---------- */ #environment-marker { - position: fixed; - bottom: var(--space-3xl); - right: var(--space-3xl); - font-size: var(--font-size-env); - color: var(--color-primary); - text-shadow: var(--glow-md); - z-index: 1000; -} \ No newline at end of file + position: fixed; + bottom: var(--space-3xl); + right: var(--space-3xl); + font-size: var(--font-size-env); + color: var(--color-primary); + text-shadow: var(--glow-md); + z-index: 1000; +} diff --git a/src/styles/clock.css b/src/styles/clock.css index 253b807..d2fa3ed 100644 --- a/src/styles/clock.css +++ b/src/styles/clock.css @@ -4,22 +4,22 @@ ============================================================ */ #clock-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-3xl); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-3xl); } #clock { - font-size: var(--font-size-display); - text-align: center; - font-weight: var(--font-weight-bold); + font-size: var(--font-size-display); + text-align: center; + font-weight: var(--font-weight-bold); } #timezone { - font-size: var(--font-size-timezone); - text-align: center; - margin-top: var(--space-lg); - color: var(--color-primary); -} \ No newline at end of file + font-size: var(--font-size-timezone); + text-align: center; + margin-top: var(--space-lg); + color: var(--color-primary); +} diff --git a/src/styles/main.css b/src/styles/main.css index 3b7d802..d185a67 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -22,4 +22,4 @@ @import './animations.css'; /* --- Responsive (must come last to override above) --- */ -@import './responsive.css'; \ No newline at end of file +@import './responsive.css'; diff --git a/src/styles/responsive.css b/src/styles/responsive.css index 42151e6..1b8af81 100644 --- a/src/styles/responsive.css +++ b/src/styles/responsive.css @@ -8,121 +8,121 @@ /* ---------- Tablet / Small screens (≤600px) ---------- */ @media (max-width: 600px) { - #clock-container { - gap: var(--space-xl); - } - - #clock { - font-size: 4em; - } - - #timezone { - font-size: var(--font-size-body); - } - - #timer-toggle { - font-size: 1.4em; - padding: 3px var(--space-lg); - } - - #timer-display { - font-size: 2em; - } - - #timer-controls input[type="number"] { - width: 2em; - font-size: var(--font-size-body); - } - - #timer-controls button { - font-size: var(--font-size-body); - padding: 3px var(--space-md); - } - - #timer-panel { - padding: var(--space-lg) var(--space-xl); - max-width: calc(100vw - 24px); - } - - #timer-actions button { - font-size: 0.9em; - padding: 5px var(--space-xl); - } + #clock-container { + gap: var(--space-xl); + } + + #clock { + font-size: 4em; + } + + #timezone { + font-size: var(--font-size-body); + } + + #timer-toggle { + font-size: 1.4em; + padding: 3px var(--space-lg); + } + + #timer-display { + font-size: 2em; + } + + #timer-controls input[type='number'] { + width: 2em; + font-size: var(--font-size-body); + } + + #timer-controls button { + font-size: var(--font-size-body); + padding: 3px var(--space-md); + } + + #timer-panel { + padding: var(--space-lg) var(--space-xl); + max-width: calc(100vw - 24px); + } + + #timer-actions button { + font-size: 0.9em; + padding: 5px var(--space-xl); + } } /* ---------- Small phone (≤400px) ---------- */ @media (max-width: 400px) { - #clock { - font-size: 3em; - } + #clock { + font-size: 3em; + } - #timezone { - font-size: 0.85em; - } + #timezone { + font-size: 0.85em; + } - #timer-panel { - padding: var(--space-md) var(--space-lg); - } + #timer-panel { + padding: var(--space-md) var(--space-lg); + } } /* ---------- Landscape on phones (short viewport) ---------- */ @media (max-height: 500px) and (orientation: landscape) { - body { - overflow: auto; - min-height: 100vh; - } - - #clock-container { - gap: var(--space-sm); - } - - #clock { - font-size: 2.5em; - } - - #timezone { - font-size: 0.8em; - margin-top: var(--space-xs); - } - - #timer-wrapper { - gap: var(--space-xs); - } - - #timer-toggle { - font-size: 1.1em; - padding: var(--space-2xs) var(--space-md); - } - - #timer-display { - font-size: 1.4em; - } - - #timer-panel { - padding: var(--space-sm) var(--space-lg); - gap: var(--space-sm); - max-width: calc(100vw - 16px); - } - - #timer-controls input[type="number"] { - width: 1.8em; - font-size: 0.85em; - padding: var(--space-2xs) var(--space-xs); - } - - #timer-controls button { - font-size: 0.85em; - padding: var(--space-2xs) var(--space-sm); - } - - .timer-colon { - font-size: 0.9em; - } - - #timer-actions button { - font-size: 0.8em; - padding: var(--space-xs) var(--space-lg); - } -} \ No newline at end of file + body { + overflow: auto; + min-height: 100vh; + } + + #clock-container { + gap: var(--space-sm); + } + + #clock { + font-size: 2.5em; + } + + #timezone { + font-size: 0.8em; + margin-top: var(--space-xs); + } + + #timer-wrapper { + gap: var(--space-xs); + } + + #timer-toggle { + font-size: 1.1em; + padding: var(--space-2xs) var(--space-md); + } + + #timer-display { + font-size: 1.4em; + } + + #timer-panel { + padding: var(--space-sm) var(--space-lg); + gap: var(--space-sm); + max-width: calc(100vw - 16px); + } + + #timer-controls input[type='number'] { + width: 1.8em; + font-size: 0.85em; + padding: var(--space-2xs) var(--space-xs); + } + + #timer-controls button { + font-size: 0.85em; + padding: var(--space-2xs) var(--space-sm); + } + + .timer-colon { + font-size: 0.9em; + } + + #timer-actions button { + font-size: 0.8em; + padding: var(--space-xs) var(--space-lg); + } +} diff --git a/src/styles/timer.css b/src/styles/timer.css index 76654b3..5d370a8 100644 --- a/src/styles/timer.css +++ b/src/styles/timer.css @@ -7,289 +7,295 @@ /* ---------- Wrapper & Toggle ---------- */ #timer-wrapper { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-md); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-md); } #timer-toggle { - background: none; - border: var(--border-width-strong) solid var(--color-primary); - color: var(--color-primary); - font-size: var(--font-size-toggle); - cursor: pointer; - border-radius: var(--border-radius-md); - padding: var(--space-xs) var(--space-xl); - transition: background-color var(--transition-normal), - box-shadow var(--transition-normal); - line-height: 1; + background: none; + border: var(--border-width-strong) solid var(--color-primary); + color: var(--color-primary); + font-size: var(--font-size-toggle); + cursor: pointer; + border-radius: var(--border-radius-md); + padding: var(--space-xs) var(--space-xl); + transition: + background-color var(--transition-normal), + box-shadow var(--transition-normal); + line-height: 1; } #timer-toggle:hover, #timer-toggle:focus-visible { - background-color: var(--color-primary-soft); - box-shadow: var(--glow-md); + background-color: var(--color-primary-soft); + box-shadow: var(--glow-md); } #timer-toggle.active { - box-shadow: var(--glow-md); + box-shadow: var(--glow-md); } #timer-toggle.running { - animation: timer-pulse 1.5s ease-in-out infinite; + animation: timer-pulse 1.5s ease-in-out infinite; } /* ---------- Panel ---------- */ #timer-panel { - display: flex; - flex-direction: column; - align-items: stretch; - gap: var(--space-sm); - padding: var(--panel-padding-y) var(--panel-padding-x); - border: var(--border-width) solid var(--color-primary); - border-radius: var(--border-radius-lg); - background-color: var(--color-surface); - animation: timer-fade-in var(--transition-normal) ease-out; - box-sizing: border-box; - width: var(--panel-width); - box-shadow: var(--glow-subtle); + display: flex; + flex-direction: column; + align-items: stretch; + gap: var(--space-sm); + padding: var(--panel-padding-y) var(--panel-padding-x); + border: var(--border-width) solid var(--color-primary); + border-radius: var(--border-radius-lg); + background-color: var(--color-surface); + animation: timer-fade-in var(--transition-normal) ease-out; + box-sizing: border-box; + width: var(--panel-width); + box-shadow: var(--glow-subtle); } #timer-panel[hidden] { - display: none; + display: none; } /* ---------- Display ---------- */ #timer-display { - font-size: var(--font-size-timer-display); - font-weight: var(--font-weight-bold); - text-align: center; - letter-spacing: 0.05em; - font-variant-numeric: tabular-nums; - width: 100%; + font-size: var(--font-size-timer-display); + font-weight: var(--font-weight-bold); + text-align: center; + letter-spacing: 0.05em; + font-variant-numeric: tabular-nums; + width: 100%; } #timer-finish-time { - font-size: var(--font-size-timer-finish); - text-align: center; - opacity: 0; - transition: opacity var(--transition-slow); - min-height: 1.4em; - color: var(--color-primary); - font-variant-numeric: tabular-nums; - letter-spacing: 0.06em; - width: 100%; + font-size: var(--font-size-timer-finish); + text-align: center; + opacity: 0; + transition: opacity var(--transition-slow); + min-height: 1.4em; + color: var(--color-primary); + font-variant-numeric: tabular-nums; + letter-spacing: 0.06em; + width: 100%; } #timer-finish-time.visible { - opacity: 0.75; + opacity: 0.75; } /* ---------- Controls (Inputs + Inc/Dec) ---------- */ #timer-controls { - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-xs); - width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + width: 100%; } -#timer-controls input[type="number"] { - width: 2.5em; - padding: var(--space-xs) var(--space-sm); - font-size: var(--font-size-timer-input); - text-align: center; - background-color: var(--color-surface-alt); - border: var(--border-width) solid var(--color-primary); - border-radius: var(--border-radius-sm); - color: var(--color-primary); - font-family: var(--font-family); - -moz-appearance: textfield; +#timer-controls input[type='number'] { + width: 2.5em; + padding: var(--space-xs) var(--space-sm); + font-size: var(--font-size-timer-input); + text-align: center; + background-color: var(--color-surface-alt); + border: var(--border-width) solid var(--color-primary); + border-radius: var(--border-radius-sm); + color: var(--color-primary); + font-family: var(--font-family); + -moz-appearance: textfield; } -#timer-controls input[type="number"]::-webkit-inner-spin-button, -#timer-controls input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; +#timer-controls input[type='number']::-webkit-inner-spin-button, +#timer-controls input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } #timer-controls button { - background: none; - border: var(--border-width) solid var(--color-primary); - color: var(--color-primary); - font-size: var(--font-size-timer-input); - cursor: pointer; - border-radius: var(--border-radius-sm); - padding: var(--space-xs) var(--space-lg); - transition: background-color var(--transition-normal), - opacity var(--transition-normal), - visibility var(--transition-normal); - line-height: 1; - opacity: 0; - visibility: hidden; + background: none; + border: var(--border-width) solid var(--color-primary); + color: var(--color-primary); + font-size: var(--font-size-timer-input); + cursor: pointer; + border-radius: var(--border-radius-sm); + padding: var(--space-xs) var(--space-lg); + transition: + background-color var(--transition-normal), + opacity var(--transition-normal), + visibility var(--transition-normal); + line-height: 1; + opacity: 0; + visibility: hidden; } #timer-controls:hover button, #timer-controls:focus-within button { - opacity: 0.5; - visibility: visible; + opacity: 0.5; + visibility: visible; } #timer-controls button:hover, #timer-controls button:focus-visible { - opacity: 1 !important; - background-color: var(--color-primary-medium); + opacity: 1 !important; + background-color: var(--color-primary-medium); } .timer-colon { - font-size: var(--font-size-timer-input); - font-weight: var(--font-weight-bold); + font-size: var(--font-size-timer-input); + font-weight: var(--font-weight-bold); } /* ---------- Presets ---------- */ #timer-presets { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-sm); - max-height: 66px; - overflow-y: auto; - overflow-x: hidden; - padding: var(--space-xs) var(--space-2xs); - width: 100%; - box-sizing: border-box; - scrollbar-width: thin; + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-sm); + max-height: 66px; + overflow-y: auto; + overflow-x: hidden; + padding: var(--space-xs) var(--space-2xs); + width: 100%; + box-sizing: border-box; + scrollbar-width: thin; } .preset-row { - display: flex; - align-items: center; - justify-content: space-between; - background: var(--color-primary-dim); - border: var(--border-width) solid rgba(0, 255, 0, 0.2); - border-radius: var(--border-radius-sm); - padding: 5px var(--space-md); - cursor: pointer; - transition: background-color var(--transition-fast), - box-shadow var(--transition-fast), - transform var(--transition-fast); - gap: var(--space-sm); - min-width: 0; - box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + background: var(--color-primary-dim); + border: var(--border-width) solid rgba(0, 255, 0, 0.2); + border-radius: var(--border-radius-sm); + padding: 5px var(--space-md); + cursor: pointer; + transition: + background-color var(--transition-fast), + box-shadow var(--transition-fast), + transform var(--transition-fast); + gap: var(--space-sm); + min-width: 0; + box-sizing: border-box; } .preset-row:hover { - background-color: rgba(0, 255, 0, 0.12); - box-shadow: 0 0 8px var(--color-primary-strong); - transform: scale(1.02); + background-color: rgba(0, 255, 0, 0.12); + box-shadow: 0 0 8px var(--color-primary-strong); + transform: scale(1.02); } .preset-row-label { - font-size: var(--font-size-small); - line-height: 1; - user-select: none; + font-size: var(--font-size-small); + line-height: 1; + user-select: none; } .preset-row-remove { - background: none; - border: none; - color: var(--color-primary); - opacity: 0.5; - font-size: 0.9em; - line-height: 1; - padding: 0 var(--space-2xs); - cursor: pointer; - transition: opacity var(--transition-fast), - color var(--transition-fast); - flex-shrink: 0; + background: none; + border: none; + color: var(--color-primary); + opacity: 0.5; + font-size: 0.9em; + line-height: 1; + padding: 0 var(--space-2xs); + cursor: pointer; + transition: + opacity var(--transition-fast), + color var(--transition-fast); + flex-shrink: 0; } .preset-row-remove:hover { - opacity: 1; - color: var(--color-danger); + opacity: 1; + color: var(--color-danger); } /* ---------- Presets Bar (Add Button) ---------- */ #timer-presets-bar { - display: flex; - align-items: center; - gap: var(--space-sm); - justify-content: center; - width: 100%; - transition: opacity var(--transition-normal); + display: flex; + align-items: center; + gap: var(--space-sm); + justify-content: center; + width: 100%; + transition: opacity var(--transition-normal); } #timer-preset-add { - background: none; - border: var(--border-width) dashed var(--color-primary); - color: var(--color-primary); - font-size: var(--font-size-xs); - cursor: pointer; - border-radius: var(--border-radius-sm); - padding: var(--space-2xs) var(--space-lg); - transition: background-color var(--transition-normal), - box-shadow var(--transition-normal); - line-height: 1; - opacity: 0.7; + background: none; + border: var(--border-width) dashed var(--color-primary); + color: var(--color-primary); + font-size: var(--font-size-xs); + cursor: pointer; + border-radius: var(--border-radius-sm); + padding: var(--space-2xs) var(--space-lg); + transition: + background-color var(--transition-normal), + box-shadow var(--transition-normal); + line-height: 1; + opacity: 0.7; } #timer-preset-add:hover, #timer-preset-add:focus-visible { - background-color: var(--color-primary-soft); - opacity: 1; - box-shadow: var(--glow-sm); + background-color: var(--color-primary-soft); + opacity: 1; + box-shadow: var(--glow-sm); } #timer-preset-add.inactive { - opacity: 0.3; - cursor: default; - box-shadow: none; + opacity: 0.3; + cursor: default; + box-shadow: none; } #timer-preset-add.inactive:hover, #timer-preset-add.inactive:focus-visible { - background-color: transparent; - opacity: 0.3; - box-shadow: none; + background-color: transparent; + opacity: 0.3; + box-shadow: none; } /* ---------- Actions (Start / Reset) ---------- */ #timer-actions { - display: flex; - gap: var(--space-md); - width: 100%; + display: flex; + gap: var(--space-md); + width: 100%; } #timer-actions button { - flex: 1; - background: none; - border: var(--border-width) solid var(--color-primary); - color: var(--color-primary); - font-size: var(--font-size-body); - cursor: pointer; - border-radius: var(--border-radius-sm); - padding: var(--space-sm) var(--space-2xl); - text-align: center; - transition: background-color var(--transition-normal), - box-shadow var(--transition-normal); + flex: 1; + background: none; + border: var(--border-width) solid var(--color-primary); + color: var(--color-primary); + font-size: var(--font-size-body); + cursor: pointer; + border-radius: var(--border-radius-sm); + padding: var(--space-sm) var(--space-2xl); + text-align: center; + transition: + background-color var(--transition-normal), + box-shadow var(--transition-normal); } #timer-actions button:hover, #timer-actions button:focus-visible { - background-color: var(--color-primary-medium); - box-shadow: var(--glow-md); + background-color: var(--color-primary-medium); + box-shadow: var(--glow-md); } #timer-actions button:disabled { - opacity: 0.4; - cursor: not-allowed; - box-shadow: none; + opacity: 0.4; + cursor: not-allowed; + box-shadow: none; } /* ---------- Custom Scrollbars ---------- */ @@ -297,36 +303,36 @@ /* Firefox */ #timer-panel, #timer-presets { - scrollbar-width: thin; - scrollbar-color: var(--color-primary) transparent; + scrollbar-width: thin; + scrollbar-color: var(--color-primary) transparent; } /* Webkit (Chrome, Safari, Edge) */ #timer-panel::-webkit-scrollbar, #timer-presets::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 6px; + height: 6px; } #timer-panel::-webkit-scrollbar-track, #timer-presets::-webkit-scrollbar-track { - background: transparent; - border-radius: 3px; + background: transparent; + border-radius: 3px; } #timer-panel::-webkit-scrollbar-thumb, #timer-presets::-webkit-scrollbar-thumb { - background-color: var(--color-primary); - border-radius: 3px; - box-shadow: var(--glow-sm); + background-color: var(--color-primary); + border-radius: 3px; + box-shadow: var(--glow-sm); } #timer-panel::-webkit-scrollbar-thumb:hover, #timer-presets::-webkit-scrollbar-thumb:hover { - background-color: var(--color-primary); - box-shadow: var(--glow-md); + background-color: var(--color-primary); + box-shadow: var(--glow-md); } #timer-panel::-webkit-scrollbar-corner { - background: transparent; -} \ No newline at end of file + background: transparent; +} diff --git a/src/styles/variables.css b/src/styles/variables.css index d30786e..0bd4ab0 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -5,69 +5,69 @@ ============================================================ */ :root { - /* === Colors === */ - --color-bg: #222; - --color-surface: #1a1a1a; - --color-surface-alt: #111; + /* === Colors === */ + --color-bg: #222; + --color-surface: #1a1a1a; + --color-surface-alt: #111; - --color-primary: #0f0; - --color-primary-dim: rgba(0, 255, 0, 0.03); - --color-primary-subtle: rgba(0, 255, 0, 0.08); - --color-primary-soft: rgba(0, 255, 0, 0.1); - --color-primary-medium: rgba(0, 255, 0, 0.15); - --color-primary-strong: rgba(0, 255, 0, 0.3); + --color-primary: #0f0; + --color-primary-dim: rgba(0, 255, 0, 0.03); + --color-primary-subtle: rgba(0, 255, 0, 0.08); + --color-primary-soft: rgba(0, 255, 0, 0.1); + --color-primary-medium: rgba(0, 255, 0, 0.15); + --color-primary-strong: rgba(0, 255, 0, 0.3); - --color-danger: #f00; + --color-danger: #f00; - /* === Typography === */ - --font-family: 'Arial', sans-serif; + /* === Typography === */ + --font-family: 'Arial', sans-serif; - --font-size-display: 8em; - --font-size-timezone: 2em; - --font-size-toggle: 1.8em; - --font-size-timer-display: 3em; - --font-size-timer-finish: 1.5em; - --font-size-timer-input: 1.2em; - --font-size-body: 1em; - --font-size-small: 0.85em; - --font-size-xs: 0.8em; - --font-size-env: 2em; + --font-size-display: 8em; + --font-size-timezone: 2em; + --font-size-toggle: 1.8em; + --font-size-timer-display: 3em; + --font-size-timer-finish: 1.5em; + --font-size-timer-input: 1.2em; + --font-size-body: 1em; + --font-size-small: 0.85em; + --font-size-xs: 0.8em; + --font-size-env: 2em; - --font-weight-bold: bold; + --font-weight-bold: bold; - /* === Spacing === */ - --space-2xs: 2px; - --space-xs: 4px; - --space-sm: 6px; - --space-md: 8px; - --space-lg: 10px; - --space-xl: 12px; - --space-2xl: 16px; - --space-3xl: 20px; - --space-4xl: 24px; + /* === Spacing === */ + --space-2xs: 2px; + --space-xs: 4px; + --space-sm: 6px; + --space-md: 8px; + --space-lg: 10px; + --space-xl: 12px; + --space-2xl: 16px; + --space-3xl: 20px; + --space-4xl: 24px; - /* === Layout === */ - --panel-width: 320px; - --panel-padding-x: var(--space-4xl); - --panel-padding-y: var(--space-2xl); - /* Inner content width — all interactive rows align to this */ - --panel-content-width: calc(var(--panel-width) - var(--panel-padding-x) * 2); + /* === Layout === */ + --panel-width: 320px; + --panel-padding-x: var(--space-4xl); + --panel-padding-y: var(--space-2xl); + /* Inner content width — all interactive rows align to this */ + --panel-content-width: calc(var(--panel-width) - var(--panel-padding-x) * 2); - /* === Borders === */ - --border-width: 1px; - --border-width-strong: 2px; - --border-radius-sm: 4px; - --border-radius-md: 6px; - --border-radius-lg: 8px; + /* === Borders === */ + --border-width: 1px; + --border-width-strong: 2px; + --border-radius-sm: 4px; + --border-radius-md: 6px; + --border-radius-lg: 8px; - /* === Shadows & Glow === */ - --glow-sm: 0 0 5px var(--color-primary); - --glow-md: 0 0 10px var(--color-primary); - --glow-lg: 0 0 20px var(--color-primary); - --glow-subtle: 0 0 20px var(--color-primary-subtle); + /* === Shadows & Glow === */ + --glow-sm: 0 0 5px var(--color-primary); + --glow-md: 0 0 10px var(--color-primary); + --glow-lg: 0 0 20px var(--color-primary); + --glow-subtle: 0 0 20px var(--color-primary-subtle); - /* === Transitions === */ - --transition-fast: 0.15s ease; - --transition-normal: 0.2s ease; - --transition-slow: 0.3s ease; -} \ No newline at end of file + /* === Transitions === */ + --transition-fast: 0.15s ease; + --transition-normal: 0.2s ease; + --transition-slow: 0.3s ease; +} diff --git a/src/utils/__tests__/time.test.ts b/src/utils/__tests__/time.test.ts index a5c637a..21ef746 100644 --- a/src/utils/__tests__/time.test.ts +++ b/src/utils/__tests__/time.test.ts @@ -1,102 +1,116 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; -import { formatTime, formatTimeForTitle, formatDuration, formatFinishTime } from '../time'; - -function fakeDate(year: number, month: number, day: number, h: number, m: number, s: number): Date { - return new Date(year, month - 1, day, h, m, s); +import { + formatTime, + formatTimeForTitle, + formatDuration, + formatFinishTime, +} from '../time'; + +function fakeDate( + year: number, + month: number, + day: number, + h: number, + m: number, + s: number, +): Date { + return new Date(year, month - 1, day, h, m, s); } describe('formatTime', () => { - it('formats midnight as 12:00:00 AM', () => { - expect(formatTime(fakeDate(2025, 1, 1, 0, 0, 0))).toBe('12:00:00 AM'); - }); - - it('formats noon as 12:00:00 PM', () => { - expect(formatTime(fakeDate(2025, 6, 15, 12, 0, 0))).toBe('12:00:00 PM'); - }); - - it('formats morning time correctly', () => { - expect(formatTime(fakeDate(2025, 3, 10, 9, 5, 3))).toBe('9:05:03 AM'); - }); - - it('formats evening time correctly', () => { - expect(formatTime(fakeDate(2025, 12, 31, 23, 59, 59))).toBe('11:59:59 PM'); - }); - - it('pads single-digit hours', () => { - // 1 AM → "1:00:00 AM" (no leading zero on hours) - expect(formatTime(fakeDate(2025, 1, 1, 1, 0, 0))).toBe('1:00:00 AM'); - }); + it('formats midnight as 12:00:00 AM', () => { + expect(formatTime(fakeDate(2025, 1, 1, 0, 0, 0))).toBe('12:00:00 AM'); + }); + + it('formats noon as 12:00:00 PM', () => { + expect(formatTime(fakeDate(2025, 6, 15, 12, 0, 0))).toBe('12:00:00 PM'); + }); + + it('formats morning time correctly', () => { + expect(formatTime(fakeDate(2025, 3, 10, 9, 5, 3))).toBe('9:05:03 AM'); + }); + + it('formats evening time correctly', () => { + expect(formatTime(fakeDate(2025, 12, 31, 23, 59, 59))).toBe('11:59:59 PM'); + }); + + it('pads single-digit hours', () => { + // 1 AM → "1:00:00 AM" (no leading zero on hours) + expect(formatTime(fakeDate(2025, 1, 1, 1, 0, 0))).toBe('1:00:00 AM'); + }); }); describe('formatTimeForTitle', () => { - it('formats midnight without seconds', () => { - expect(formatTimeForTitle(fakeDate(2025, 1, 1, 0, 0, 0))).toBe('12:00 AM'); - }); - - it('formats evening time without seconds', () => { - expect(formatTimeForTitle(fakeDate(2025, 7, 4, 17, 30, 45))).toBe('5:30 PM'); - }); + it('formats midnight without seconds', () => { + expect(formatTimeForTitle(fakeDate(2025, 1, 1, 0, 0, 0))).toBe('12:00 AM'); + }); + + it('formats evening time without seconds', () => { + expect(formatTimeForTitle(fakeDate(2025, 7, 4, 17, 30, 45))).toBe( + '5:30 PM', + ); + }); }); describe('formatDuration', () => { - it('formats zero seconds as 00:00', () => { - expect(formatDuration(0)).toBe('00:00'); - }); + it('formats zero seconds as 00:00', () => { + expect(formatDuration(0)).toBe('00:00'); + }); - it('formats seconds only', () => { - expect(formatDuration(45)).toBe('00:45'); - }); + it('formats seconds only', () => { + expect(formatDuration(45)).toBe('00:45'); + }); - it('formats minutes only', () => { - expect(formatDuration(300)).toBe('05:00'); - }); + it('formats minutes only', () => { + expect(formatDuration(300)).toBe('05:00'); + }); - it('formats mixed minutes and seconds', () => { - expect(formatDuration(150)).toBe('02:30'); - }); + it('formats mixed minutes and seconds', () => { + expect(formatDuration(150)).toBe('02:30'); + }); - it('handles large durations', () => { - expect(formatDuration(3661)).toBe('61:01'); - }); + it('handles large durations', () => { + expect(formatDuration(3661)).toBe('61:01'); + }); - it('clamps negative values to 00:00', () => { - expect(formatDuration(-10)).toBe('00:00'); - }); + it('clamps negative values to 00:00', () => { + expect(formatDuration(-10)).toBe('00:00'); + }); }); describe('formatFinishTime', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); + afterEach(() => { + vi.restoreAllMocks(); + }); - it('returns a string matching h:mm:ss AM/PM pattern', () => { - vi.useFakeTimers(); - vi.setSystemTime(fakeDate(2025, 6, 15, 12, 0, 0)); + it('returns a string matching h:mm:ss AM/PM pattern', () => { + vi.useFakeTimers(); + vi.setSystemTime(fakeDate(2025, 6, 15, 12, 0, 0)); - const result = formatFinishTime(0); - // Should be current time formatted - expect(result).toBe('12:00:00 PM'); + const result = formatFinishTime(0); + // Should be current time formatted + expect(result).toBe('12:00:00 PM'); - vi.useRealTimers(); - }); + vi.useRealTimers(); + }); - it('formats finish time 60 seconds from now', () => { - vi.useFakeTimers(); - vi.setSystemTime(fakeDate(2025, 6, 15, 12, 0, 0)); + it('formats finish time 60 seconds from now', () => { + vi.useFakeTimers(); + vi.setSystemTime(fakeDate(2025, 6, 15, 12, 0, 0)); - const result = formatFinishTime(60); - expect(result).toBe('12:01:00 PM'); + const result = formatFinishTime(60); + expect(result).toBe('12:01:00 PM'); - vi.useRealTimers(); - }); + vi.useRealTimers(); + }); - it('wraps across noon', () => { - vi.useFakeTimers(); - vi.setSystemTime(fakeDate(2025, 6, 15, 11, 59, 50)); + it('wraps across noon', () => { + vi.useFakeTimers(); + vi.setSystemTime(fakeDate(2025, 6, 15, 11, 59, 50)); - const result = formatFinishTime(15); // 15 seconds later → 12:00:05 PM - expect(result).toBe('12:00:05 PM'); + const result = formatFinishTime(15); // 15 seconds later → 12:00:05 PM + expect(result).toBe('12:00:05 PM'); - vi.useRealTimers(); - }); -}); \ No newline at end of file + vi.useRealTimers(); + }); +}); diff --git a/src/utils/__tests__/timer-core.test.ts b/src/utils/__tests__/timer-core.test.ts index 98f7f7c..8ba2fb4 100644 --- a/src/utils/__tests__/timer-core.test.ts +++ b/src/utils/__tests__/timer-core.test.ts @@ -1,423 +1,432 @@ import { describe, it, expect } from 'vitest'; import { - parseInputSeconds, - clampInputs, - secondsToInputs, - hasValidInput, - decInputs, - incInputs, - parsePresets, - formatPresetLabel, - addPreset, - removePreset, - createTimerCore, - canStart, - startTimer, - pauseTimer, - resetTimer, - tickTimer, - applyPreset, - addUserPreset, - removeUserPreset, - titlePrefix, - isResetVisible, - isPresetsHidden, - isInputsEnabled, - isAddBtnInactive, - DEFAULT_PRESETS, - DEFAULT_DURATION, - MAX_PRESETS, + parseInputSeconds, + clampInputs, + secondsToInputs, + hasValidInput, + decInputs, + incInputs, + parsePresets, + formatPresetLabel, + addPreset, + removePreset, + createTimerCore, + canStart, + startTimer, + pauseTimer, + resetTimer, + tickTimer, + applyPreset, + addUserPreset, + removeUserPreset, + titlePrefix, + isResetVisible, + isPresetsHidden, + isInputsEnabled, + isAddBtnInactive, + DEFAULT_PRESETS, + DEFAULT_DURATION, } from '../timer-core'; import type { TimerCore } from '../timer-core'; function makeCore(overrides: Partial = {}): TimerCore { - return { ...createTimerCore(), ...overrides }; + return { ...createTimerCore(), ...overrides }; } // ---------- Input helpers ---------- describe('parseInputSeconds', () => { - it('parses valid inputs', () => { - expect(parseInputSeconds('5', '0')).toBe(300); - expect(parseInputSeconds('0', '30')).toBe(30); - expect(parseInputSeconds('10', '45')).toBe(645); - }); - - it('handles empty/NaN inputs as 0', () => { - expect(parseInputSeconds('', '')).toBe(0); - expect(parseInputSeconds('abc', 'xyz')).toBe(0); - }); - - it('clamps values to max ranges', () => { - expect(parseInputSeconds('99', '59')).toBe(99 * 60 + 59); - expect(parseInputSeconds('100', '60')).toBe(99 * 60 + 59); - }); + it('parses valid inputs', () => { + expect(parseInputSeconds('5', '0')).toBe(300); + expect(parseInputSeconds('0', '30')).toBe(30); + expect(parseInputSeconds('10', '45')).toBe(645); + }); + + it('handles empty/NaN inputs as 0', () => { + expect(parseInputSeconds('', '')).toBe(0); + expect(parseInputSeconds('abc', 'xyz')).toBe(0); + }); + + it('clamps values to max ranges', () => { + expect(parseInputSeconds('99', '59')).toBe(99 * 60 + 59); + expect(parseInputSeconds('100', '60')).toBe(99 * 60 + 59); + }); }); describe('clampInputs', () => { - it('clamps to valid ranges', () => { - expect(clampInputs(-1, -1)).toEqual({ mins: 0, secs: 0 }); - expect(clampInputs(100, 60)).toEqual({ mins: 99, secs: 59 }); - }); - - it('passes through valid values', () => { - expect(clampInputs(5, 30)).toEqual({ mins: 5, secs: 30 }); - }); + it('clamps to valid ranges', () => { + expect(clampInputs(-1, -1)).toEqual({ mins: 0, secs: 0 }); + expect(clampInputs(100, 60)).toEqual({ mins: 99, secs: 59 }); + }); + + it('passes through valid values', () => { + expect(clampInputs(5, 30)).toEqual({ mins: 5, secs: 30 }); + }); }); describe('secondsToInputs', () => { - it('converts 0 to 0:0', () => { - expect(secondsToInputs(0)).toEqual({ mins: 0, secs: 0 }); - }); + it('converts 0 to 0:0', () => { + expect(secondsToInputs(0)).toEqual({ mins: 0, secs: 0 }); + }); - it('converts 300 to 5:0', () => { - expect(secondsToInputs(300)).toEqual({ mins: 5, secs: 0 }); - }); + it('converts 300 to 5:0', () => { + expect(secondsToInputs(300)).toEqual({ mins: 5, secs: 0 }); + }); - it('converts 90 to 1:30', () => { - expect(secondsToInputs(90)).toEqual({ mins: 1, secs: 30 }); - }); + it('converts 90 to 1:30', () => { + expect(secondsToInputs(90)).toEqual({ mins: 1, secs: 30 }); + }); }); describe('hasValidInput', () => { - it('returns true for positive values', () => { - expect(hasValidInput('1', '0')).toBe(true); - expect(hasValidInput('0', '1')).toBe(true); - }); - - it('returns false for zero', () => { - expect(hasValidInput('0', '0')).toBe(false); - }); + it('returns true for positive values', () => { + expect(hasValidInput('1', '0')).toBe(true); + expect(hasValidInput('0', '1')).toBe(true); + }); + + it('returns false for zero', () => { + expect(hasValidInput('0', '0')).toBe(false); + }); }); // ---------- Inc / Dec ---------- describe('decInputs', () => { - it('decrements minutes when activeInput is min', () => { - expect(decInputs(5, 0, 'min')).toEqual({ mins: 4, secs: 0 }); - }); - - it('does not go below 0 minutes', () => { - expect(decInputs(0, 0, 'min')).toEqual({ mins: 0, secs: 0 }); - }); - - it('decrements seconds by 15s steps', () => { - expect(decInputs(5, 30, 'sec')).toEqual({ mins: 5, secs: 15 }); - expect(decInputs(5, 15, 'sec')).toEqual({ mins: 5, secs: 0 }); - }); - - it('wraps from 0s to 45s and decrements minute', () => { - expect(decInputs(5, 0, 'sec')).toEqual({ mins: 4, secs: 45 }); - }); - - it('does not wrap below 0 minutes', () => { - expect(decInputs(0, 0, 'sec')).toEqual({ mins: 0, secs: 0 }); - }); + it('decrements minutes when activeInput is min', () => { + expect(decInputs(5, 0, 'min')).toEqual({ mins: 4, secs: 0 }); + }); + + it('does not go below 0 minutes', () => { + expect(decInputs(0, 0, 'min')).toEqual({ mins: 0, secs: 0 }); + }); + + it('decrements seconds by 15s steps', () => { + expect(decInputs(5, 30, 'sec')).toEqual({ mins: 5, secs: 15 }); + expect(decInputs(5, 15, 'sec')).toEqual({ mins: 5, secs: 0 }); + }); + + it('wraps from 0s to 45s and decrements minute', () => { + expect(decInputs(5, 0, 'sec')).toEqual({ mins: 4, secs: 45 }); + }); + + it('does not wrap below 0 minutes', () => { + expect(decInputs(0, 0, 'sec')).toEqual({ mins: 0, secs: 0 }); + }); }); describe('incInputs', () => { - it('increments minutes when activeInput is min', () => { - expect(incInputs(5, 0, 'min')).toEqual({ mins: 6, secs: 0 }); - }); - - it('does not go above 99 minutes', () => { - expect(incInputs(99, 0, 'min')).toEqual({ mins: 99, secs: 0 }); - }); - - it('increments seconds by 15s steps', () => { - expect(incInputs(5, 0, 'sec')).toEqual({ mins: 5, secs: 15 }); - expect(incInputs(5, 15, 'sec')).toEqual({ mins: 5, secs: 30 }); - }); - - it('wraps from 45s to 0s and increments minute', () => { - expect(incInputs(5, 45, 'sec')).toEqual({ mins: 6, secs: 0 }); - }); - - it('does not wrap above 99 minutes', () => { - expect(incInputs(99, 45, 'sec')).toEqual({ mins: 99, secs: 0 }); - }); + it('increments minutes when activeInput is min', () => { + expect(incInputs(5, 0, 'min')).toEqual({ mins: 6, secs: 0 }); + }); + + it('does not go above 99 minutes', () => { + expect(incInputs(99, 0, 'min')).toEqual({ mins: 99, secs: 0 }); + }); + + it('increments seconds by 15s steps', () => { + expect(incInputs(5, 0, 'sec')).toEqual({ mins: 5, secs: 15 }); + expect(incInputs(5, 15, 'sec')).toEqual({ mins: 5, secs: 30 }); + }); + + it('wraps from 45s to 0s and increments minute', () => { + expect(incInputs(5, 45, 'sec')).toEqual({ mins: 6, secs: 0 }); + }); + + it('does not wrap above 99 minutes', () => { + expect(incInputs(99, 45, 'sec')).toEqual({ mins: 99, secs: 0 }); + }); }); // ---------- Preset helpers ---------- describe('parsePresets', () => { - it('returns defaults for null', () => { - expect(parsePresets(null)).toEqual(DEFAULT_PRESETS); - }); + it('returns defaults for null', () => { + expect(parsePresets(null)).toEqual(DEFAULT_PRESETS); + }); - it('returns defaults for invalid JSON', () => { - expect(parsePresets('not json')).toEqual(DEFAULT_PRESETS); - }); + it('returns defaults for invalid JSON', () => { + expect(parsePresets('not json')).toEqual(DEFAULT_PRESETS); + }); - it('returns defaults for non-array', () => { - expect(parsePresets('{"foo": 1}')).toEqual(DEFAULT_PRESETS); - }); + it('returns defaults for non-array', () => { + expect(parsePresets('{"foo": 1}')).toEqual(DEFAULT_PRESETS); + }); - it('returns defaults for array with non-numbers', () => { - expect(parsePresets('[1, "a", 3]')).toEqual(DEFAULT_PRESETS); - }); + it('returns defaults for array with non-numbers', () => { + expect(parsePresets('[1, "a", 3]')).toEqual(DEFAULT_PRESETS); + }); - it('returns defaults for array with negative numbers', () => { - expect(parsePresets('[1, -5, 3]')).toEqual(DEFAULT_PRESETS); - }); + it('returns defaults for array with negative numbers', () => { + expect(parsePresets('[1, -5, 3]')).toEqual(DEFAULT_PRESETS); + }); - it('parses valid preset arrays', () => { - expect(parsePresets('[60, 300, 900]')).toEqual([60, 300, 900]); - }); + it('parses valid preset arrays', () => { + expect(parsePresets('[60, 300, 900]')).toEqual([60, 300, 900]); + }); }); describe('formatPresetLabel', () => { - it('formats whole minutes', () => { - expect(formatPresetLabel(300)).toBe('5m'); - expect(formatPresetLabel(60)).toBe('1m'); - }); - - it('formats minutes with seconds', () => { - expect(formatPresetLabel(90)).toBe('1m 30s'); - expect(formatPresetLabel(330)).toBe('5m 30s'); - }); + it('formats whole minutes', () => { + expect(formatPresetLabel(300)).toBe('5m'); + expect(formatPresetLabel(60)).toBe('1m'); + }); + + it('formats minutes with seconds', () => { + expect(formatPresetLabel(90)).toBe('1m 30s'); + expect(formatPresetLabel(330)).toBe('5m 30s'); + }); }); describe('addPreset', () => { - it('adds a new preset', () => { - expect(addPreset([300, 900], 1500)).toEqual([300, 900, 1500]); - }); - - it('sorts presets after adding', () => { - expect(addPreset([900], 300)).toEqual([300, 900]); - }); - - it('does not add duplicates', () => { - expect(addPreset([300, 900], 300)).toEqual([300, 900]); - }); - - it('does not add zero or negative', () => { - expect(addPreset([300], 0)).toEqual([300]); - expect(addPreset([300], -1)).toEqual([300]); - }); - - it('respects max limit', () => { - const presets = [1, 2, 3]; - expect(addPreset(presets, 4, 3)).toEqual([1, 2, 3]); - }); + it('adds a new preset', () => { + expect(addPreset([300, 900], 1500)).toEqual([300, 900, 1500]); + }); + + it('sorts presets after adding', () => { + expect(addPreset([900], 300)).toEqual([300, 900]); + }); + + it('does not add duplicates', () => { + expect(addPreset([300, 900], 300)).toEqual([300, 900]); + }); + + it('does not add zero or negative', () => { + expect(addPreset([300], 0)).toEqual([300]); + expect(addPreset([300], -1)).toEqual([300]); + }); + + it('respects max limit', () => { + const presets = [1, 2, 3]; + expect(addPreset(presets, 4, 3)).toEqual([1, 2, 3]); + }); }); describe('removePreset', () => { - it('removes by index', () => { - expect(removePreset([300, 900, 1500], 1)).toEqual([300, 1500]); - }); - - it('does not mutate original array', () => { - const original = [300, 900]; - removePreset(original, 0); - expect(original).toEqual([300, 900]); - }); + it('removes by index', () => { + expect(removePreset([300, 900, 1500], 1)).toEqual([300, 1500]); + }); + + it('does not mutate original array', () => { + const original = [300, 900]; + removePreset(original, 0); + expect(original).toEqual([300, 900]); + }); }); // ---------- State machine ---------- describe('createTimerCore', () => { - it('creates with default values', () => { - const core = createTimerCore(); - expect(core.state).toBe('idle'); - expect(core.remaining).toBe(DEFAULT_DURATION); - expect(core.configuredDuration).toBe(DEFAULT_DURATION); - expect(core.presets).toEqual(DEFAULT_PRESETS); - }); - - it('accepts custom presets', () => { - const core = createTimerCore([60, 120]); - expect(core.presets).toEqual([60, 120]); - }); + it('creates with default values', () => { + const core = createTimerCore(); + expect(core.state).toBe('idle'); + expect(core.remaining).toBe(DEFAULT_DURATION); + expect(core.configuredDuration).toBe(DEFAULT_DURATION); + expect(core.presets).toEqual(DEFAULT_PRESETS); + }); + + it('accepts custom presets', () => { + const core = createTimerCore([60, 120]); + expect(core.presets).toEqual([60, 120]); + }); }); describe('canStart', () => { - it('returns true for idle, paused, finished', () => { - expect(canStart(makeCore({ state: 'idle' }))).toBe(true); - expect(canStart(makeCore({ state: 'paused' }))).toBe(true); - expect(canStart(makeCore({ state: 'finished' }))).toBe(true); - }); - - it('returns false for running', () => { - expect(canStart(makeCore({ state: 'running' }))).toBe(false); - }); + it('returns true for idle, paused, finished', () => { + expect(canStart(makeCore({ state: 'idle' }))).toBe(true); + expect(canStart(makeCore({ state: 'paused' }))).toBe(true); + expect(canStart(makeCore({ state: 'finished' }))).toBe(true); + }); + + it('returns false for running', () => { + expect(canStart(makeCore({ state: 'running' }))).toBe(false); + }); }); describe('startTimer', () => { - const now = new Date(2025, 5, 15, 12, 0, 0); - - it('transitions idle → running', () => { - const result = startTimer(makeCore({ remaining: 300 }), now); - expect(result).not.toBeNull(); - expect(result!.state).toBe('running'); - expect(result!.finishTimestamp).toBe(now.getTime() + 300000); - }); - - it('transitions paused → running (resume)', () => { - const result = startTimer(makeCore({ state: 'paused', remaining: 120 }), now); - expect(result!.state).toBe('running'); - expect(result!.remaining).toBe(120); - }); - - it('does nothing if already running', () => { - const result = startTimer(makeCore({ state: 'running', remaining: 100 }), now); - expect(result).toBeNull(); - }); - - it('returns null if remaining is 0 from idle', () => { - const result = startTimer(makeCore({ state: 'idle', remaining: 0 }), now); - expect(result).toBeNull(); - }); + const now = new Date(2025, 5, 15, 12, 0, 0); + + it('transitions idle → running', () => { + const result = startTimer(makeCore({ remaining: 300 }), now); + expect(result).not.toBeNull(); + expect(result!.state).toBe('running'); + expect(result!.finishTimestamp).toBe(now.getTime() + 300000); + }); + + it('transitions paused → running (resume)', () => { + const result = startTimer( + makeCore({ state: 'paused', remaining: 120 }), + now, + ); + expect(result!.state).toBe('running'); + expect(result!.remaining).toBe(120); + }); + + it('does nothing if already running', () => { + const result = startTimer( + makeCore({ state: 'running', remaining: 100 }), + now, + ); + expect(result).toBeNull(); + }); + + it('returns null if remaining is 0 from idle', () => { + const result = startTimer(makeCore({ state: 'idle', remaining: 0 }), now); + expect(result).toBeNull(); + }); }); describe('pauseTimer', () => { - it('transitions running → paused', () => { - const startTime = new Date(2025, 5, 15, 12, 0, 0); - const core = startTimer(makeCore({ remaining: 300 }), startTime)!; - // Simulate 10 seconds passing - const pauseTime = new Date(2025, 5, 15, 12, 0, 10); - const result = pauseTimer(core, pauseTime); - expect(result).not.toBeNull(); - expect(result!.state).toBe('paused'); - expect(result!.remaining).toBe(290); - expect(result!.finishTimestamp).toBeNull(); - }); - - it('does nothing if not running', () => { - expect(pauseTimer(makeCore({ state: 'idle' }))).toBeNull(); - expect(pauseTimer(makeCore({ state: 'paused' }))).toBeNull(); - expect(pauseTimer(makeCore({ state: 'finished' }))).toBeNull(); - }); + it('transitions running → paused', () => { + const startTime = new Date(2025, 5, 15, 12, 0, 0); + const core = startTimer(makeCore({ remaining: 300 }), startTime)!; + // Simulate 10 seconds passing + const pauseTime = new Date(2025, 5, 15, 12, 0, 10); + const result = pauseTimer(core, pauseTime); + expect(result).not.toBeNull(); + expect(result!.state).toBe('paused'); + expect(result!.remaining).toBe(290); + expect(result!.finishTimestamp).toBeNull(); + }); + + it('does nothing if not running', () => { + expect(pauseTimer(makeCore({ state: 'idle' }))).toBeNull(); + expect(pauseTimer(makeCore({ state: 'paused' }))).toBeNull(); + expect(pauseTimer(makeCore({ state: 'finished' }))).toBeNull(); + }); }); describe('resetTimer', () => { - it('returns to idle with configured duration', () => { - const core = makeCore({ configuredDuration: 600, remaining: 100, state: 'running' }); - const result = resetTimer(core); - expect(result.state).toBe('idle'); - expect(result.remaining).toBe(600); - expect(result.finishTimestamp).toBeNull(); - }); + it('returns to idle with configured duration', () => { + const core = makeCore({ + configuredDuration: 600, + remaining: 100, + state: 'running', + }); + const result = resetTimer(core); + expect(result.state).toBe('idle'); + expect(result.remaining).toBe(600); + expect(result.finishTimestamp).toBeNull(); + }); }); describe('tickTimer', () => { - it('decrements remaining while running', () => { - const core = makeCore({ state: 'running', remaining: 300 }); - const result = tickTimer(core); - expect(result.remaining).toBe(299); - expect(result.state).toBe('running'); - }); - - it('transitions to finished when remaining hits 0', () => { - const core = makeCore({ state: 'running', remaining: 1 }); - const result = tickTimer(core); - expect(result.state).toBe('finished'); - expect(result.remaining).toBe(0); - expect(result.finishTimestamp).toBeNull(); - }); - - it('does nothing if not running', () => { - const core = makeCore({ state: 'idle', remaining: 300 }); - expect(tickTimer(core)).toEqual(core); - }); + it('decrements remaining while running', () => { + const core = makeCore({ state: 'running', remaining: 300 }); + const result = tickTimer(core); + expect(result.remaining).toBe(299); + expect(result.state).toBe('running'); + }); + + it('transitions to finished when remaining hits 0', () => { + const core = makeCore({ state: 'running', remaining: 1 }); + const result = tickTimer(core); + expect(result.state).toBe('finished'); + expect(result.remaining).toBe(0); + expect(result.finishTimestamp).toBeNull(); + }); + + it('does nothing if not running', () => { + const core = makeCore({ state: 'idle', remaining: 300 }); + expect(tickTimer(core)).toEqual(core); + }); }); describe('applyPreset', () => { - it('sets duration and remaining from preset', () => { - const result = applyPreset(makeCore(), 900); - expect(result.configuredDuration).toBe(900); - expect(result.remaining).toBe(900); - }); - - it('does nothing while running', () => { - const core = makeCore({ state: 'running' }); - expect(applyPreset(core, 900)).toEqual(core); - }); - - it('transitions paused to idle', () => { - const result = applyPreset(makeCore({ state: 'paused' }), 900); - expect(result.state).toBe('idle'); - }); + it('sets duration and remaining from preset', () => { + const result = applyPreset(makeCore(), 900); + expect(result.configuredDuration).toBe(900); + expect(result.remaining).toBe(900); + }); + + it('does nothing while running', () => { + const core = makeCore({ state: 'running' }); + expect(applyPreset(core, 900)).toEqual(core); + }); + + it('transitions paused to idle', () => { + const result = applyPreset(makeCore({ state: 'paused' }), 900); + expect(result.state).toBe('idle'); + }); }); describe('addUserPreset / removeUserPreset', () => { - it('adds a preset to the core', () => { - const core = addUserPreset(makeCore(), 120); - expect(core.presets).toContain(120); - }); - - it('removes a preset by index', () => { - const core = removeUserPreset(makeCore(), 0); - expect(core.presets.length).toBe(DEFAULT_PRESETS.length - 1); - }); + it('adds a preset to the core', () => { + const core = addUserPreset(makeCore(), 120); + expect(core.presets).toContain(120); + }); + + it('removes a preset by index', () => { + const core = removeUserPreset(makeCore(), 0); + expect(core.presets.length).toBe(DEFAULT_PRESETS.length - 1); + }); }); // ---------- Derived display values ---------- describe('titlePrefix', () => { - it('returns empty for idle', () => { - expect(titlePrefix('idle', 300)).toBe(''); - }); + it('returns empty for idle', () => { + expect(titlePrefix('idle', 300)).toBe(''); + }); - it('returns empty for finished', () => { - expect(titlePrefix('finished', 0)).toBe(''); - }); + it('returns empty for finished', () => { + expect(titlePrefix('finished', 0)).toBe(''); + }); - it('returns formatted prefix for running', () => { - expect(titlePrefix('running', 300)).toBe('05:00 | '); - }); + it('returns formatted prefix for running', () => { + expect(titlePrefix('running', 300)).toBe('05:00 | '); + }); - it('returns formatted prefix for paused', () => { - expect(titlePrefix('paused', 65)).toBe('01:05 | '); - }); + it('returns formatted prefix for paused', () => { + expect(titlePrefix('paused', 65)).toBe('01:05 | '); + }); }); describe('isResetVisible', () => { - it('is false for idle', () => { - expect(isResetVisible('idle')).toBe(false); - }); - - it('is true for all other states', () => { - expect(isResetVisible('running')).toBe(true); - expect(isResetVisible('paused')).toBe(true); - expect(isResetVisible('finished')).toBe(true); - }); + it('is false for idle', () => { + expect(isResetVisible('idle')).toBe(false); + }); + + it('is true for all other states', () => { + expect(isResetVisible('running')).toBe(true); + expect(isResetVisible('paused')).toBe(true); + expect(isResetVisible('finished')).toBe(true); + }); }); describe('isPresetsHidden', () => { - it('is true for running and paused', () => { - expect(isPresetsHidden('running')).toBe(true); - expect(isPresetsHidden('paused')).toBe(true); - }); - - it('is false for idle and finished', () => { - expect(isPresetsHidden('idle')).toBe(false); - expect(isPresetsHidden('finished')).toBe(false); - }); + it('is true for running and paused', () => { + expect(isPresetsHidden('running')).toBe(true); + expect(isPresetsHidden('paused')).toBe(true); + }); + + it('is false for idle and finished', () => { + expect(isPresetsHidden('idle')).toBe(false); + expect(isPresetsHidden('finished')).toBe(false); + }); }); describe('isInputsEnabled', () => { - it('is true for idle and finished', () => { - expect(isInputsEnabled('idle')).toBe(true); - expect(isInputsEnabled('finished')).toBe(true); - }); - - it('is false for running and paused', () => { - expect(isInputsEnabled('running')).toBe(false); - expect(isInputsEnabled('paused')).toBe(false); - }); + it('is true for idle and finished', () => { + expect(isInputsEnabled('idle')).toBe(true); + expect(isInputsEnabled('finished')).toBe(true); + }); + + it('is false for running and paused', () => { + expect(isInputsEnabled('running')).toBe(false); + expect(isInputsEnabled('paused')).toBe(false); + }); }); describe('isAddBtnInactive', () => { - it('is true when total is 0', () => { - expect(isAddBtnInactive([300], 0)).toBe(true); - }); - - it('is true when preset already exists', () => { - expect(isAddBtnInactive([300, 900], 300)).toBe(true); - }); - - it('is false when valid and unique', () => { - expect(isAddBtnInactive([300, 900], 150)).toBe(false); - }); -}); \ No newline at end of file + it('is true when total is 0', () => { + expect(isAddBtnInactive([300], 0)).toBe(true); + }); + + it('is true when preset already exists', () => { + expect(isAddBtnInactive([300, 900], 300)).toBe(true); + }); + + it('is false when valid and unique', () => { + expect(isAddBtnInactive([300, 900], 150)).toBe(false); + }); +}); diff --git a/src/utils/audio.ts b/src/utils/audio.ts index bef30b7..3b2a8e1 100644 --- a/src/utils/audio.ts +++ b/src/utils/audio.ts @@ -3,29 +3,26 @@ * Accepts an optional AudioContext for testability (dependency injection). */ export function playBeep(audioCtx?: AudioContext): Promise { - const ctx = audioCtx ?? new AudioContext(); - return new Promise((resolve) => { - const oscillator = ctx.createOscillator(); - const gainNode = ctx.createGain(); + const ctx = audioCtx ?? new AudioContext(); + return new Promise((resolve) => { + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); - oscillator.type = 'square'; - oscillator.frequency.value = 880; + oscillator.type = 'square'; + oscillator.frequency.value = 880; - gainNode.gain.value = 0.3; - gainNode.gain.exponentialRampToValueAtTime( - 0.001, - ctx.currentTime + 0.5, - ); + gainNode.gain.value = 0.3; + gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5); - oscillator.connect(gainNode); - gainNode.connect(ctx.destination); + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); - oscillator.start(); - oscillator.stop(ctx.currentTime + 0.5); + oscillator.start(); + oscillator.stop(ctx.currentTime + 0.5); - oscillator.onended = () => { - ctx.close(); - resolve(); - }; - }); -} \ No newline at end of file + oscillator.onended = () => { + ctx.close(); + resolve(); + }; + }); +} diff --git a/src/utils/time.ts b/src/utils/time.ts index 3762c77..e54bf20 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,46 +1,51 @@ /** * Converts a 24-hour Date into 12-hour components. */ -function to12Hour(date: Date): { hours: number; minutes: string; seconds: string; ampm: string } { - const hours24 = date.getHours(); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const ampm = hours24 >= 12 ? 'PM' : 'AM'; - const hours12 = hours24 % 12 || 12; - return { hours: hours12, minutes, seconds, ampm }; +function to12Hour(date: Date): { + hours: number; + minutes: string; + seconds: string; + ampm: string; +} { + const hours24 = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const ampm = hours24 >= 12 ? 'PM' : 'AM'; + const hours12 = hours24 % 12 || 12; + return { hours: hours12, minutes, seconds, ampm }; } /** * Formats a Date object into a `h:mm:ss AM/PM` string. */ export function formatTime(date: Date): string { - const { hours, minutes, seconds, ampm } = to12Hour(date); - return `${hours}:${minutes}:${seconds} ${ampm}`; + const { hours, minutes, seconds, ampm } = to12Hour(date); + return `${hours}:${minutes}:${seconds} ${ampm}`; } /** * Formats a Date object into a `h:mm AM/PM` string for the document title. */ export function formatTimeForTitle(date: Date): string { - const { hours, minutes, ampm } = to12Hour(date); - return `${hours}:${minutes} ${ampm}`; + const { hours, minutes, ampm } = to12Hour(date); + return `${hours}:${minutes} ${ampm}`; } /** * Converts total seconds into a `MM:SS` string. */ export function formatDuration(totalSeconds: number): string { - if (totalSeconds < 0) totalSeconds = 0; - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + if (totalSeconds < 0) totalSeconds = 0; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } /** * Converts a number of seconds from now into a wall-clock time string. */ export function formatFinishTime(secondsFromNow: number): string { - const finish = new Date(Date.now() + secondsFromNow * 1000); - const { hours, minutes, seconds, ampm } = to12Hour(finish); - return `${hours}:${minutes}:${seconds} ${ampm}`; -} \ No newline at end of file + const finish = new Date(Date.now() + secondsFromNow * 1000); + const { hours, minutes, seconds, ampm } = to12Hour(finish); + return `${hours}:${minutes}:${seconds} ${ampm}`; +} diff --git a/src/utils/timer-core.ts b/src/utils/timer-core.ts index e6cae6a..f0f47d6 100644 --- a/src/utils/timer-core.ts +++ b/src/utils/timer-core.ts @@ -10,11 +10,11 @@ export type TimerState = 'idle' | 'running' | 'paused' | 'finished'; export interface TimerCore { - state: TimerState; - remaining: number; - configuredDuration: number; - finishTimestamp: number | null; - presets: number[]; + state: TimerState; + remaining: number; + configuredDuration: number; + finishTimestamp: number | null; + presets: number[]; } export const DEFAULT_DURATION = 5 * 60; // 5 minutes @@ -25,224 +25,261 @@ export const DEFAULT_PRESETS = [300, 900, 1500]; // 5m, 15m, 25m /** Parse min/sec string inputs into total seconds, clamped to valid ranges. */ export function parseInputSeconds(mins: string, secs: string): number { - const m = parseInt(mins, 10) || 0; - const s = parseInt(secs, 10) || 0; - return Math.min(m, 99) * 60 + Math.min(s, 59); + const m = parseInt(mins, 10) || 0; + const s = parseInt(secs, 10) || 0; + return Math.min(m, 99) * 60 + Math.min(s, 59); } /** Clamp raw min/sec numbers to valid ranges. */ -export function clampInputs(mins: number, secs: number): { mins: number; secs: number } { - return { - mins: Math.max(0, Math.min(mins, 99)), - secs: Math.max(0, Math.min(secs, 59)), - }; +export function clampInputs( + mins: number, + secs: number, +): { mins: number; secs: number } { + return { + mins: Math.max(0, Math.min(mins, 99)), + secs: Math.max(0, Math.min(secs, 59)), + }; } /** Convert total seconds to { mins, secs } for display in inputs. */ -export function secondsToInputs(totalSec: number): { mins: number; secs: number } { - return { - mins: Math.floor(totalSec / 60), - secs: totalSec % 60, - }; +export function secondsToInputs(totalSec: number): { + mins: number; + secs: number; +} { + return { + mins: Math.floor(totalSec / 60), + secs: totalSec % 60, + }; } /** Check if the parsed input is greater than zero. */ export function hasValidInput(mins: string, secs: string): boolean { - return parseInputSeconds(mins, secs) > 0; + return parseInputSeconds(mins, secs) > 0; } // ---------- Inc / Dec ---------- /** Decrement seconds by 15-second steps, wrapping across minutes. */ -export function decInputs(mins: number, secs: number, activeInput: 'min' | 'sec'): { mins: number; secs: number } { - if (activeInput === 'sec') { - const raw = Math.ceil(secs / 15) * 15 - 15; - if (raw < 0) { - return { - mins: mins > 0 ? mins - 1 : mins, - secs: mins > 0 ? 45 : 0, - }; - } - return { mins, secs: raw }; - } - return { mins: mins > 0 ? mins - 1 : mins, secs }; +export function decInputs( + mins: number, + secs: number, + activeInput: 'min' | 'sec', +): { mins: number; secs: number } { + if (activeInput === 'sec') { + const raw = Math.ceil(secs / 15) * 15 - 15; + if (raw < 0) { + return { + mins: mins > 0 ? mins - 1 : mins, + secs: mins > 0 ? 45 : 0, + }; + } + return { mins, secs: raw }; + } + return { mins: mins > 0 ? mins - 1 : mins, secs }; } /** Increment seconds by 15-second steps, wrapping across minutes. */ -export function incInputs(mins: number, secs: number, activeInput: 'min' | 'sec'): { mins: number; secs: number } { - if (activeInput === 'sec') { - const raw = Math.floor(secs / 15) * 15 + 15; - if (raw > 59) { - return { - mins: mins < 99 ? mins + 1 : mins, - secs: 0, - }; - } - return { mins, secs: raw }; - } - return { mins: mins < 99 ? mins + 1 : mins, secs }; +export function incInputs( + mins: number, + secs: number, + activeInput: 'min' | 'sec', +): { mins: number; secs: number } { + if (activeInput === 'sec') { + const raw = Math.floor(secs / 15) * 15 + 15; + if (raw > 59) { + return { + mins: mins < 99 ? mins + 1 : mins, + secs: 0, + }; + } + return { mins, secs: raw }; + } + return { mins: mins < 99 ? mins + 1 : mins, secs }; } // ---------- Preset helpers ---------- /** Attempt to parse presets from a raw localStorage value. Returns defaults on failure. */ export function parsePresets(raw: string | null): number[] { - if (!raw) return [...DEFAULT_PRESETS]; - try { - const parsed = JSON.parse(raw); - if (Array.isArray(parsed) && parsed.every((n: unknown) => typeof n === 'number' && n > 0)) { - return parsed; - } - } catch { - // ignore - } - return [...DEFAULT_PRESETS]; + if (!raw) return [...DEFAULT_PRESETS]; + try { + const parsed = JSON.parse(raw); + if ( + Array.isArray(parsed) && + parsed.every((n: unknown) => typeof n === 'number' && n > 0) + ) { + return parsed; + } + } catch { + // ignore + } + return [...DEFAULT_PRESETS]; } /** Format a preset duration into a human label like "5m" or "15m 30s". */ export function formatPresetLabel(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - if (secs === 0) return `${mins}m`; - return `${mins}m ${secs}s`; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (secs === 0) return `${mins}m`; + return `${mins}m ${secs}s`; } /** Add a preset if it's valid, not a duplicate, and within limit. Returns new array. */ -export function addPreset(presets: number[], seconds: number, max = MAX_PRESETS): number[] { - if (seconds <= 0) return presets; - if (presets.includes(seconds)) return presets; - if (presets.length >= max) return presets; - return [...presets, seconds].sort((a, b) => a - b); +export function addPreset( + presets: number[], + seconds: number, + max = MAX_PRESETS, +): number[] { + if (seconds <= 0) return presets; + if (presets.includes(seconds)) return presets; + if (presets.length >= max) return presets; + return [...presets, seconds].sort((a, b) => a - b); } /** Remove a preset by index. Returns new array. */ export function removePreset(presets: number[], index: number): number[] { - return presets.filter((_, i) => i !== index); + return presets.filter((_, i) => i !== index); } // ---------- State machine ---------- /** Create the initial timer core state. */ export function createTimerCore(presets?: number[]): TimerCore { - return { - state: 'idle', - remaining: DEFAULT_DURATION, - configuredDuration: DEFAULT_DURATION, - finishTimestamp: null, - presets: presets ?? [...DEFAULT_PRESETS], - }; + return { + state: 'idle', + remaining: DEFAULT_DURATION, + configuredDuration: DEFAULT_DURATION, + finishTimestamp: null, + presets: presets ?? [...DEFAULT_PRESETS], + }; } /** Can the timer be started from the current state? */ export function canStart(core: TimerCore): boolean { - return core.state === 'idle' || core.state === 'paused' || core.state === 'finished'; + return ( + core.state === 'idle' || + core.state === 'paused' || + core.state === 'finished' + ); } /** Transition to running. Returns new state or null if invalid. */ -export function startTimer(core: TimerCore, now: Date = new Date()): TimerCore | null { - if (core.state === 'running') return null; - if (core.state === 'idle' || core.state === 'finished') { - if (core.remaining <= 0) return null; - } - return { - ...core, - state: 'running', - finishTimestamp: now.getTime() + core.remaining * 1000, - }; +export function startTimer( + core: TimerCore, + now: Date = new Date(), +): TimerCore | null { + if (core.state === 'running') return null; + if (core.state === 'idle' || core.state === 'finished') { + if (core.remaining <= 0) return null; + } + return { + ...core, + state: 'running', + finishTimestamp: now.getTime() + core.remaining * 1000, + }; } /** Transition to paused. Returns new state or null if invalid. */ -export function pauseTimer(core: TimerCore, now: Date = new Date()): TimerCore | null { - if (core.state !== 'running') return null; - if (core.finishTimestamp === null) return null; - const remaining = Math.max(0, Math.ceil((core.finishTimestamp - now.getTime()) / 1000)); - return { - ...core, - state: 'paused', - remaining, - finishTimestamp: null, - }; +export function pauseTimer( + core: TimerCore, + now: Date = new Date(), +): TimerCore | null { + if (core.state !== 'running') return null; + if (core.finishTimestamp === null) return null; + const remaining = Math.max( + 0, + Math.ceil((core.finishTimestamp - now.getTime()) / 1000), + ); + return { + ...core, + state: 'paused', + remaining, + finishTimestamp: null, + }; } /** Transition to idle (reset). Returns new state. */ export function resetTimer(core: TimerCore): TimerCore { - return { - ...core, - state: 'idle', - remaining: core.configuredDuration, - finishTimestamp: null, - }; + return { + ...core, + state: 'idle', + remaining: core.configuredDuration, + finishTimestamp: null, + }; } /** Tick the countdown. Returns new state (may be 'finished'). */ export function tickTimer(core: TimerCore): TimerCore { - if (core.state !== 'running') return core; - const remaining = core.remaining - 1; - if (remaining <= 0) { - return { - ...core, - state: 'finished', - remaining: 0, - finishTimestamp: null, - }; - } - return { ...core, remaining }; + if (core.state !== 'running') return core; + const remaining = core.remaining - 1; + if (remaining <= 0) { + return { + ...core, + state: 'finished', + remaining: 0, + finishTimestamp: null, + }; + } + return { ...core, remaining }; } /** Apply a preset duration. Returns new state. */ export function applyPreset(core: TimerCore, seconds: number): TimerCore { - if (core.state === 'running') return core; - return { - ...core, - state: core.state === 'paused' ? 'idle' : core.state, - configuredDuration: seconds, - remaining: seconds, - finishTimestamp: null, - }; + if (core.state === 'running') return core; + return { + ...core, + state: core.state === 'paused' ? 'idle' : core.state, + configuredDuration: seconds, + remaining: seconds, + finishTimestamp: null, + }; } /** Add a user-defined preset. Returns new core with updated presets. */ export function addUserPreset(core: TimerCore, seconds: number): TimerCore { - return { - ...core, - presets: addPreset(core.presets, seconds), - }; + return { + ...core, + presets: addPreset(core.presets, seconds), + }; } /** Remove a preset by index. Returns new core with updated presets. */ export function removeUserPreset(core: TimerCore, index: number): TimerCore { - return { - ...core, - presets: removePreset(core.presets, index), - }; + return { + ...core, + presets: removePreset(core.presets, index), + }; } // ---------- Derived display values ---------- /** Compute the document title prefix. */ export function titlePrefix(state: TimerState, remaining: number): string { - if (state === 'idle' || state === 'finished') return ''; - const m = Math.floor(remaining / 60); - const s = remaining % 60; - return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')} | `; + if (state === 'idle' || state === 'finished') return ''; + const m = Math.floor(remaining / 60); + const s = remaining % 60; + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')} | `; } /** Determine if the reset button should be visible. */ export function isResetVisible(state: TimerState): boolean { - return state !== 'idle'; + return state !== 'idle'; } /** Determine if presets should be hidden. */ export function isPresetsHidden(state: TimerState): boolean { - return state === 'running' || state === 'paused'; + return state === 'running' || state === 'paused'; } /** Determine if inputs should be enabled. */ export function isInputsEnabled(state: TimerState): boolean { - return state !== 'running' && state !== 'paused'; + return state !== 'running' && state !== 'paused'; } /** Determine the add-preset button disabled state. */ -export function isAddBtnInactive(presets: number[], totalSeconds: number): boolean { - return totalSeconds <= 0 || presets.includes(totalSeconds); -} \ No newline at end of file +export function isAddBtnInactive( + presets: number[], + totalSeconds: number, +): boolean { + return totalSeconds <= 0 || presets.includes(totalSeconds); +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 01eb644..7f90fd0 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,14 +1,14 @@ /// interface ImportMetaEnv { - readonly VITE_ENVIRONMENT: string; - readonly BASE_URL: string; - readonly MODE: string; - readonly DEV: boolean; - readonly PROD: boolean; - readonly SSR: boolean; + readonly VITE_ENVIRONMENT: string; + readonly BASE_URL: string; + readonly MODE: string; + readonly DEV: boolean; + readonly PROD: boolean; + readonly SSR: boolean; } interface ImportMeta { - readonly env: ImportMetaEnv; + readonly env: ImportMetaEnv; } diff --git a/tsconfig.json b/tsconfig.json index df37ce3..1c63047 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,19 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "rootDir": "./src", - "noEmit": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "skipLibCheck": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} \ No newline at end of file + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "rootDir": "./src", + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/vite.config.ts b/vite.config.ts index 1d4b590..c3e6e2f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,16 +1,16 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ - root: 'src', - build: { - outDir: '../dist', - emptyOutDir: true, - }, - server: { - open: true, - }, - test: { - globals: true, - environment: 'jsdom', - }, -}); \ No newline at end of file + root: 'src', + build: { + outDir: '../dist', + emptyOutDir: true, + }, + server: { + open: true, + }, + test: { + globals: true, + environment: 'jsdom', + }, +}); From 329e77eeb627bf04558ae6e7d0d47ab79146e46c Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 08:00:26 -0500 Subject: [PATCH 08/10] css: add standard appearance property for browser compatibility --- src/styles/timer.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/timer.css b/src/styles/timer.css index 5d370a8..355da79 100644 --- a/src/styles/timer.css +++ b/src/styles/timer.css @@ -109,6 +109,7 @@ border-radius: var(--border-radius-sm); color: var(--color-primary); font-family: var(--font-family); + appearance: textfield; -moz-appearance: textfield; } From 022529e4b21e07fb649ca5fef4c6d7a3c93436ae Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 08:10:37 -0500 Subject: [PATCH 09/10] ci: add GitHub Actions CI pipeline for build, lint, and test --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..df6629f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI Pipeline + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + validate: + name: Build, Lint, and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check Formatting + run: npx prettier --check . + + - name: Run Linter + run: npm run lint + + - name: Check Types + run: npm run typecheck + + - name: Run Vitest + run: npm run test + + - name: Verify Build + run: npm run build From 8971d36915a03764a7e58179922d6f366d4fd098 Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Fri, 5 Jun 2026 08:16:55 -0500 Subject: [PATCH 10/10] chore: add missing typecheck script for CI pipeline --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index eda8fda..69cefad 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", + "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "format": "prettier --write .",