feat: core instruments

This commit is contained in:
2026-08-22 09:49:53 +05:00
parent 4ec2b418fd
commit 9ccc9f94ac
13 changed files with 829 additions and 2 deletions
+4 -2
View File
@@ -9,7 +9,8 @@
"preview": "vite preview", "preview": "vite preview",
"prepare": "svelte-kit sync || echo ''", "prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest run"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
@@ -18,6 +19,7 @@
"svelte": "^5.56.1", "svelte": "^5.56.1",
"svelte-check": "^4.6.0", "svelte-check": "^4.6.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.16" "vite": "^8.0.16",
"vitest": "^4.1.11"
} }
} }
+234
View File
@@ -29,6 +29,9 @@ importers:
vite: vite:
specifier: ^8.0.16 specifier: ^8.0.16
version: 8.2.1 version: 8.2.1
vitest:
specifier: ^4.1.11
version: 4.1.11(vite@8.2.1)
packages: packages:
@@ -187,15 +190,50 @@ packages:
svelte: ^5.46.4 svelte: ^5.46.4
vite: ^8.0.0-beta.7 || ^8.0.0 vite: ^8.0.0-beta.7 || ^8.0.0
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/cookie@0.6.0': '@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.9': '@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@vitest/expect@4.1.11':
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
'@vitest/mocker@4.1.11':
resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.1.11':
resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
'@vitest/runner@4.1.11':
resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
'@vitest/snapshot@4.1.11':
resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
'@vitest/spy@4.1.11':
resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
'@vitest/utils@4.1.11':
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
acorn@8.18.0: acorn@8.18.0:
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@@ -205,10 +243,18 @@ packages:
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -217,6 +263,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie@0.6.0: cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -232,6 +281,9 @@ packages:
devalue@5.9.0: devalue@5.9.0:
resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==}
es-module-lexer@2.3.2:
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
esm-env@1.2.2: esm-env@1.2.2:
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
@@ -243,6 +295,13 @@ packages:
'@typescript-eslint/types': '@typescript-eslint/types':
optional: true optional: true
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
expect-type@1.4.0:
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -364,6 +423,9 @@ packages:
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -391,6 +453,9 @@ packages:
set-cookie-parser@3.1.2: set-cookie-parser@3.1.2:
resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
sirv@3.0.2: sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -399,6 +464,12 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@4.2.0:
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
svelte-check@4.7.6: svelte-check@4.7.6:
resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
@@ -411,10 +482,21 @@ packages:
resolution: {integrity: sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==} resolution: {integrity: sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==}
engines: {node: '>=18'} engines: {node: '>=18'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@1.3.0:
resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
engines: {node: '>=18'}
tinyglobby@0.2.17: tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinyrainbow@3.1.1:
resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
engines: {node: '>=14.0.0'}
totalist@3.0.1: totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -475,6 +557,52 @@ packages:
vite: vite:
optional: true optional: true
vitest@4.1.11:
resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
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.11
'@vitest/browser-preview': 4.1.11
'@vitest/browser-webdriverio': 4.1.11
'@vitest/coverage-istanbul': 4.1.11
'@vitest/coverage-v8': 4.1.11
'@vitest/ui': 4.1.11
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
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
zimmerframe@1.1.4: zimmerframe@1.1.4:
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
@@ -588,24 +716,78 @@ snapshots:
vite: 8.2.1 vite: 8.2.1
vitefu: 1.1.3(vite@8.2.1) vitefu: 1.1.3(vite@8.2.1)
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/cookie@0.6.0': {} '@types/cookie@0.6.0': {}
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.9': {} '@types/estree@1.0.9': {}
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
'@vitest/expect@4.1.11':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
chai: 6.2.2
tinyrainbow: 3.1.1
'@vitest/mocker@4.1.11(vite@8.2.1)':
dependencies:
'@vitest/spy': 4.1.11
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.2.1
'@vitest/pretty-format@4.1.11':
dependencies:
tinyrainbow: 3.1.1
'@vitest/runner@4.1.11':
dependencies:
'@vitest/utils': 4.1.11
pathe: 2.0.3
'@vitest/snapshot@4.1.11':
dependencies:
'@vitest/pretty-format': 4.1.11
'@vitest/utils': 4.1.11
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.11': {}
'@vitest/utils@4.1.11':
dependencies:
'@vitest/pretty-format': 4.1.11
convert-source-map: 2.0.0
tinyrainbow: 3.1.1
acorn@8.18.0: {} acorn@8.18.0: {}
aria-query@5.3.1: {} aria-query@5.3.1: {}
assertion-error@2.0.1: {}
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
chai@6.2.2: {}
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
clsx@2.1.1: {} clsx@2.1.1: {}
convert-source-map@2.0.0: {}
cookie@0.6.0: {} cookie@0.6.0: {}
deepmerge@4.3.1: {} deepmerge@4.3.1: {}
@@ -614,12 +796,20 @@ snapshots:
devalue@5.9.0: {} devalue@5.9.0: {}
es-module-lexer@2.3.2: {}
esm-env@1.2.2: {} esm-env@1.2.2: {}
esrap@2.3.5: esrap@2.3.5:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
expect-type@1.4.0: {}
fdir@6.5.0(picomatch@4.0.5): fdir@6.5.0(picomatch@4.0.5):
optionalDependencies: optionalDependencies:
picomatch: 4.0.5 picomatch: 4.0.5
@@ -700,6 +890,8 @@ snapshots:
obug@2.1.4: {} obug@2.1.4: {}
pathe@2.0.3: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.5: {} picomatch@4.0.5: {}
@@ -738,6 +930,8 @@ snapshots:
set-cookie-parser@3.1.2: {} set-cookie-parser@3.1.2: {}
siginfo@2.0.0: {}
sirv@3.0.2: sirv@3.0.2:
dependencies: dependencies:
'@polka/url': 1.0.0-next.29 '@polka/url': 1.0.0-next.29
@@ -746,6 +940,10 @@ snapshots:
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@4.2.0: {}
svelte-check@4.7.6(picomatch@4.0.5)(svelte@5.56.9)(typescript@6.0.3): svelte-check@4.7.6(picomatch@4.0.5)(svelte@5.56.9)(typescript@6.0.3):
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
@@ -780,11 +978,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@typescript-eslint/types' - '@typescript-eslint/types'
tinybench@2.9.0: {}
tinyexec@1.3.0: {}
tinyglobby@0.2.17: tinyglobby@0.2.17:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.5) fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5 picomatch: 4.0.5
tinyrainbow@3.1.1: {}
totalist@3.0.1: {} totalist@3.0.1: {}
typescript@6.0.3: {} typescript@6.0.3: {}
@@ -803,4 +1007,34 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 8.2.1 vite: 8.2.1
vitest@4.1.11(vite@8.2.1):
dependencies:
'@vitest/expect': 4.1.11
'@vitest/mocker': 4.1.11(vite@8.2.1)
'@vitest/pretty-format': 4.1.11
'@vitest/runner': 4.1.11
'@vitest/snapshot': 4.1.11
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
es-module-lexer: 2.3.2
expect-type: 1.4.0
magic-string: 0.30.21
obug: 2.1.4
pathe: 2.0.3
picomatch: 4.0.5
std-env: 4.2.0
tinybench: 2.9.0
tinyexec: 1.3.0
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
vite: 8.2.1
why-is-node-running: 2.3.0
transitivePeerDependencies:
- msw
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
zimmerframe@1.1.4: {} zimmerframe@1.1.4: {}
+68
View File
@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest';
import { parseHex, removeColorToAlpha } from './alpha';
import { makeImage } from './test-helpers';
describe('removeColorToAlpha', () => {
const fixture = () =>
makeImage(2, 1, [
[255, 255, 255, 255],
[255, 0, 0, 255]
]);
it('обнуляет альфу только у точного совпадения при tolerance=0', () => {
const out = removeColorToAlpha(fixture(), '#ff0000', 0);
expect(out.data[3]).toBe(255);
expect(out.data[7]).toBe(0);
expect(out.data[4]).toBe(255);
expect(out.data[5]).toBe(0);
});
it('поддерживает короткую форму и регистр hex', () => {
expect([...removeColorToAlpha(fixture(), '#f00', 0).data.slice(4)]).toEqual([
255, 0, 0, 0
]);
expect([...removeColorToAlpha(fixture(), '#FF0000', 0).data.slice(4)]).toEqual([
255, 0, 0, 0
]);
});
it('tolerance 100% удаляет весь диапазон расстояний', () => {
const out = removeColorToAlpha(fixture(), '#000000', 100);
expect(out.data[3]).toBe(0);
expect(out.data[7]).toBe(0);
});
it('промежуточный tolerance различает близкие и далёкие цвета', () => {
const img = makeImage(2, 1, [
[0, 0, 0, 255],
[128, 128, 128, 255]
]);
const kept = removeColorToAlpha(img, '#000000', 40);
const removed = removeColorToAlpha(img, '#000000', 60);
expect(kept.data[3]).toBe(0);
expect(kept.data[7]).toBe(255);
expect(removed.data[3]).toBe(0);
expect(removed.data[7]).toBe(0);
});
it('не мутирует вход', () => {
const img = fixture();
removeColorToAlpha(img, '#ffffff', 100);
expect([...img.data]).toEqual([
255, 255, 255, 255,
255, 0, 0, 255
]);
});
});
describe('parseHex', () => {
it('разбирает #rrggbb, rrggbb, #rgb', () => {
expect(parseHex('#ff8040')).toEqual([255, 128, 64]);
expect(parseHex('ff8040')).toEqual([255, 128, 64]);
expect(parseHex('#F80')).toEqual([255, 136, 0]);
});
it.each(['zzz', '12345', '##ff', ''])('бросает ошибку на "%s"', (bad) => {
expect(() => parseHex(bad)).toThrow(/Некорректный HEX/);
});
});
+47
View File
@@ -0,0 +1,47 @@
import type { PixelImage } from './types';
const MAX_COLOR_DISTANCE = Math.sqrt(3 * 255 * 255);
export function removeColorToAlpha(
img: PixelImage,
hex: string,
tolerancePercent = 0
): PixelImage {
const [targetR, targetG, targetB] = parseHex(hex);
const tolerance = (clamp(tolerancePercent, 0, 100) / 100) * MAX_COLOR_DISTANCE;
const thresholdSq = tolerance * tolerance;
const out: PixelImage = { width: img.width, height: img.height, data: img.data.slice() };
for (let i = 0; i < out.data.length; i += 4) {
const dr = out.data[i] - targetR;
const dg = out.data[i + 1] - targetG;
const db = out.data[i + 2] - targetB;
if (dr * dr + dg * dg + db * db <= thresholdSq) {
out.data[i + 3] = 0;
}
}
return out;
}
export function parseHex(hex: string): [number, number, number] {
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
if (!match) {
throw new Error(`Некорректный HEX-цвет: "${hex}"`);
}
const digits = match[1];
if (digits.length === 3) {
return [
parseInt(digits[0] + digits[0], 16),
parseInt(digits[1] + digits[1], 16),
parseInt(digits[2] + digits[2], 16)
];
}
return [
parseInt(digits.slice(0, 2), 16),
parseInt(digits.slice(2, 4), 16),
parseInt(digits.slice(4, 6), 16)
];
}
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import { imageInfo } from './analyze';
import { makeImage } from './test-helpers';
describe('imageInfo', () => {
it('находит полупрозрачные пиксели и считает уникальные RGBA-цвета', () => {
const info = imageInfo(
makeImage(2, 2, [
[0, 0, 0, 255],
[0, 0, 0, 255],
[255, 0, 0, 128],
[255, 0, 0, 128]
])
);
expect(info).toEqual({ width: 2, height: 2, hasAlpha: true, colorCount: 2 });
});
it('полностью непрозрачное изображение — hasAlpha false', () => {
const info = imageInfo(
makeImage(2, 1, [
[10, 20, 30, 255],
[40, 50, 60, 255]
])
);
expect(info.hasAlpha).toBe(false);
expect(info.colorCount).toBe(2);
});
it('разная альфа означает разные цвета', () => {
const info = imageInfo(
makeImage(2, 1, [
[255, 0, 0, 255],
[255, 0, 0, 128]
])
);
expect(info.hasAlpha).toBe(true);
expect(info.colorCount).toBe(2);
});
it('возвращает корректные размеры неквадрата', () => {
const info = imageInfo(makeImage(5, 3, new Array(15).fill([1, 2, 3, 4])));
expect(info.width).toBe(5);
expect(info.height).toBe(3);
});
});
+26
View File
@@ -0,0 +1,26 @@
import type { PixelImage } from './types';
export type ImageInfo = {
width: number;
height: number;
hasAlpha: boolean;
colorCount: number;
};
export function imageInfo(img: PixelImage): ImageInfo {
let hasAlpha = false;
const seen = new Set<number>();
for (let i = 0; i < img.data.length; i += 4) {
if (!hasAlpha && img.data[i + 3] !== 255) {
hasAlpha = true;
}
const key =
((img.data[i] << 24) |
(img.data[i + 1] << 16) |
(img.data[i + 2] << 8) |
img.data[i + 3]) >>>
0;
seen.add(key);
}
return { width: img.width, height: img.height, hasAlpha, colorCount: seen.size };
}
+69
View File
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import { brightnessContrast, grayscale, invert } from './color';
import { makeImage } from './test-helpers';
describe('grayscale', () => {
it('считает luma по весам BT.601 с округлением', () => {
const out = grayscale(
makeImage(3, 1, [
[255, 0, 0, 255],
[0, 255, 0, 255],
[0, 0, 255, 255]
])
);
const rgb = [...out.data].reduce<number[]>((acc, v, i) => {
if (i % 4 === 0) acc.push(v);
return acc;
}, []);
expect(rgb).toEqual([76, 150, 29]);
});
it('сохраняет альфу и не мутирует вход', () => {
const img = makeImage(1, 1, [[10, 20, 30, 200]]);
const out = grayscale(img);
expect([...out.data]).toEqual([18, 18, 18, 200]);
expect([...img.data]).toEqual([10, 20, 30, 200]);
});
});
describe('invert', () => {
it('инвертирует RGB, не трогая альфу', () => {
const out = invert(makeImage(1, 1, [[10, 200, 30, 7]]));
expect([...out.data]).toEqual([245, 55, 225, 7]);
});
});
describe('brightnessContrast', () => {
const pixel = (r: number) => makeImage(1, 1, [[r, r, r, 255]]);
const red = (out: number[]) => [out[0], out[1], out[2]];
it('b=0, c=0 — тождественное преобразование', () => {
const out = brightnessContrast(pixel(77), 0, 0);
expect(red([...out.data])).toEqual([77, 77, 77]);
});
it('brightness +100 насыщает всё в белый', () => {
const out = brightnessContrast(pixel(10), 100, 0);
expect(red([...out.data])).toEqual([255, 255, 255]);
});
it('brightness -100 заливает чёрным', () => {
const out = brightnessContrast(pixel(240), -100, 0);
expect(red([...out.data])).toEqual([0, 0, 0]);
});
it('contrast -100 сводит всё к серому 128', () => {
const out = brightnessContrast(pixel(30), 0, -100);
expect(red([...out.data])).toEqual([128, 128, 128]);
});
it('параметры вне диапазона клампятся', () => {
const out = brightnessContrast(pixel(10), 150, 0);
expect(red([...out.data])).toEqual([255, 255, 255]);
});
it('альфа не меняется', () => {
const out = brightnessContrast(makeImage(1, 1, [[10, 20, 30, 64]]), 50, 50);
expect(out.data[3]).toBe(64);
});
});
+47
View File
@@ -0,0 +1,47 @@
import { createPixelImage, type PixelImage } from './types';
export function grayscale(img: PixelImage): PixelImage {
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < img.data.length; i += 4) {
const luma = 0.299 * img.data[i] + 0.587 * img.data[i + 1] + 0.114 * img.data[i + 2];
out.data[i] = luma;
out.data[i + 1] = luma;
out.data[i + 2] = luma;
out.data[i + 3] = img.data[i + 3];
}
return out;
}
export function invert(img: PixelImage): PixelImage {
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < img.data.length; i += 4) {
out.data[i] = 255 - img.data[i];
out.data[i + 1] = 255 - img.data[i + 1];
out.data[i + 2] = 255 - img.data[i + 2];
out.data[i + 3] = img.data[i + 3];
}
return out;
}
export function brightnessContrast(
img: PixelImage,
brightness: number,
contrast: number
): PixelImage {
const offset = (clamp(brightness, -100, 100) / 100) * 255;
const c = (clamp(contrast, -100, 100) / 100) * 255;
const factor = (259 * (c + 255)) / (255 * (259 - c));
const out = createPixelImage(img.width, img.height);
for (let i = 0; i < img.data.length; i += 4) {
for (let ch = 0; ch < 3; ch++) {
const v = factor * (img.data[i + ch] + offset - 128) + 128;
out.data[i + ch] = v;
}
out.data[i + 3] = img.data[i + 3];
}
return out;
}
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
+157
View File
@@ -0,0 +1,157 @@
import { describe, expect, it } from 'vitest';
import { crop, flip, resize, rotate90 } from './geometry';
import { makeImage } from './test-helpers';
const square = () =>
makeImage(2, 2, [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[4, 4, 4, 4]
]);
describe('flip', () => {
it('отражает по горизонтали (зеркало слева-направо)', () => {
const out = flip(square(), 'horizontal');
expect(out.width).toBe(2);
expect(out.height).toBe(2);
expect([...out.data]).toEqual([
2, 2, 2, 2,
1, 1, 1, 1,
4, 4, 4, 4,
3, 3, 3, 3
]);
});
it('отражает по вертикали (сверху-вниз)', () => {
const out = flip(square(), 'vertical');
expect([...out.data]).toEqual([
3, 3, 3, 3,
4, 4, 4, 4,
1, 1, 1, 1,
2, 2, 2, 2
]);
});
it('не мутирует вход', () => {
const img = square();
flip(img, 'horizontal');
expect([...img.data]).toEqual([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4]);
});
});
describe('rotate90', () => {
it('поворачивает неквадрат 2x3 на 90° по часовой', () => {
const rect = makeImage(2, 3, [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[4, 4, 4, 4],
[5, 5, 5, 5],
[6, 6, 6, 6]
]);
const out = rotate90(rect, 1);
expect(out.width).toBe(3);
expect(out.height).toBe(2);
expect([...out.data]).toEqual([
5, 5, 5, 5,
3, 3, 3, 3,
1, 1, 1, 1,
6, 6, 6, 6,
4, 4, 4, 4,
2, 2, 2, 2
]);
});
it('turns=0 возвращает копию без изменений', () => {
const img = square();
const out = rotate90(img, 0);
expect(out).not.toBe(img);
expect([...out.data]).toEqual([...img.data]);
});
it('нормализует turns: 5 ≡ 1, -3 ≡ 1, 4 ≡ 0', () => {
const once = [...rotate90(square(), 1).data];
expect([...rotate90(square(), 5).data]).toEqual(once);
expect([...rotate90(square(), -3).data]).toEqual(once);
expect([...rotate90(square(), 4).data]).toEqual([...square().data]);
});
it('turns=2 на квадрате равно двойному отражению', () => {
const out = rotate90(square(), 2);
expect([...out.data]).toEqual([
4, 4, 4, 4,
3, 3, 3, 3,
2, 2, 2, 2,
1, 1, 1, 1
]);
});
});
describe('crop', () => {
const grid = () =>
makeImage(3, 3, [
[1, 1, 1, 1],
[2, 2, 2, 2],
[3, 3, 3, 3],
[4, 4, 4, 4],
[5, 5, 5, 5],
[6, 6, 6, 6],
[7, 7, 7, 7],
[8, 8, 8, 8],
[9, 9, 9, 9]
]);
it('вырезает центральную область 2x2 из 3x3', () => {
const out = crop(grid(), 1, 1, 2, 2);
expect(out.width).toBe(2);
expect(out.height).toBe(2);
expect([...out.data]).toEqual([
5, 5, 5, 5,
6, 6, 6, 6,
8, 8, 8, 8,
9, 9, 9, 9
]);
});
it('усекает область, выходящую за границы', () => {
const out = crop(grid(), -1, -1, 2, 2);
expect(out.width).toBe(1);
expect(out.height).toBe(1);
expect([...out.data]).toEqual([1, 1, 1, 1]);
});
it('бросает RangeError для области вне изображения', () => {
expect(() => crop(grid(), 5, 5, 2, 2)).toThrow(RangeError);
});
});
describe('resize', () => {
const twoByTwo = () =>
makeImage(2, 2, [
[0, 0, 0, 255],
[100, 100, 100, 255],
[200, 200, 200, 255],
[255, 255, 255, 255]
]);
it('совпадает с входом при тех же размерах', () => {
const img = twoByTwo();
expect([...resize(img, 2, 2).data]).toEqual([...img.data]);
});
it('апскейл 2x2 -> 4x4 билинейно интерполирует', () => {
const out = resize(twoByTwo(), 4, 4);
expect(out.width).toBe(4);
expect(out.height).toBe(4);
const red = [...out.data].filter((_, i) => i % 4 === 0);
expect(red.slice(0, 4)).toEqual([0, 25, 75, 100]);
expect(red.slice(4, 8)).toEqual([50, 72, 117, 139]);
});
it('бросает RangeError на некорректные размеры', () => {
const img = twoByTwo();
expect(() => resize(img, 0, 10)).toThrow(RangeError);
expect(() => resize(img, 10.5, 10)).toThrow(RangeError);
});
});
+106
View File
@@ -0,0 +1,106 @@
import { clonePixelImage, createPixelImage, type PixelImage } from './types';
export type FlipAxis = 'horizontal' | 'vertical';
export function flip(img: PixelImage, axis: FlipAxis): PixelImage {
const out = createPixelImage(img.width, img.height);
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const sx = axis === 'horizontal' ? img.width - 1 - x : x;
const sy = axis === 'vertical' ? img.height - 1 - y : y;
copyPixel(img, sx, sy, out, x, y);
}
}
return out;
}
export function rotate90(img: PixelImage, turns: number): PixelImage {
const t = ((Math.trunc(turns) % 4) + 4) % 4;
if (t === 0) return clonePixelImage(img);
let current = img;
for (let i = 0; i < t; i++) {
current = rotateClockwise(current);
}
return current;
}
function rotateClockwise(img: PixelImage): PixelImage {
const out = createPixelImage(img.height, img.width);
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
copyPixel(img, x, y, out, img.height - 1 - y, x);
}
}
return out;
}
export function crop(
img: PixelImage,
x: number,
y: number,
width: number,
height: number
): PixelImage {
const sx = clampInt(x, 0, img.width);
const sy = clampInt(y, 0, img.height);
const ex = clampInt(x + width, sx, img.width);
const ey = clampInt(y + height, sy, img.height);
const w = ex - sx;
const h = ey - sy;
if (w <= 0 || h <= 0) {
throw new RangeError('Область обрезки пуста: она целиком вне изображения');
}
const out = createPixelImage(w, h);
for (let row = 0; row < h; row++) {
const si = (sy + row) * img.width * 4 + sx * 4;
out.data.set(img.data.subarray(si, si + w * 4), row * w * 4);
}
return out;
}
export function resize(img: PixelImage, width: number, height: number): PixelImage {
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
throw new RangeError('Размеры должны быть целыми числами >= 1');
}
const out = createPixelImage(width, height);
const xr = img.width / width;
const yr = img.height / height;
const maxX = img.width - 1;
const maxY = img.height - 1;
for (let dy = 0; dy < height; dy++) {
const fy = Math.min(Math.max((dy + 0.5) * yr - 0.5, 0), maxY);
const y0 = Math.floor(fy);
const ty = fy - y0;
const y1 = Math.min(y0 + 1, maxY);
for (let dx = 0; dx < width; dx++) {
const fx = Math.min(Math.max((dx + 0.5) * xr - 0.5, 0), maxX);
const x0 = Math.floor(fx);
const tx = fx - x0;
const x1 = Math.min(x0 + 1, maxX);
const di = (dy * width + dx) * 4;
for (let ch = 0; ch < 4; ch++) {
const p00 = img.data[(y0 * img.width + x0) * 4 + ch];
const p10 = img.data[(y0 * img.width + x1) * 4 + ch];
const p01 = img.data[(y1 * img.width + x0) * 4 + ch];
const p11 = img.data[(y1 * img.width + x1) * 4 + ch];
const top = (1 - tx) * p00 + tx * p10;
const bottom = (1 - tx) * p01 + tx * p11;
out.data[di + ch] = (1 - ty) * top + ty * bottom;
}
}
}
return out;
}
function copyPixel(src: PixelImage, sx: number, sy: number, dst: PixelImage, dx: number, dy: number): void {
const si = (sy * src.width + sx) * 4;
const di = (dy * dst.width + dx) * 4;
dst.data[di] = src.data[si];
dst.data[di + 1] = src.data[si + 1];
dst.data[di + 2] = src.data[si + 2];
dst.data[di + 3] = src.data[si + 3];
}
function clampInt(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, Math.trunc(value)));
}
+14
View File
@@ -0,0 +1,14 @@
import { expect } from 'vitest';
import type { PixelImage } from './types';
export function makeImage(width: number, height: number, pixels: number[][]): PixelImage {
const data = new Uint8ClampedArray(pixels.flat());
if (data.length !== width * height * 4) {
throw new Error(`Fixture mismatch: ${data.length} байт на ${width}x${height}`);
}
return { width, height, data };
}
export function expectImageEqual(actual: PixelImage, expectedPixels: number[][]): void {
expect([...actual.data]).toEqual(expectedPixels.flat());
}
+4
View File
@@ -11,3 +11,7 @@ export function createPixelImage(width: number, height: number): PixelImage {
data: new Uint8ClampedArray(width * height * 4) data: new Uint8ClampedArray(width * height * 4)
}; };
} }
export function clonePixelImage(img: PixelImage): PixelImage {
return { width: img.width, height: img.height, data: img.data.slice() };
}
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
environment: 'node'
}
});