This commit is contained in:
Denis Savitskiy 2026-06-09 20:16:58 +00:00 committed by GitHub
commit d839f7ed4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 460 additions and 175 deletions

2
components.d.ts vendored
View file

@ -78,6 +78,7 @@ declare module '@vue/runtime-core' {
EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default']
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
FaviconGenerator: typeof import('./src/tools/favicon-generator/favicon-generator.vue')['default']
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
@ -129,6 +130,7 @@ declare module '@vue/runtime-core' {
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']

View file

@ -72,6 +72,7 @@
"ibantools": "^4.3.3",
"js-base64": "^3.7.6",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
"lodash": "^4.17.21",
@ -105,6 +106,11 @@
"xml-js": "^1.6.11",
"yaml": "^2.2.1"
},
"pnpm": {
"overrides": {
"@vueuse/shared": "10.3.0"
}
},
"devDependencies": {
"@antfu/eslint-config": "^0.41.0",
"@iconify-json/mdi": "^1.1.50",

285
pnpm-lock.yaml generated
View file

@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@vueuse/shared': 10.3.0
importers:
.:
@ -49,7 +52,7 @@ importers:
version: 10.3.0(vue@3.3.4)
'@vueuse/head':
specifier: ^1.0.0
version: 1.0.0(typescript@5.2.2)(vue@3.3.4)
version: 1.0.0(vue@3.3.4)
'@vueuse/router':
specifier: ^10.0.0
version: 10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4)
@ -113,6 +116,9 @@ importers:
json5:
specifier: ^2.2.3
version: 2.2.3
jszip:
specifier: ^3.10.1
version: 3.10.1
jwt-decode:
specifier: ^3.1.2
version: 3.1.2
@ -2303,9 +2309,6 @@ packages:
'@vue/compiler-core@3.3.7':
resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==}
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
'@vue/compiler-dom@3.2.47':
resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
@ -2315,18 +2318,12 @@ packages:
'@vue/compiler-dom@3.3.7':
resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==}
'@vue/compiler-dom@3.5.13':
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
'@vue/compiler-sfc@3.2.47':
resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
'@vue/compiler-sfc@3.3.4':
resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
'@vue/compiler-sfc@3.5.13':
resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
'@vue/compiler-ssr@3.2.47':
resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
@ -2336,9 +2333,6 @@ packages:
'@vue/compiler-ssr@3.3.7':
resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==}
'@vue/compiler-ssr@3.5.13':
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
'@vue/devtools-api@6.5.0':
resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
@ -2359,21 +2353,12 @@ packages:
'@vue/reactivity@3.3.4':
resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
'@vue/runtime-core@3.3.4':
resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==}
'@vue/runtime-core@3.5.13':
resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
'@vue/runtime-dom@3.3.4':
resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==}
'@vue/runtime-dom@3.5.13':
resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
'@vue/server-renderer@3.3.4':
resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==}
peerDependencies:
@ -2384,11 +2369,6 @@ packages:
peerDependencies:
vue: 3.3.7
'@vue/server-renderer@3.5.13':
resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
peerDependencies:
vue: 3.5.13
'@vue/shared@3.2.47':
resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
@ -2398,9 +2378,6 @@ packages:
'@vue/shared@3.3.7':
resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==}
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vue/test-utils@2.3.2':
resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==}
peerDependencies:
@ -2428,15 +2405,9 @@ packages:
peerDependencies:
vue-router: '>=4.0.0-rc.1'
'@vueuse/shared@10.0.0':
resolution: {integrity: sha512-Zh3LgJqvUBWVY3SiMvXanTcfAneGbt63QPczBRDNgQ6jd/ehodO9a1lCFzaA6SWJJoI+ugVTjHFYJdoR656DVQ==}
'@vueuse/shared@10.3.0':
resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
'@vueuse/shared@12.0.0':
resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==}
'@zhead/schema@1.0.0-beta.13':
resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==}
@ -2831,6 +2802,9 @@ packages:
core-js-compat@3.39.0:
resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
country-code-lookup@0.1.0:
resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==}
@ -2896,9 +2870,6 @@ packages:
csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dash-get@1.0.2:
resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==}
@ -3246,6 +3217,7 @@ packages:
eslint-plugin-i@2.28.0-2:
resolution: {integrity: sha512-z48kG4qmE4TmiLcxbmvxMT5ycwvPkXaWW0XpU1L768uZaTbiDbxsHMEdV24JHlOR1xDsPpKW39BfP/pRdYIwFA==}
engines: {node: '>=12'}
deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead
peerDependencies:
eslint: ^7.2.0 || ^8
@ -3649,10 +3621,6 @@ packages:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
hasown@2.0.0:
resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@ -3737,6 +3705,9 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@ -3974,6 +3945,9 @@ packages:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@ -4096,6 +4070,9 @@ packages:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
jwt-decode@3.1.2:
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
@ -4122,6 +4099,9 @@ packages:
libphonenumber-js@1.10.28:
resolution: {integrity: sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw==}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -4498,6 +4478,9 @@ packages:
package-manager-detector@0.2.7:
resolution: {integrity: sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
param-case@2.1.1:
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
@ -4665,6 +4648,9 @@ packages:
resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
prosemirror-changeset@2.2.1:
resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==}
@ -4788,6 +4774,9 @@ packages:
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
engines: {node: '>=8'}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@ -4868,10 +4857,6 @@ packages:
resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==}
hasBin: true
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
resolve@1.22.9:
resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==}
hasBin: true
@ -4919,6 +4904,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -4996,6 +4984,9 @@ packages:
resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==}
engines: {node: '>=11.0'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -5071,6 +5062,7 @@ packages:
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@ -5132,6 +5124,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@ -5701,14 +5696,6 @@ packages:
vue@3.3.4:
resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
vuedraggable@4.1.0:
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
peerDependencies:
@ -6117,7 +6104,7 @@ snapshots:
'@babel/traverse': 7.23.2
'@babel/types': 7.23.0
convert-source-map: 2.0.0
debug: 4.3.4
debug: 4.4.0
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -7000,7 +6987,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.22.10
'@babel/types': 7.22.10
debug: 4.3.4
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -7015,7 +7002,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.5
'@babel/parser': 7.22.5
'@babel/types': 7.22.5
debug: 4.3.4
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -7030,7 +7017,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.0
'@babel/types': 7.23.0
debug: 4.3.4
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -7228,7 +7215,7 @@ snapshots:
'@eslint/eslintrc@2.1.2':
dependencies:
ajv: 6.12.6
debug: 4.3.4
debug: 4.4.0
espree: 9.6.1
globals: 13.20.0
ignore: 5.2.4
@ -7244,7 +7231,7 @@ snapshots:
'@humanwhocodes/config-array@0.11.10':
dependencies:
'@humanwhocodes/object-schema': 1.2.1
debug: 4.3.4
debug: 4.4.0
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -7401,7 +7388,7 @@ snapshots:
'@linaria/logger@4.0.0':
dependencies:
debug: 4.3.4
debug: 4.4.0
picocolors: 1.0.0
transitivePeerDependencies:
- supports-color
@ -7922,7 +7909,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2)
'@typescript-eslint/utils': 6.4.1(eslint@8.47.0)(typescript@5.2.2)
debug: 4.3.4
debug: 4.4.0
eslint: 8.47.0
ts-api-utils: 1.0.1(typescript@5.2.2)
optionalDependencies:
@ -7940,10 +7927,10 @@ snapshots:
dependencies:
'@typescript-eslint/types': 5.60.0
'@typescript-eslint/visitor-keys': 5.60.0
debug: 4.3.4
debug: 4.4.0
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
semver: 7.6.3
tsutils: 3.21.0(typescript@5.2.2)
optionalDependencies:
typescript: 5.2.2
@ -7954,10 +7941,10 @@ snapshots:
dependencies:
'@typescript-eslint/types': 6.4.1
'@typescript-eslint/visitor-keys': 6.4.1
debug: 4.3.4
debug: 4.4.0
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
semver: 7.6.3
ts-api-utils: 1.0.1(typescript@5.2.2)
optionalDependencies:
typescript: 5.2.2
@ -7968,10 +7955,10 @@ snapshots:
dependencies:
'@typescript-eslint/types': 6.9.1
'@typescript-eslint/visitor-keys': 6.9.1
debug: 4.3.4
debug: 4.4.0
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
semver: 7.6.3
ts-api-utils: 1.0.1(typescript@5.2.2)
optionalDependencies:
typescript: 5.2.2
@ -7988,7 +7975,7 @@ snapshots:
'@typescript-eslint/typescript-estree': 5.60.0(typescript@5.2.2)
eslint: 8.47.0
eslint-scope: 5.1.1
semver: 7.5.4
semver: 7.6.3
transitivePeerDependencies:
- supports-color
- typescript
@ -8002,7 +7989,7 @@ snapshots:
'@typescript-eslint/types': 6.4.1
'@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2)
eslint: 8.47.0
semver: 7.5.4
semver: 7.6.3
transitivePeerDependencies:
- supports-color
- typescript
@ -8016,7 +8003,7 @@ snapshots:
'@typescript-eslint/types': 6.9.1
'@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2)
eslint: 8.47.0
semver: 7.5.4
semver: 7.6.3
transitivePeerDependencies:
- supports-color
- typescript
@ -8049,15 +8036,15 @@ snapshots:
dependencies:
'@unhead/schema': 0.5.1
'@unhead/vue@0.5.1(typescript@5.2.2)(vue@3.3.4)':
'@unhead/vue@0.5.1(vue@3.3.4)':
dependencies:
'@unhead/dom': 0.5.1
'@unhead/schema': 0.5.1
'@vueuse/shared': 12.0.0(typescript@5.2.2)
'@vueuse/shared': 10.3.0(vue@3.3.4)
unhead: 0.5.1
vue: 3.3.4
transitivePeerDependencies:
- typescript
- '@vue/composition-api'
'@unocss/astro@0.65.1(rollup@2.79.2)(vite@4.4.9(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0))(vue@3.3.4)':
dependencies:
@ -8341,14 +8328,6 @@ snapshots:
source-map-js: 1.0.2
optional: true
'@vue/compiler-core@3.5.13':
dependencies:
'@babel/parser': 7.26.3
'@vue/shared': 3.5.13
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.2.47':
dependencies:
'@vue/compiler-core': 3.2.47
@ -8365,11 +8344,6 @@ snapshots:
'@vue/shared': 3.3.7
optional: true
'@vue/compiler-dom@3.5.13':
dependencies:
'@vue/compiler-core': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-sfc@3.2.47':
dependencies:
'@babel/parser': 7.21.4
@ -8396,18 +8370,6 @@ snapshots:
postcss: 8.4.28
source-map-js: 1.0.2
'@vue/compiler-sfc@3.5.13':
dependencies:
'@babel/parser': 7.26.3
'@vue/compiler-core': 3.5.13
'@vue/compiler-dom': 3.5.13
'@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13
estree-walker: 2.0.2
magic-string: 0.30.15
postcss: 8.4.49
source-map-js: 1.2.1
'@vue/compiler-ssr@3.2.47':
dependencies:
'@vue/compiler-dom': 3.2.47
@ -8424,11 +8386,6 @@ snapshots:
'@vue/shared': 3.3.7
optional: true
'@vue/compiler-ssr@3.5.13':
dependencies:
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
'@vue/devtools-api@6.5.0': {}
'@vue/language-core@1.8.1(typescript@5.2.2)':
@ -8464,33 +8421,17 @@ snapshots:
dependencies:
'@vue/shared': 3.3.4
'@vue/reactivity@3.5.13':
dependencies:
'@vue/shared': 3.5.13
'@vue/runtime-core@3.3.4':
dependencies:
'@vue/reactivity': 3.3.4
'@vue/shared': 3.3.4
'@vue/runtime-core@3.5.13':
dependencies:
'@vue/reactivity': 3.5.13
'@vue/shared': 3.5.13
'@vue/runtime-dom@3.3.4':
dependencies:
'@vue/runtime-core': 3.3.4
'@vue/shared': 3.3.4
csstype: 3.1.2
'@vue/runtime-dom@3.5.13':
dependencies:
'@vue/reactivity': 3.5.13
'@vue/runtime-core': 3.5.13
'@vue/shared': 3.5.13
csstype: 3.1.3
'@vue/server-renderer@3.3.4(vue@3.3.4)':
dependencies:
'@vue/compiler-ssr': 3.3.4
@ -8504,12 +8445,6 @@ snapshots:
vue: 3.3.4
optional: true
'@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.2.2))':
dependencies:
'@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13
vue: 3.5.13(typescript@5.2.2)
'@vue/shared@3.2.47': {}
'@vue/shared@3.3.4': {}
@ -8517,8 +8452,6 @@ snapshots:
'@vue/shared@3.3.7':
optional: true
'@vue/shared@3.5.13': {}
'@vue/test-utils@2.3.2(vue@3.3.4)':
dependencies:
js-beautify: 1.14.6
@ -8546,33 +8479,26 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/head@1.0.0(typescript@5.2.2)(vue@3.3.4)':
'@vueuse/head@1.0.0(vue@3.3.4)':
dependencies:
'@unhead/schema': 0.5.1
'@unhead/ssr': 0.5.1
'@unhead/vue': 0.5.1(typescript@5.2.2)(vue@3.3.4)
'@unhead/vue': 0.5.1(vue@3.3.4)
vue: 3.3.4
transitivePeerDependencies:
- typescript
- '@vue/composition-api'
'@vueuse/metadata@10.3.0': {}
'@vueuse/router@10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4)':
dependencies:
'@vueuse/shared': 10.0.0(vue@3.3.4)
'@vueuse/shared': 10.3.0(vue@3.3.4)
vue-demi: 0.14.5(vue@3.3.4)
vue-router: 4.1.6(vue@3.3.4)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@10.0.0(vue@3.3.4)':
dependencies:
vue-demi: 0.14.5(vue@3.3.4)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@10.3.0(vue@3.3.4)':
dependencies:
vue-demi: 0.14.5(vue@3.3.4)
@ -8580,12 +8506,6 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/shared@12.0.0(typescript@5.2.2)':
dependencies:
vue: 3.5.13(typescript@5.2.2)
transitivePeerDependencies:
- typescript
'@zhead/schema@1.0.0-beta.13': {}
abab@2.0.6: {}
@ -8608,7 +8528,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.3.4
debug: 4.4.0
transitivePeerDependencies:
- supports-color
@ -9022,6 +8942,8 @@ snapshots:
dependencies:
browserslist: 4.24.3
core-util-is@1.0.3: {}
country-code-lookup@0.1.0: {}
crelt@1.0.6: {}
@ -9084,8 +9006,6 @@ snapshots:
csstype@3.1.2: {}
csstype@3.1.3: {}
dash-get@1.0.2: {}
data-uri-to-buffer@4.0.1: {}
@ -9971,10 +9891,6 @@ snapshots:
dependencies:
function-bind: 1.1.2
hasown@2.0.0:
dependencies:
function-bind: 1.1.2
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@ -10016,14 +9932,14 @@ snapshots:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
debug: 4.3.4
debug: 4.4.0
transitivePeerDependencies:
- supports-color
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.3.4
debug: 4.4.0
transitivePeerDependencies:
- supports-color
@ -10071,6 +9987,8 @@ snapshots:
image-size@0.5.5:
optional: true
immediate@3.0.6: {}
import-fresh@3.3.0:
dependencies:
parent-module: 1.0.1
@ -10162,7 +10080,7 @@ snapshots:
is-core-module@2.13.1:
dependencies:
hasown: 2.0.0
hasown: 2.0.2
is-core-module@2.16.0:
dependencies:
@ -10293,6 +10211,8 @@ snapshots:
dependencies:
is-docker: 2.2.1
isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {}
@ -10412,6 +10332,13 @@ snapshots:
jsonpointer@5.0.1: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
jwt-decode@3.1.2: {}
kind-of@6.0.3: {}
@ -10443,6 +10370,10 @@ snapshots:
libphonenumber-js@1.10.28: {}
lie@3.3.0:
dependencies:
immediate: 3.0.6
lines-and-columns@1.2.4: {}
linkify-it@4.0.1:
@ -10753,7 +10684,7 @@ snapshots:
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9
resolve: 1.22.8
resolve: 1.22.9
semver: 5.7.2
validate-npm-package-license: 3.0.4
@ -10857,6 +10788,8 @@ snapshots:
package-manager-detector@0.2.7: {}
pako@1.0.11: {}
param-case@2.1.1:
dependencies:
no-case: 2.3.2
@ -11017,6 +10950,8 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.2.0
process-nextick-args@2.0.1: {}
prosemirror-changeset@2.2.1:
dependencies:
prosemirror-transform: 1.7.3
@ -11184,6 +11119,16 @@ snapshots:
parse-json: 5.2.0
type-fest: 0.6.0
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@ -11271,12 +11216,6 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
resolve@1.22.8:
dependencies:
is-core-module: 2.13.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
resolve@1.22.9:
dependencies:
is-core-module: 2.16.0
@ -11322,6 +11261,8 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safe-regex-test@1.1.0:
@ -11401,6 +11342,8 @@ snapshots:
is-plain-object: 2.0.4
is-primitive: 3.0.1
setimmediate@1.0.5: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -11565,6 +11508,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@ -12018,7 +11965,7 @@ snapshots:
vite-node@0.34.0(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0):
dependencies:
cac: 6.7.14
debug: 4.3.4
debug: 4.4.0
mlly: 1.4.0
pathe: 1.1.1
picocolors: 1.0.0
@ -12180,16 +12127,6 @@ snapshots:
'@vue/server-renderer': 3.3.4(vue@3.3.4)
'@vue/shared': 3.3.4
vue@3.5.13(typescript@5.2.2):
dependencies:
'@vue/compiler-dom': 3.5.13
'@vue/compiler-sfc': 3.5.13
'@vue/runtime-dom': 3.5.13
'@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.2.2))
'@vue/shared': 3.5.13
optionalDependencies:
typescript: 5.2.2
vuedraggable@4.1.0(vue@3.3.4):
dependencies:
sortablejs: 1.14.0

