feat(pwa): manifest installable + icons gem rubis sur fond crème
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 58s
Build & Deploy API / build-and-deploy (push) Successful in 1m35s

L'app est désormais installable sur écran d'accueil (Android via Chrome,
iOS via Safari → Partager → Sur l'écran d'accueil). Identité visuelle
strictement alignée sur le SPA :
  - background_color : crème (#FAF7F2)
  - theme_color      : rubis primaire (#9F1239)
  - icon             : gem 4-facettes sur canvas crème, padding 15% pour
                       safe-area maskable Android tout en remplissant
                       suffisamment iOS qui ne masque pas

3 formats générés depuis le SVG via @resvg/resvg-js :
  - icon-192.png + icon-512.png (manifest, splashscreen Android)
  - apple-touch-icon.png 180×180 (iOS home screen)
Plus la SVG vectorielle servie en favicon.

Le script `pnpm --filter @rubis/web run icons` re-génère tout depuis
`public/icon.svg` — utile si on retouche le design.

Drop le favicon.svg de 1.1 MB hérité du landing (vestige), remplacé par
notre gem propre à 1.1 KB.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-05-07 13:38:32 +02:00
parent 639191bef9
commit 6c3b5e36b9
10 changed files with 257 additions and 6 deletions

View File

@ -2,13 +2,23 @@
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#9F1239" />
<meta
name="description"
content="Rubis Sur l'Ongle — Vos factures relancées toutes seules pendant que vous travaillez."
/>
<!-- Favicons : SVG en priorité, PNG en fallback iOS -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- PWA — install sur écran d'accueil avec fond crème + gem rubis -->
<link rel="manifest" href="/site.webmanifest" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Rubis" />
<title>Rubis Sur l'Ongle</title>
</head>
<body>

View File

@ -14,7 +14,8 @@
"typecheck": "tsc -b --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"msw:init": "msw init public --save"
"msw:init": "msw init public --save",
"icons": "node scripts/generate-icons.mjs"
},
"dependencies": {
"@fontsource-variable/bricolage-grotesque": "^5.2.5",
@ -45,6 +46,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@resvg/resvg-js": "^2.6.2",
"@tailwindcss/vite": "^4.1.0",
"@tanstack/router-cli": "^1.114.3",
"@tanstack/router-plugin": "^1.114.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

22
apps/web/public/icon.svg Normal file
View File

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!--
Icon PWA / apple-touch-icon — gem rubis sur fond crème.
Reprend exactement <Gem/> du SPA. Padding ~15% sur chaque bord pour
rester safe en maskable Android (le spec demande ≥80% de safe area
au centre) tout en remplissant assez le canvas pour iOS qui ne masque
pas.
-->
<rect width="512" height="512" fill="#FAF7F2"/>
<g transform="translate(76 76) scale(3.6)">
<!-- Facette haut-gauche (lumière) -->
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
<!-- Facette haut-droite -->
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
<!-- Facette bas-gauche -->
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
<!-- Facette bas-droite (ombre) -->
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
<!-- Contour propre -->
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,32 @@
{
"name": "Rubis.",
"short_name": "Rubis",
"description": "Vos factures relancées toutes seules pendant que vous travaillez.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"lang": "fr-FR",
"theme_color": "#9F1239",
"background_color": "#FAF7F2",
"icons": [
{
"src": "/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

View File

@ -0,0 +1,36 @@
/**
* Génère les PNGs d'icône PWA depuis `public/icon.svg`.
*
* Sort :
* - public/icon-192.png (Android mostly, manifest)
* - public/icon-512.png (Android, splashscreen)
* - public/apple-touch-icon.png (iOS, 180×180 recommandation Apple)
*
* À relancer si on touche au design de la gem :
* pnpm --filter @rubis/web run icons
*/
import { readFile, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { Resvg } from '@resvg/resvg-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = join(__dirname, '..')
const SRC = join(ROOT, 'public/icon.svg')
async function render(size, out) {
const svg = await readFile(SRC, 'utf-8')
const resvg = new Resvg(svg, {
fitTo: { mode: 'width', value: size },
background: '#FAF7F2',
})
const buf = resvg.render().asPng()
const dest = join(ROOT, 'public', out)
await writeFile(dest, buf)
console.log(`${out} (${size}×${size}, ${buf.byteLength.toLocaleString()} B)`)
}
await render(192, 'icon-192.png')
await render(512, 'icon-512.png')
await render(180, 'apple-touch-icon.png')

130
pnpm-lock.yaml generated
View File

@ -247,6 +247,9 @@ importers:
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@10.3.0(jiti@2.7.0))
'@resvg/resvg-js':
specifier: ^2.6.2
version: 2.6.2
'@tailwindcss/vite':
specifier: ^4.1.0
version: 4.2.4(vite@8.0.10(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.8.4))
@ -2001,6 +2004,82 @@ packages:
react-redux:
optional: true
'@resvg/resvg-js-android-arm-eabi@2.6.2':
resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
'@resvg/resvg-js-android-arm64@2.6.2':
resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@resvg/resvg-js-darwin-arm64@2.6.2':
resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@resvg/resvg-js-darwin-x64@2.6.2':
resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@resvg/resvg-js-linux-x64-musl@2.6.2':
resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@resvg/resvg-js@2.6.2':
resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==}
engines: {node: '>= 10'}
'@rolldown/binding-android-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -8051,6 +8130,57 @@ snapshots:
react: 19.2.5
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1)
'@resvg/resvg-js-android-arm-eabi@2.6.2':
optional: true
'@resvg/resvg-js-android-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-arm64@2.6.2':
optional: true
'@resvg/resvg-js-darwin-x64@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm-gnueabihf@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-arm64-musl@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-gnu@2.6.2':
optional: true
'@resvg/resvg-js-linux-x64-musl@2.6.2':
optional: true
'@resvg/resvg-js-win32-arm64-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-ia32-msvc@2.6.2':
optional: true
'@resvg/resvg-js-win32-x64-msvc@2.6.2':
optional: true
'@resvg/resvg-js@2.6.2':
optionalDependencies:
'@resvg/resvg-js-android-arm-eabi': 2.6.2
'@resvg/resvg-js-android-arm64': 2.6.2
'@resvg/resvg-js-darwin-arm64': 2.6.2
'@resvg/resvg-js-darwin-x64': 2.6.2
'@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2
'@resvg/resvg-js-linux-arm64-gnu': 2.6.2
'@resvg/resvg-js-linux-arm64-musl': 2.6.2
'@resvg/resvg-js-linux-x64-gnu': 2.6.2
'@resvg/resvg-js-linux-x64-musl': 2.6.2
'@resvg/resvg-js-win32-arm64-msvc': 2.6.2
'@resvg/resvg-js-win32-ia32-msvc': 2.6.2
'@resvg/resvg-js-win32-x64-msvc': 2.6.2
'@rolldown/binding-android-arm64@1.0.0-rc.17':
optional: true