From 9efd7d795ed69439cca66f29c495bbaf32fa7dc0 Mon Sep 17 00:00:00 2001 From: sho Date: Thu, 26 Feb 2026 23:37:38 +0900 Subject: [PATCH 1/6] Add icon generator with ZIP export and optional PWA manifest --- locales/en.yml | 26 ++ package.json | 1 + pnpm-lock.yaml | 292 +++++++----------- .../icon-generator/icon-generator.e2e.spec.ts | 56 ++++ .../icon-generator.service.test.ts | 103 ++++++ .../icon-generator/icon-generator.service.ts | 179 +++++++++++ src/tools/icon-generator/icon-generator.vue | 267 ++++++++++++++++ src/tools/icon-generator/index.ts | 13 + src/tools/index.ts | 3 +- 9 files changed, 764 insertions(+), 176 deletions(-) create mode 100644 src/tools/icon-generator/icon-generator.e2e.spec.ts create mode 100644 src/tools/icon-generator/icon-generator.service.test.ts create mode 100644 src/tools/icon-generator/icon-generator.service.ts create mode 100644 src/tools/icon-generator/icon-generator.vue create mode 100644 src/tools/icon-generator/index.ts diff --git a/locales/en.yml b/locales/en.yml index d03d80d3..b6932a82 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -93,6 +93,32 @@ tools: button: copy: Copy refresh: Refresh + + icon-generator: + title: Icon generator + description: Generate resized app icons from one source image for PWA, Android, and iOS presets. + presets: Presets + presetLabels: + pwa: PWA + android: Android + ios: iOS + fitMode: Fit mode + cover: Cover (crop) + contain: Contain + clearPresetSizes: Clear preset sizes + addSize: Add size + baseName: Base name + baseNameHint: File names use "-x.png". Invalid characters are replaced with "-". + includeManifest: Include manifest.json in ZIP + selectedSizes: Selected output sizes + uploadTitle: Drag and drop an image here, or click to select an image + noImage: Upload an image to start. + generate: Generate icons + output: Generated icons + download: Download + downloadAll: Download all + downloadZip: Download zip + percentage-calculator: title: Percentage calculator description: Easily calculate percentages from a value to another value, or from a percentage to a value. diff --git a/package.json b/package.json index 5738b632..9776019f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d113893..6565a746 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,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 +113,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 +2306,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 +2315,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 +2330,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 +2350,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 +2366,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 +2375,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: @@ -2434,8 +2408,10 @@ packages: '@vueuse/shared@10.3.0': resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} - '@vueuse/shared@12.0.0': - resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 '@zhead/schema@1.0.0-beta.13': resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==} @@ -2798,7 +2774,7 @@ packages: hasBin: true concat-map@0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -2831,6 +2807,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 +2875,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 +3222,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 +3626,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 +3710,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'} @@ -3817,9 +3793,6 @@ packages: is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-core-module@2.16.0: resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} engines: {node: '>= 0.4'} @@ -3974,6 +3947,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 +4072,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 +4101,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 +4480,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 +4650,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 +4776,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 +4859,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 +4906,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 +4986,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 +5064,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 +5126,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 +5698,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 +6106,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 +6989,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 +7004,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 +7019,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 +7217,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 +7233,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 @@ -7264,7 +7253,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.6 '@iconify/types': 2.0.0 - debug: 4.3.4 + debug: 4.4.0 kolorist: 1.8.0 local-pkg: 0.4.3 transitivePeerDependencies: @@ -7401,7 +7390,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 +7911,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 +7929,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 +7943,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 +7957,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 +7977,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 +7991,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 +8005,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 +8038,13 @@ 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': 14.2.1(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 - transitivePeerDependencies: - - typescript '@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,14 +8479,12 @@ 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 '@vueuse/metadata@10.3.0': {} @@ -8580,11 +8511,9 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/shared@12.0.0(typescript@5.2.2)': + '@vueuse/shared@14.2.1(vue@3.3.4)': dependencies: - vue: 3.5.13(typescript@5.2.2) - transitivePeerDependencies: - - typescript + vue: 3.3.4 '@zhead/schema@1.0.0-beta.13': {} @@ -8608,7 +8537,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -8780,7 +8709,7 @@ snapshots: builtins@5.0.1: dependencies: - semver: 7.5.4 + semver: 7.6.3 bundle-require@5.0.0(esbuild@0.23.1): dependencies: @@ -9022,6 +8951,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 +9015,6 @@ snapshots: csstype@3.1.2: {} - csstype@3.1.3: {} - dash-get@1.0.2: {} data-uri-to-buffer@4.0.1: {} @@ -9971,10 +9900,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 +9941,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 +9996,8 @@ snapshots: image-size@0.5.5: optional: true + immediate@3.0.6: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -10160,10 +10087,6 @@ snapshots: dependencies: has: 1.0.3 - is-core-module@2.13.1: - dependencies: - hasown: 2.0.0 - is-core-module@2.16.0: dependencies: hasown: 2.0.2 @@ -10293,6 +10216,8 @@ snapshots: dependencies: is-docker: 2.2.1 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -10412,6 +10337,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 +10375,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 +10689,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 +10793,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 +10955,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 +11124,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 @@ -11261,19 +11211,13 @@ snapshots: resolve@1.22.2: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 resolve@1.22.4: dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 + is-core-module: 2.16.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -11322,6 +11266,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 +11347,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 +11513,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 +11970,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 @@ -12125,14 +12077,14 @@ snapshots: vue-eslint-parser@9.3.1(eslint@8.47.0): dependencies: - debug: 4.3.4 + debug: 4.4.0 eslint: 8.47.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.5.0 lodash: 4.17.21 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -12180,16 +12132,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 diff --git a/src/tools/icon-generator/icon-generator.e2e.spec.ts b/src/tools/icon-generator/icon-generator.e2e.spec.ts new file mode 100644 index 00000000..2ac549c1 --- /dev/null +++ b/src/tools/icon-generator/icon-generator.e2e.spec.ts @@ -0,0 +1,56 @@ +import { Buffer } from 'node:buffer'; +import { expect, test } from '@playwright/test'; + +const sampleSvg = ` + + +`; + +test.describe('Tool - Icon generator', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/icon-generator'); + }); + + test('has correct title', async ({ page }) => { + await expect(page).toHaveTitle('Icon generator - IT Tools'); + }); + + test('shows generator controls and default state', async ({ page }) => { + await expect(page.getByText('Generate icons')).toBeVisible(); + await expect(page.getByText('Download zip')).toBeVisible(); + await expect(page.getByText('Selected output sizes: -')).toBeVisible(); + await expect(page.getByRole('checkbox', { name: 'Include manifest.json in ZIP' })).toBeDisabled(); + }); + + test('applies preset sizes and toggles manifest option with reset', async ({ page }) => { + await page.getByRole('button', { name: 'PWA' }).click(); + await expect(page.getByText('Selected output sizes: 72, 96, 128, 144, 152, 192, 384, 512')).toBeVisible(); + + const manifestCheckbox = page.getByRole('checkbox', { name: 'Include manifest.json in ZIP' }); + await expect(manifestCheckbox).toBeEnabled(); + await manifestCheckbox.check(); + await expect(manifestCheckbox).toBeChecked(); + + await page.getByRole('button', { name: 'Clear preset sizes' }).click(); + await expect(page.getByText('Selected output sizes: -')).toBeVisible(); + await expect(manifestCheckbox).toBeDisabled(); + await expect(manifestCheckbox).not.toBeChecked(); + }); + + test('generates icons and downloads zip', async ({ page }) => { + await page.getByRole('button', { name: 'PWA' }).click(); + await page.locator('input[type="file"]').setInputFiles({ + name: 'source.svg', + mimeType: 'image/svg+xml', + buffer: Buffer.from(sampleSvg), + }); + + await page.getByRole('button', { name: 'Generate icons' }).click(); + await expect(page.getByText('Generated icons (8)')).toBeVisible(); + + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Download zip' }).click(); + const download = await downloadPromise; + expect(download.suggestedFilename()).toBe('icon-icons.zip'); + }); +}); diff --git a/src/tools/icon-generator/icon-generator.service.test.ts b/src/tools/icon-generator/icon-generator.service.test.ts new file mode 100644 index 00000000..46bfa8a0 --- /dev/null +++ b/src/tools/icon-generator/icon-generator.service.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest'; +import { + ICON_PRESETS, + buildIconFilename, + calculateDrawArea, + createWebAppManifest, + getPresetSizes, + normalizeSizes, + sanitizeBaseName, +} from './icon-generator.service'; + +describe('icon-generator', () => { + describe('normalizeSizes', () => { + it('should remove invalid values and deduplicate sizes', () => { + expect(normalizeSizes([192, 192, 0, -1, 512.8, 72])).toEqual([72, 192, 512]); + }); + }); + + describe('getPresetSizes', () => { + it('should return merged and sorted sizes from multiple presets', () => { + const sizes = getPresetSizes(['pwa', 'android']); + + expect(sizes).toEqual([48, 72, 96, 128, 144, 152, 192, 384, 512]); + }); + + it('should include known iOS size', () => { + const sizes = getPresetSizes(['ios']); + expect(sizes).toContain(180); + expect(sizes).toContain(1024); + }); + }); + + describe('calculateDrawArea', () => { + it('should crop in cover mode for rectangular source', () => { + const drawArea = calculateDrawArea({ + sourceWidth: 400, + sourceHeight: 200, + targetSize: 100, + fitMode: 'cover', + }); + + expect(drawArea.width).toBeGreaterThan(100); + expect(drawArea.height).toBe(100); + expect(drawArea.x).toBeLessThan(0); + }); + + it('should keep full image in contain mode for rectangular source', () => { + const drawArea = calculateDrawArea({ + sourceWidth: 400, + sourceHeight: 200, + targetSize: 100, + fitMode: 'contain', + }); + + expect(drawArea.width).toBe(100); + expect(drawArea.height).toBeLessThan(100); + expect(drawArea.y).toBeGreaterThan(0); + }); + }); + + it('should expose 3 platform presets', () => { + expect(ICON_PRESETS.map(preset => preset.key)).toEqual(['pwa', 'android', 'ios']); + }); + + it('should sanitize base name and use it in icon filename', () => { + expect(sanitizeBaseName(' My App/Icon ')).toBe('My-App-Icon'); + expect(buildIconFilename(192, ' My App/Icon ')).toBe('My-App-Icon-192x192.png'); + expect(buildIconFilename(192, ' ')).toBe('icon-192x192.png'); + }); + + it('should create manifest json with provided icons', () => { + const manifestJson = createWebAppManifest({ + appName: 'my-app', + description: 'my generated icons', + icons: [ + { filename: 'my-app-192x192.png', size: 192 }, + { filename: 'my-app-512x512.png', size: 512 }, + ], + }); + + const manifest = JSON.parse(manifestJson) as { + id: string + name: string + short_name: string + description: string + lang: string + start_url: string + scope: string + icons: { src: string; sizes: string; type: string; purpose: string }[] + }; + expect(manifest.id).toBe('/'); + expect(manifest.name).toBe('my-app'); + expect(manifest.short_name).toBe('my-app'); + expect(manifest.description).toBe('my generated icons'); + expect(manifest.lang).toBe('en'); + expect(manifest.start_url).toBe('/'); + expect(manifest.scope).toBe('/'); + expect(manifest.icons).toEqual([ + { src: 'my-app-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any maskable' }, + { src: 'my-app-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }, + ]); + }); +}); diff --git a/src/tools/icon-generator/icon-generator.service.ts b/src/tools/icon-generator/icon-generator.service.ts new file mode 100644 index 00000000..b7b33bf0 --- /dev/null +++ b/src/tools/icon-generator/icon-generator.service.ts @@ -0,0 +1,179 @@ +export type IconPresetKey = 'pwa' | 'android' | 'ios'; +export type IconFitMode = 'cover' | 'contain'; + +export interface IconPreset { + key: IconPresetKey + sizes: number[] +} + +export interface GeneratedIcon { + size: number + filename: string + dataUrl: string +} + +export function createWebAppManifest({ + appName, + icons, + description, + startUrl = '/', + scope = '/', + lang = 'en', + backgroundColor = '#ffffff', + themeColor = '#ffffff', +}: { + appName: string + icons: { filename: string; size: number }[] + description?: string + startUrl?: string + scope?: string + lang?: string + backgroundColor?: string + themeColor?: string +}) { + return JSON.stringify({ + id: startUrl, + name: appName, + short_name: appName, + description: description ?? `${appName} icons`, + lang, + start_url: startUrl, + scope, + display: 'standalone', + background_color: backgroundColor, + theme_color: themeColor, + icons: icons.map(icon => ({ + src: icon.filename, + sizes: `${icon.size}x${icon.size}`, + type: 'image/png', + purpose: 'any maskable', + })), + }, null, 2); +} + +export const ICON_PRESETS: IconPreset[] = [ + { key: 'pwa', sizes: [72, 96, 128, 144, 152, 192, 384, 512] }, + { key: 'android', sizes: [48, 72, 96, 144, 192, 512] }, + { key: 'ios', sizes: [20, 29, 40, 58, 60, 76, 80, 87, 120, 152, 167, 180, 1024] }, +]; + +export function normalizeSizes(sizes: number[]) { + return Array.from( + new Set( + sizes + .map(size => Math.floor(size)) + .filter(size => Number.isFinite(size) && size > 0), + ), + ).sort((a, b) => a - b); +} + +export function getPresetSizes(presetKeys: IconPresetKey[]) { + const selectedSizes = ICON_PRESETS + .filter(preset => presetKeys.includes(preset.key)) + .flatMap(preset => preset.sizes); + + return normalizeSizes(selectedSizes); +} + +export function sanitizeBaseName(baseName: string) { + const normalized = baseName.trim().replace(/[<>:"/\\|?*]+/g, '-').replace(/\s+/g, '-'); + return normalized || 'icon'; +} + +export function buildIconFilename(size: number, baseName = 'icon') { + const safeBaseName = sanitizeBaseName(baseName); + return `${safeBaseName}-${size}x${size}.png`; +} + +export function calculateDrawArea({ + sourceWidth, + sourceHeight, + targetSize, + fitMode, +}: { + sourceWidth: number + sourceHeight: number + targetSize: number + fitMode: IconFitMode +}) { + const scale = fitMode === 'cover' + ? Math.max(targetSize / sourceWidth, targetSize / sourceHeight) + : Math.min(targetSize / sourceWidth, targetSize / sourceHeight); + + const drawWidth = sourceWidth * scale; + const drawHeight = sourceHeight * scale; + + return { + x: (targetSize - drawWidth) / 2, + y: (targetSize - drawHeight) / 2, + width: drawWidth, + height: drawHeight, + }; +} + +export function loadImageFromFile(file: File) { + return new Promise((resolve, reject) => { + const url = URL.createObjectURL(file); + const image = new Image(); + image.onload = () => { + URL.revokeObjectURL(url); + resolve(image); + }; + image.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Unable to load image file.')); + }; + image.src = url; + }); +} + +export function createResizedIcon({ + image, + size, + fitMode, +}: { + image: HTMLImageElement + size: number + fitMode: IconFitMode +}) { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Unable to get canvas context.'); + } + + const drawArea = calculateDrawArea({ + sourceWidth: image.width, + sourceHeight: image.height, + targetSize: size, + fitMode, + }); + + context.clearRect(0, 0, size, size); + context.imageSmoothingEnabled = true; + context.imageSmoothingQuality = 'high'; + context.drawImage(image, drawArea.x, drawArea.y, drawArea.width, drawArea.height); + + return canvas.toDataURL('image/png'); +} + +export function createResizedIcons({ + image, + sizes, + fitMode, + baseName, +}: { + image: HTMLImageElement + sizes: number[] + fitMode: IconFitMode + baseName?: string +}) { + return normalizeSizes(sizes).map((size): GeneratedIcon => ({ + size, + filename: buildIconFilename(size, baseName), + dataUrl: createResizedIcon({ image, size, fitMode }), + })); +} diff --git a/src/tools/icon-generator/icon-generator.vue b/src/tools/icon-generator/icon-generator.vue new file mode 100644 index 00000000..99e1bde7 --- /dev/null +++ b/src/tools/icon-generator/icon-generator.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/src/tools/icon-generator/index.ts b/src/tools/icon-generator/index.ts new file mode 100644 index 00000000..06b7481c --- /dev/null +++ b/src/tools/icon-generator/index.ts @@ -0,0 +1,13 @@ +import { ArrowsShuffle } from '@vicons/tabler'; +import { defineTool } from '../tool'; +import { translate } from '@/plugins/i18n.plugin'; + +export const tool = defineTool({ + name: translate('tools.icon-generator.title'), + path: '/icon-generator', + description: translate('tools.icon-generator.description'), + keywords: ['icon', 'generator', 'pwa', 'android', 'ios', 'app icon', 'resize', 'favicon'], + component: () => import('./icon-generator.vue'), + icon: ArrowsShuffle, + createdAt: new Date('2026-02-26'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf4..8954b626 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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 iconGenerator } from './icon-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, iconGenerator], }, { name: 'Development', From 86bf428302b1750a6b74906691d55e75d06acdbf Mon Sep 17 00:00:00 2001 From: sho Date: Thu, 26 Feb 2026 23:55:43 +0900 Subject: [PATCH 2/6] Fix build by pinning @vueuse/shared resolution --- package.json | 5 +++ pnpm-lock.yaml | 97 +++++++++++++++++++++++++++++++------------------- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 9776019f..3ceae537 100644 --- a/package.json +++ b/package.json @@ -150,5 +150,10 @@ "vitest": "^0.34.0", "workbox-window": "^7.0.0", "zx": "^7.2.1" + }, + "pnpm": { + "overrides": { + "@vueuse/shared": "10.3.0" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6565a746..dd0f24f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@vueuse/shared': 10.3.0 + importers: .: @@ -359,17 +362,20 @@ packages: '@antfu/eslint-config-basic@0.41.0': resolution: {integrity: sha512-zcwFv+nEV/NroeeVWriNdnIGd9soOLRG8wIiVz4VVJ0BjONrqQR56HLG/gDxH/1GUYBnQCEcVxGUmegce08cnw==} + deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config peerDependencies: eslint: '>=7.4.0' '@antfu/eslint-config-ts@0.41.0': resolution: {integrity: sha512-ng3GYpJGZgrxGwBVda/MgUpveH3LbEqdPCFi1+S5e62W4kf8rmEVbhc0I8w7/aKN0uNqir5SInYg8gob2maDAQ==} + deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config peerDependencies: eslint: '>=7.4.0' typescript: '>=3.9' '@antfu/eslint-config-vue@0.41.0': resolution: {integrity: sha512-iJiEGRUgRmT3mQCmGl0hTMwq/ShXRjRPjpgsDcphKJyEF06ZIR/4gxHn+utQRLT2hD39DQN8gk0ZbpV3gWtf/g==} + deprecated: Deprecated, please migrate to @antfu/eslint-config with the flat config peerDependencies: eslint: '>=7.4.0' @@ -1467,6 +1473,7 @@ packages: '@humanwhocodes/config-array@0.11.10': resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -1474,6 +1481,7 @@ packages: '@humanwhocodes/object-schema@1.2.1': resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + deprecated: Use @eslint/object-schema instead '@iconify-json/mdi@1.1.50': resolution: {integrity: sha512-SgbT5w5eHCdOG74ZWPz7HlTGk6VsifIJhNi6lAsxj/5Nlqt6Cz4LlQmSa9eecU9p075Jub2aAx/o7YI+GCahRQ==} @@ -1632,6 +1640,7 @@ packages: '@playwright/test@1.32.3': resolution: {integrity: sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==} engines: {node: '>=14'} + deprecated: Please update to the latest version of Playwright to test up-to-date browsers. hasBin: true '@polka/url@1.0.0-next.28': @@ -2402,22 +2411,15 @@ 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@14.2.1': - resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} - peerDependencies: - vue: ^3.5.0 - '@zhead/schema@1.0.0-beta.13': resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==} abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -2528,6 +2530,7 @@ packages: babel-merge@3.0.0: resolution: {integrity: sha512-eBOBtHnzt9xvnjpYNI5HmaPp/b2vMveE5XggzqHnQeHJ8mFIBrBv6WZEVIj5jJ2uwTItkqKo9gWzEEcBxEq0yw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: '@babel/core': ^7.0.0 @@ -3030,6 +3033,7 @@ packages: domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} @@ -3248,6 +3252,7 @@ packages: eslint-plugin-markdown@3.0.1: resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: Please use @eslint/markdown instead peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3314,6 +3319,7 @@ packages: eslint@8.47.0: resolution: {integrity: sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -3543,10 +3549,12 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -3626,6 +3634,10 @@ 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'} @@ -3734,6 +3746,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3793,6 +3806,9 @@ packages: is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.16.0: resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} engines: {node: '>= 0.4'} @@ -4154,6 +4170,7 @@ packages: loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + deprecated: Please upgrade to 2.3.7 which fixes GHSA-4q6p-r6v2-jvc5 lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} @@ -4362,6 +4379,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} @@ -4595,6 +4613,7 @@ packages: plausible-tracker@0.3.8: resolution: {integrity: sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==} engines: {node: '>=10'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. playwright-core@1.32.3: resolution: {integrity: sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==} @@ -4881,6 +4900,7 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rollup@2.79.2: @@ -5068,6 +5088,7 @@ packages: sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -5566,6 +5587,7 @@ packages: vite-plugin-vue-markdown@0.23.5: resolution: {integrity: sha512-NXTZ4y+n691gLPWayMBbh4jldQeaqDp9e9WjWUYbn9obsLqS9qU+hr4RAruDq5kP4siTOp7JDV34Sw5eA7WxLg==} + deprecated: '`vite-plugin-vue-markdown` is renamed to `unplugin-vue-markdown`. For usages in Vite, you also need to change the import path to `unplugin-vue-markdown/vite`.' peerDependencies: vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0 @@ -5672,6 +5694,7 @@ packages: vue-i18n@9.9.1: resolution: {integrity: sha512-xyQ4VspLdNSPTKBFBPWa1tvtj+9HuockZwgFeD2OhxxXuC2CWeNvV4seu2o9+vbQOyQbhAM5Ez56oxUrrnTWdw==} engines: {node: '>= 16'} + deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html peerDependencies: vue: ^3.0.0 @@ -5743,6 +5766,7 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} @@ -6989,7 +7013,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.10 '@babel/types': 7.22.10 - debug: 4.4.0 + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7004,7 +7028,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.5 '@babel/parser': 7.22.5 '@babel/types': 7.22.5 - debug: 4.4.0 + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7217,7 +7241,7 @@ snapshots: '@eslint/eslintrc@2.1.2': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.3.4 espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -7233,7 +7257,7 @@ snapshots: '@humanwhocodes/config-array@0.11.10': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.4.0 + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7253,7 +7277,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.6 '@iconify/types': 2.0.0 - debug: 4.4.0 + debug: 4.3.4 kolorist: 1.8.0 local-pkg: 0.4.3 transitivePeerDependencies: @@ -7943,10 +7967,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.4.1 '@typescript-eslint/visitor-keys': 6.4.1 - debug: 4.4.0 + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.5.4 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -8005,7 +8029,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.6.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript @@ -8042,9 +8066,11 @@ snapshots: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 14.2.1(vue@3.3.4) + '@vueuse/shared': 10.3.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 + transitivePeerDependencies: + - '@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: @@ -8485,25 +8511,20 @@ snapshots: '@unhead/ssr': 0.5.1 '@unhead/vue': 0.5.1(vue@3.3.4) vue: 3.3.4 + transitivePeerDependencies: + - '@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) @@ -8511,10 +8532,6 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/shared@14.2.1(vue@3.3.4)': - dependencies: - vue: 3.3.4 - '@zhead/schema@1.0.0-beta.13': {} abab@2.0.6: {} @@ -8537,7 +8554,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -9900,6 +9917,10 @@ 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 @@ -9941,14 +9962,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.3.4 transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -10087,6 +10108,10 @@ snapshots: dependencies: has: 1.0.3 + is-core-module@2.13.1: + dependencies: + hasown: 2.0.0 + is-core-module@2.16.0: dependencies: hasown: 2.0.2 @@ -11211,7 +11236,7 @@ snapshots: resolve@1.22.2: dependencies: - is-core-module: 2.16.0 + is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -11970,7 +11995,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.4.0 + debug: 4.3.4 mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -12077,14 +12102,14 @@ snapshots: vue-eslint-parser@9.3.1(eslint@8.47.0): dependencies: - debug: 4.4.0 + debug: 4.3.4 eslint: 8.47.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.5.0 lodash: 4.17.21 - semver: 7.6.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color From 1982be4f949768dbfb866565367dd3376365ded6 Mon Sep 17 00:00:00 2001 From: sho Date: Fri, 27 Feb 2026 00:02:24 +0900 Subject: [PATCH 3/6] Stabilize e2e workflow on Ubuntu 22.04 --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 13b787ef..9729845b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -7,7 +7,7 @@ on: jobs: test: timeout-minutes: 10 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: shard: [1/3, 2/3, 3/3] @@ -23,7 +23,7 @@ jobs: - name: Get Playwright version id: playwright-version - run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT" + run: echo "PLAYWRIGHT_VERSION=$(jq -r '.devDependencies[\"@playwright/test\"] // .dependencies.playwright // \"unknown\"' package.json)" >> "$GITHUB_OUTPUT" - name: Install dependencies run: pnpm install From 89b7ca6d46bdf35d181c4acaa8da836001d056a4 Mon Sep 17 00:00:00 2001 From: sho Date: Fri, 27 Feb 2026 00:05:05 +0900 Subject: [PATCH 4/6] Use replaceAll in sanitizeBaseName --- src/tools/icon-generator/icon-generator.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/icon-generator/icon-generator.service.ts b/src/tools/icon-generator/icon-generator.service.ts index b7b33bf0..c7f273a7 100644 --- a/src/tools/icon-generator/icon-generator.service.ts +++ b/src/tools/icon-generator/icon-generator.service.ts @@ -76,7 +76,7 @@ export function getPresetSizes(presetKeys: IconPresetKey[]) { } export function sanitizeBaseName(baseName: string) { - const normalized = baseName.trim().replace(/[<>:"/\\|?*]+/g, '-').replace(/\s+/g, '-'); + const normalized = baseName.trim().replaceAll(/[<>:"/\\|?*]+/g, '-').replaceAll(/\s+/g, '-'); return normalized || 'icon'; } From f4981d86edd17456fddb4adb665e6436301867d1 Mon Sep 17 00:00:00 2001 From: sho Date: Fri, 27 Feb 2026 00:08:50 +0900 Subject: [PATCH 5/6] Restore TS-compatible sanitizeBaseName implementation --- src/tools/icon-generator/icon-generator.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/icon-generator/icon-generator.service.ts b/src/tools/icon-generator/icon-generator.service.ts index c7f273a7..b7b33bf0 100644 --- a/src/tools/icon-generator/icon-generator.service.ts +++ b/src/tools/icon-generator/icon-generator.service.ts @@ -76,7 +76,7 @@ export function getPresetSizes(presetKeys: IconPresetKey[]) { } export function sanitizeBaseName(baseName: string) { - const normalized = baseName.trim().replaceAll(/[<>:"/\\|?*]+/g, '-').replaceAll(/\s+/g, '-'); + const normalized = baseName.trim().replace(/[<>:"/\\|?*]+/g, '-').replace(/\s+/g, '-'); return normalized || 'icon'; } From 795fc6dd0e4b3d575f35136530c3bcc70a29e165 Mon Sep 17 00:00:00 2001 From: sho Date: Fri, 27 Feb 2026 00:15:38 +0900 Subject: [PATCH 6/6] Make icon generator e2e checks robust for naive checkbox --- src/tools/icon-generator/icon-generator.e2e.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tools/icon-generator/icon-generator.e2e.spec.ts b/src/tools/icon-generator/icon-generator.e2e.spec.ts index 2ac549c1..76b6a760 100644 --- a/src/tools/icon-generator/icon-generator.e2e.spec.ts +++ b/src/tools/icon-generator/icon-generator.e2e.spec.ts @@ -19,7 +19,11 @@ test.describe('Tool - Icon generator', () => { await expect(page.getByText('Generate icons')).toBeVisible(); await expect(page.getByText('Download zip')).toBeVisible(); await expect(page.getByText('Selected output sizes: -')).toBeVisible(); - await expect(page.getByRole('checkbox', { name: 'Include manifest.json in ZIP' })).toBeDisabled(); + const manifestCheckbox = page.getByRole('checkbox', { name: 'Include manifest.json in ZIP' }); + await expect(manifestCheckbox).not.toBeChecked(); + + await manifestCheckbox.click(); + await expect(manifestCheckbox).not.toBeChecked(); }); test('applies preset sizes and toggles manifest option with reset', async ({ page }) => { @@ -27,13 +31,14 @@ test.describe('Tool - Icon generator', () => { await expect(page.getByText('Selected output sizes: 72, 96, 128, 144, 152, 192, 384, 512')).toBeVisible(); const manifestCheckbox = page.getByRole('checkbox', { name: 'Include manifest.json in ZIP' }); - await expect(manifestCheckbox).toBeEnabled(); await manifestCheckbox.check(); await expect(manifestCheckbox).toBeChecked(); await page.getByRole('button', { name: 'Clear preset sizes' }).click(); await expect(page.getByText('Selected output sizes: -')).toBeVisible(); - await expect(manifestCheckbox).toBeDisabled(); + await expect(manifestCheckbox).not.toBeChecked(); + + await manifestCheckbox.click(); await expect(manifestCheckbox).not.toBeChecked(); });