View file

@ -0,0 +1,15 @@
import { expect, test } from '@playwright/test';
test.describe('Tool - Favicon generator', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/favicon-generator');
});
test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('Favicon generator - IT Tools');
});
test('', async () => {
});
});

View file

@ -0,0 +1,96 @@
import { describe, expect, it } from 'vitest';
import { FAVICON_SIZES, buildIcoBuffer, generateSiteWebmanifest } from './favicon-generator.service';
describe('generateSiteWebmanifest', () => {
it('produces valid JSON', () => {
const result = generateSiteWebmanifest();
expect(() => JSON.parse(result)).not.toThrow();
});
it('uses the provided app name', () => {
const manifest = JSON.parse(generateSiteWebmanifest('Acme'));
expect(manifest.name).toBe('Acme');
expect(manifest.short_name).toBe('Acme');
});
it('includes 192x192 and 512x512 icon entries', () => {
const manifest = JSON.parse(generateSiteWebmanifest());
const sizes = manifest.icons.map((i: { sizes: string }) => i.sizes);
expect(sizes).toContain('192x192');
expect(sizes).toContain('512x512');
});
it('sets display to standalone', () => {
const manifest = JSON.parse(generateSiteWebmanifest());
expect(manifest.display).toBe('standalone');
});
});
function fakeBuffer(size: number): ArrayBuffer {
return new Uint8Array(size).buffer;
}
describe('buildIcoBuffer', () => {
it('starts with ICO magic bytes (reserved=0, type=1)', () => {
const buf = buildIcoBuffer([fakeBuffer(4), fakeBuffer(4)], [16, 32]);
const view = new DataView(buf);
expect(view.getUint16(0, true)).toBe(0);
expect(view.getUint16(2, true)).toBe(1);
});
it('writes correct image count', () => {
const buf = buildIcoBuffer([fakeBuffer(4), fakeBuffer(4), fakeBuffer(4)], [16, 32, 48]);
const view = new DataView(buf);
expect(view.getUint16(4, true)).toBe(3);
});
it('embeds png data after directory entries', () => {
const pngData = new Uint8Array([1, 2, 3, 4]);
const buf = buildIcoBuffer([pngData.buffer], [16]);
const view = new DataView(buf);
const dataOffset = view.getUint32(6 + 12, true);
const embedded = new Uint8Array(buf, dataOffset, 4);
expect(Array.from(embedded)).toEqual([1, 2, 3, 4]);
});
it('records correct byte size in directory entry', () => {
const buf = buildIcoBuffer([fakeBuffer(100)], [32]);
const view = new DataView(buf);
expect(view.getUint32(6 + 8, true)).toBe(100);
});
it('total size equals header + dir entries + all png data', () => {
const sizes = [16, 32, 48];
const pngBuffers = [fakeBuffer(10), fakeBuffer(20), fakeBuffer(30)];
const buf = buildIcoBuffer(pngBuffers, sizes);
const expected = 6 + 16 * 3 + 10 + 20 + 30;
expect(buf.byteLength).toBe(expected);
});
});
describe('FAVICON_SIZES', () => {
it('includes all required sizes', () => {
const sizes = FAVICON_SIZES.map(s => s.size);
expect(sizes).toContain(16);
expect(sizes).toContain(32);
expect(sizes).toContain(48);
expect(sizes).toContain(180);
expect(sizes).toContain(192);
expect(sizes).toContain(512);
});
it('maps 180 to apple-touch-icon.png', () => {
const entry = FAVICON_SIZES.find(s => s.size === 180);
expect(entry?.filename).toBe('apple-touch-icon.png');
});
it('maps 16 to favicon-16x16.png', () => {
const entry = FAVICON_SIZES.find(s => s.size === 16);
expect(entry?.filename).toBe('favicon-16x16.png');
});
it('maps 32 to favicon-32x32.png', () => {
const entry = FAVICON_SIZES.find(s => s.size === 32);
expect(entry?.filename).toBe('favicon-32x32.png');
});
});

