Add PWA icon generation and Grimoire logo component (#214)

* feat: add Grimoire logo SVG and generate PWA assets

- Add logo.svg with the official Grimoire logo and gradient
- Create GrimoireLogo React component for use in the app
- Add scripts/generate-pwa-icons.mjs to generate all PWA icons from SVG
- Regenerate all favicon and PWA icons from the new logo
- Update mobile welcome screen to show the logo instead of text

* feat: use transparent backgrounds for PWA icons and add theme gradient option

- Update generate-pwa-icons.mjs to output PNGs with transparent backgrounds
- Add gradient prop to GrimoireLogo component ("original" or "theme")
- Theme gradient matches text-grimoire-gradient CSS (yellow -> orange -> purple -> cyan)
- Mobile welcome screen now uses theme gradient to match ASCII art

* feat: use original gradient for mobile welcome logo

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-26 11:46:17 +01:00
committed by GitHub
parent d1ccd930ff
commit 569388c135
14 changed files with 909 additions and 7 deletions

557
package-lock.json generated
View File

@@ -104,6 +104,7 @@
"jsdom": "^27.4.0",
"postcss": "^8.4.49",
"prettier": "^3.7.4",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
@@ -857,6 +858,17 @@
"node": ">=18"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1579,6 +1591,496 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@img/colour": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -6793,6 +7295,16 @@
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@@ -11152,6 +11664,51 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -14,7 +14,8 @@
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run"
"test:run": "vitest run",
"generate-icons": "node scripts/generate-pwa-icons.mjs"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
@@ -112,6 +113,7 @@
"jsdom": "^27.4.0",
"postcss": "^8.4.49",
"prettier": "^3.7.4",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

10
public/logo.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg width="121" height="160" viewBox="0 0 121 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M99.9028 38.8215C100.25 39.0958 100.453 39.5134 100.453 39.9553V62.7095C100.453 63.5082 99.8036 64.1556 99.0027 64.1556H81.8124C81.0115 64.1556 80.3622 63.5082 80.3622 62.7095V49.645C80.3622 49.2031 80.1596 48.7854 79.8122 48.5111L51.1258 25.8633C50.5984 25.4469 49.853 25.4469 49.3256 25.8633L20.6406 48.5111C20.2932 48.7854 20.0906 49.2031 20.0906 49.645V110.352C20.0906 110.794 20.2932 111.212 20.6406 111.486L49.3256 134.134C49.853 134.55 50.5984 134.55 51.1258 134.134L72.9219 116.925C73.6317 116.364 73.6587 115.299 72.9782 114.704L60.3109 103.619C59.3516 102.78 59.869 101.203 61.1403 101.091L80.3924 99.4036C80.9294 99.3566 81.3958 99.0163 81.6036 98.5203L89.0687 80.6918C89.565 79.5066 91.2487 79.5066 91.7449 80.6918L99.2101 98.5203C99.4178 99.0163 99.8843 99.3566 100.421 99.4036L119.675 101.091C120.946 101.203 121.464 102.78 120.504 103.619L105.912 116.389C105.511 116.74 105.336 117.281 105.455 117.799L109.8 136.764C110.086 138.012 108.726 138.987 107.631 138.32L91.8717 128.724C91.3531 128.408 90.6922 128.448 90.2159 128.824L51.1258 159.688C50.5984 160.104 49.853 160.104 49.3256 159.688L0.550024 121.179C0.202601 120.904 0 120.487 0 120.045V39.9553C0 39.5134 0.202601 39.0957 0.550024 38.8215L49.3256 0.312306C49.853 -0.104099 50.5984 -0.104102 51.1258 0.312296L99.9028 38.8215Z" fill="url(#paint0_radial_11084_22058)"/>
<defs>
<radialGradient id="paint0_radial_11084_22058" cx="0" cy="0" r="1" gradientTransform="matrix(201.667 256.092 -193.67 266.667 -8.78247e-06 -0.459781)" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9913E"/>
<stop offset="0.480769" stop-color="#A05CF6"/>
<stop offset="1" stop-color="#7188F3"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env node
/**
* Generate PWA icons and favicons from the Grimoire logo SVG.
*
* Usage: node scripts/generate-pwa-icons.mjs
*
* Requires: npm install --save-dev sharp
*
* This script generates:
* - favicon-16x16.png
* - favicon-32x32.png
* - favicon-192x192.png (PWA icon)
* - favicon-512x512.png (PWA icon)
* - favicon-192x192-maskable.png (PWA maskable icon with padding)
* - favicon-512x512-maskable.png (PWA maskable icon with padding)
* - apple-touch-icon.png (180x180)
* - favicon.ico (multi-resolution ico file)
*/
import sharp from "sharp";
import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, "..");
const PUBLIC_DIR = join(ROOT_DIR, "public");
const LOGO_PATH = join(PUBLIC_DIR, "logo.svg");
// Read the SVG file
const svgBuffer = readFileSync(LOGO_PATH);
// Icon sizes to generate
const STANDARD_SIZES = [16, 32, 192, 512];
const MASKABLE_SIZES = [192, 512];
const APPLE_TOUCH_SIZE = 180;
// Transparent background for the icons
const TRANSPARENT = { r: 0, g: 0, b: 0, alpha: 0 };
/**
* Generate a standard icon (logo fills most of the space with small padding)
*/
async function generateStandardIcon(size, outputName) {
// Add 10% padding on each side for standard icons
const padding = Math.round(size * 0.1);
const logoSize = size - padding * 2;
// Calculate logo dimensions maintaining aspect ratio (121:160)
const aspectRatio = 121 / 160;
const logoHeight = logoSize;
const logoWidth = Math.round(logoHeight * aspectRatio);
// Center the logo
const left = Math.round((size - logoWidth) / 2);
const top = Math.round((size - logoHeight) / 2);
const resizedLogo = await sharp(svgBuffer)
.resize(logoWidth, logoHeight, {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer();
await sharp({
create: {
width: size,
height: size,
channels: 4,
background: TRANSPARENT,
},
})
.composite([{ input: resizedLogo, left, top }])
.png()
.toFile(join(PUBLIC_DIR, outputName));
console.log(`Generated: ${outputName} (${size}x${size})`);
}
/**
* Generate a maskable icon (logo with more padding for safe zone)
* Maskable icons need the important content within the "safe zone" (center 80%)
*/
async function generateMaskableIcon(size, outputName) {
// Maskable icons need 20% padding (40% total safe zone margin)
const padding = Math.round(size * 0.2);
const logoSize = size - padding * 2;
// Calculate logo dimensions maintaining aspect ratio (121:160)
const aspectRatio = 121 / 160;
const logoHeight = logoSize;
const logoWidth = Math.round(logoHeight * aspectRatio);
// Center the logo
const left = Math.round((size - logoWidth) / 2);
const top = Math.round((size - logoHeight) / 2);
const resizedLogo = await sharp(svgBuffer)
.resize(logoWidth, logoHeight, {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer();
await sharp({
create: {
width: size,
height: size,
channels: 4,
background: TRANSPARENT,
},
})
.composite([{ input: resizedLogo, left, top }])
.png()
.toFile(join(PUBLIC_DIR, outputName));
console.log(`Generated: ${outputName} (${size}x${size}, maskable)`);
}
/**
* Generate favicon.ico with multiple resolutions
*/
async function generateFavicon() {
// Generate 16x16, 32x32, and 48x48 versions for the .ico file
const sizes = [16, 32, 48];
const pngBuffers = [];
for (const size of sizes) {
const padding = Math.round(size * 0.1);
const logoSize = size - padding * 2;
const aspectRatio = 121 / 160;
const logoHeight = logoSize;
const logoWidth = Math.round(logoHeight * aspectRatio);
const left = Math.round((size - logoWidth) / 2);
const top = Math.round((size - logoHeight) / 2);
const resizedLogo = await sharp(svgBuffer)
.resize(logoWidth, logoHeight, {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.toBuffer();
const buffer = await sharp({
create: {
width: size,
height: size,
channels: 4,
background: TRANSPARENT,
},
})
.composite([{ input: resizedLogo, left, top }])
.png()
.toBuffer();
pngBuffers.push({ size, buffer });
}
// Create a simple ICO file manually
// ICO format: https://en.wikipedia.org/wiki/ICO_(file_format)
const icoBuffer = createIcoFromPngs(pngBuffers);
writeFileSync(join(PUBLIC_DIR, "favicon.ico"), icoBuffer);
console.log("Generated: favicon.ico (16x16, 32x32, 48x48)");
}
/**
* Create an ICO file from PNG buffers
*/
function createIcoFromPngs(pngBuffers) {
const numImages = pngBuffers.length;
// Calculate total size
let dataOffset = 6 + numImages * 16; // Header (6) + Directory entries (16 each)
const imageData = [];
for (const { size, buffer } of pngBuffers) {
imageData.push({
size,
buffer,
offset: dataOffset,
});
dataOffset += buffer.length;
}
// Create the ICO buffer
const totalSize = dataOffset;
const ico = Buffer.alloc(totalSize);
let offset = 0;
// ICO Header
ico.writeUInt16LE(0, offset); // Reserved
offset += 2;
ico.writeUInt16LE(1, offset); // Type (1 = ICO)
offset += 2;
ico.writeUInt16LE(numImages, offset); // Number of images
offset += 2;
// Directory entries
for (const { size, buffer, offset: dataOff } of imageData) {
ico.writeUInt8(size === 256 ? 0 : size, offset); // Width (0 means 256)
offset += 1;
ico.writeUInt8(size === 256 ? 0 : size, offset); // Height (0 means 256)
offset += 1;
ico.writeUInt8(0, offset); // Color palette
offset += 1;
ico.writeUInt8(0, offset); // Reserved
offset += 1;
ico.writeUInt16LE(1, offset); // Color planes
offset += 2;
ico.writeUInt16LE(32, offset); // Bits per pixel
offset += 2;
ico.writeUInt32LE(buffer.length, offset); // Image size
offset += 4;
ico.writeUInt32LE(dataOff, offset); // Image offset
offset += 4;
}
// Image data
for (const { buffer } of imageData) {
buffer.copy(ico, offset);
offset += buffer.length;
}
return ico;
}
async function main() {
console.log("Generating PWA icons from logo.svg...\n");
// Generate standard icons
for (const size of STANDARD_SIZES) {
const name =
size === 16 || size === 32
? `favicon-${size}x${size}.png`
: `favicon-${size}x${size}.png`;
await generateStandardIcon(size, name);
}
// Generate maskable icons
for (const size of MASKABLE_SIZES) {
await generateMaskableIcon(size, `favicon-${size}x${size}-maskable.png`);
}
// Generate Apple Touch Icon
await generateStandardIcon(APPLE_TOUCH_SIZE, "apple-touch-icon.png");
// Generate favicon.ico
await generateFavicon();
console.log("\nAll icons generated successfully!");
}
main().catch((err) => {
console.error("Error generating icons:", err);
process.exit(1);
});

View File

@@ -2,6 +2,7 @@ import { Terminal } from "lucide-react";
import { Button } from "./ui/button";
import { Kbd, KbdGroup } from "./ui/kbd";
import { Progress } from "./ui/progress";
import { GrimoireLogo } from "./ui/grimoire-logo";
import { MONTHLY_GOAL_SATS } from "@/services/supporters";
import { useLiveQuery } from "dexie-react-hooks";
import db from "@/services/db";
@@ -86,12 +87,10 @@ export function GrimoireWelcome({
</p>
</div>
{/* Mobile: Simple text */}
<div className="md:hidden text-center">
<h1 className="text-4xl font-bold text-grimoire-gradient mb-2">
grimoire
</h1>
<p className="text-muted-foreground text-sm font-mono">
{/* Mobile: Logo with gradient */}
<div className="md:hidden flex flex-col items-center">
<GrimoireLogo size={120} />
<p className="text-muted-foreground text-sm font-mono mt-4">
a nostr client for magicians
</p>
</div>

View File

@@ -0,0 +1,78 @@
import { cn } from "@/lib/utils";
import { useId } from "react";
interface GrimoireLogoProps {
className?: string;
size?: number;
/**
* Which gradient style to use:
* - "original": The original radial gradient (orange -> purple -> blue)
* - "theme": Linear gradient matching text-grimoire-gradient (yellow -> orange -> purple -> cyan)
*/
gradient?: "original" | "theme";
}
/**
* Grimoire logo with the signature gradient.
* The logo shape is a stylized pentagram/grimoire icon with a star element.
*/
export function GrimoireLogo({
className,
size = 160,
gradient = "original",
}: GrimoireLogoProps) {
// Maintain original aspect ratio (121:160)
const width = (size * 121) / 160;
const height = size;
// Use unique ID to avoid conflicts when multiple logos are on the page
const gradientId = useId();
return (
<svg
width={width}
height={height}
viewBox="0 0 121 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn(className)}
aria-label="Grimoire logo"
>
<path
d="M99.9028 38.8215C100.25 39.0958 100.453 39.5134 100.453 39.9553V62.7095C100.453 63.5082 99.8036 64.1556 99.0027 64.1556H81.8124C81.0115 64.1556 80.3622 63.5082 80.3622 62.7095V49.645C80.3622 49.2031 80.1596 48.7854 79.8122 48.5111L51.1258 25.8633C50.5984 25.4469 49.853 25.4469 49.3256 25.8633L20.6406 48.5111C20.2932 48.7854 20.0906 49.2031 20.0906 49.645V110.352C20.0906 110.794 20.2932 111.212 20.6406 111.486L49.3256 134.134C49.853 134.55 50.5984 134.55 51.1258 134.134L72.9219 116.925C73.6317 116.364 73.6587 115.299 72.9782 114.704L60.3109 103.619C59.3516 102.78 59.869 101.203 61.1403 101.091L80.3924 99.4036C80.9294 99.3566 81.3958 99.0163 81.6036 98.5203L89.0687 80.6918C89.565 79.5066 91.2487 79.5066 91.7449 80.6918L99.2101 98.5203C99.4178 99.0163 99.8843 99.3566 100.421 99.4036L119.675 101.091C120.946 101.203 121.464 102.78 120.504 103.619L105.912 116.389C105.511 116.74 105.336 117.281 105.455 117.799L109.8 136.764C110.086 138.012 108.726 138.987 107.631 138.32L91.8717 128.724C91.3531 128.408 90.6922 128.448 90.2159 128.824L51.1258 159.688C50.5984 160.104 49.853 160.104 49.3256 159.688L0.550024 121.179C0.202601 120.904 0 120.487 0 120.045V39.9553C0 39.5134 0.202601 39.0957 0.550024 38.8215L49.3256 0.312306C49.853 -0.104099 50.5984 -0.104102 51.1258 0.312296L99.9028 38.8215Z"
fill={`url(#${gradientId})`}
/>
<defs>
{gradient === "original" ? (
<radialGradient
id={gradientId}
cx="0"
cy="0"
r="1"
gradientTransform="matrix(201.667 256.092 -193.67 266.667 -8.78247e-06 -0.459781)"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F9913E" />
<stop offset="0.480769" stopColor="#A05CF6" />
<stop offset="1" stopColor="#7188F3" />
</radialGradient>
) : (
<linearGradient
id={gradientId}
x1="60.5"
y1="0"
x2="60.5"
y2="160"
gradientUnits="userSpaceOnUse"
>
{/* Matches text-grimoire-gradient CSS variables */}
<stop offset="0%" stopColor="rgb(var(--gradient-1))" />
<stop offset="33%" stopColor="rgb(var(--gradient-2))" />
<stop offset="66%" stopColor="rgb(var(--gradient-3))" />
<stop offset="100%" stopColor="rgb(var(--gradient-4))" />
</linearGradient>
)}
</defs>
</svg>
);
}