View file

@ -0,0 +1,132 @@
import JSZip from 'jszip';
export interface FaviconSize {
filename: string
size: number
}
export const FAVICON_SIZES: FaviconSize[] = [
{ filename: 'favicon-16x16.png', size: 16 },
{ filename: 'favicon-32x32.png', size: 32 },
{ filename: 'favicon-48x48.png', size: 48 },
{ filename: 'apple-touch-icon.png', size: 180 },
{ filename: 'android-chrome-192x192.png', size: 192 },
{ filename: 'android-chrome-512x512.png', size: 512 },
];
export function generateSiteWebmanifest(appName: string = 'My App'): string {
return JSON.stringify(
{
name: appName,
short_name: appName,
icons: [
{ src: '/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: '/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' },
],
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
},
null,
2,
);
}
export async function resizeImageToBlob(img: HTMLImageElement, size: number): Promise<Blob> {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not get 2D context');
}
ctx.drawImage(img, 0, 0, size, size);
return new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
}
else {
reject(new Error(`Failed to create blob for size ${size}`));
}
}, 'image/png');
});
}
const ICO_SIZES = [16, 32, 48];
export function buildIcoBuffer(pngBuffers: ArrayBuffer[], sizes: number[]): ArrayBuffer {
const headerSize = 6;
const dirEntrySize = 16;
const totalHeaderSize = headerSize + dirEntrySize * sizes.length;
let offset = totalHeaderSize;
const offsets = pngBuffers.map((buf) => {
const o = offset;
offset += buf.byteLength;
return o;
});
const buffer = new ArrayBuffer(offset);
const view = new DataView(buffer);
view.setUint16(0, 0, true);
view.setUint16(2, 1, true);
view.setUint16(4, sizes.length, true);
sizes.forEach((size, i) => {
const base = headerSize + i * dirEntrySize;
view.setUint8(base, size);
view.setUint8(base + 1, size);
view.setUint8(base + 2, 0);
view.setUint8(base + 3, 0);
view.setUint16(base + 4, 1, true);
view.setUint16(base + 6, 32, true);
view.setUint32(base + 8, pngBuffers[i].byteLength, true);
view.setUint32(base + 12, offsets[i], true);
});
const uint8 = new Uint8Array(buffer);
pngBuffers.forEach((pngBuf, i) => uint8.set(new Uint8Array(pngBuf), offsets[i]));
return buffer;
}
export async function generateIcoBlob(img: HTMLImageElement): Promise<Blob> {
const pngBlobs = await Promise.all(ICO_SIZES.map(s => resizeImageToBlob(img, s)));
const pngBuffers = await Promise.all(pngBlobs.map(b => b.arrayBuffer()));
return new Blob([buildIcoBuffer(pngBuffers, ICO_SIZES)], { type: 'image/x-icon' });
}
export async function loadImageFromFile(file: File): Promise<HTMLImageElement> {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
resolve(img);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load image'));
};
img.src = url;
});
}
export async function generateFaviconZip(file: File): Promise<Blob> {
const img = await loadImageFromFile(file);
const zip = new JSZip();
for (const { filename, size } of FAVICON_SIZES) {
const blob = await resizeImageToBlob(img, size);
zip.file(filename, blob);
}
const icoBlob = await generateIcoBlob(img);
zip.file('favicon.ico', icoBlob);
zip.file('site.webmanifest', generateSiteWebmanifest());
return zip.generateAsync({ type: 'blob' });
}

View file

@ -0,0 +1,84 @@
<script setup lang="ts">
import { FAVICON_SIZES, generateFaviconZip } from './favicon-generator.service';
const uploadedFile = ref<File | null>(null);
const previewUrl = ref<string>('');
const isGenerating = ref(false);
const errorMessage = ref('');
function onUpload(file: File) {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
}
uploadedFile.value = file;
previewUrl.value = URL.createObjectURL(file);
errorMessage.value = '';
}
async function downloadFavicons() {
if (!uploadedFile.value) {
return;
}
isGenerating.value = true;
errorMessage.value = '';
try {
const blob = await generateFaviconZip(uploadedFile.value);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'favicons.zip';
a.click();
URL.revokeObjectURL(url);
}
catch {
errorMessage.value = 'Failed to generate favicons. Make sure the uploaded file is a valid image.';
}
finally {
isGenerating.value = false;
}
}
onUnmounted(() => {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
}
});
</script>
<template>
<c-card title="Favicon generator">
<p mb-4 op-70>
Upload a square image (PNG, JPG, SVG, or WebP). All conversion happens in your browser nothing is sent to a server.
</p>
<c-file-upload
title="Drag and drop an image here, or click to select"
accept="image/*"
@file-upload="onUpload"
/>
<div v-if="previewUrl" mt-6 flex flex-col items-center gap-4>
<img
:src="previewUrl"
alt="Uploaded preview"
style="max-width: 160px; max-height: 160px; border-radius: 8px; object-fit: contain;"
>
<div text-sm op-60 text-center>
Generated files:
<span v-for="entry in FAVICON_SIZES" :key="entry.filename">
{{ entry.filename }} ({{ entry.size }}×{{ entry.size }}),
</span>
site.webmanifest
</div>
<c-button :disabled="isGenerating" @click="downloadFavicons">
{{ isGenerating ? 'Generating…' : 'Download favicons.zip' }}
</c-button>
<n-alert v-if="errorMessage" type="error" mt-2 style="width: 100%;">
{{ errorMessage }}
</n-alert>
</div>
</c-card>
</template>

View file

@ -0,0 +1,12 @@
import { Photo } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Favicon generator',
path: '/favicon-generator',
description: 'Convert any image into a complete favicon package. Upload a logo or square image — get a ZIP with favicon.ico, 16×16, 32×32, 48×48, 180×180, 192×192, and 512×512 PNGs plus site.webmanifest. Everything runs in your browser.',
keywords: ['favicon', 'generator', 'icon', 'apple-touch-icon', 'webmanifest', 'png', 'image', 'convert'],
component: () => import('./favicon-generator.vue'),
icon: Photo,
createdAt: new Date('2026-06-09'),
});

View file

@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as faviconGenerator } from './favicon-generator';
import { tool as emailNormalizer } from './email-normalizer';
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@ -141,7 +142,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Images and videos',
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder, faviconGenerator],
},
{
name: 'Development',