diff --git a/apps/web/index.html b/apps/web/index.html
index a4a4648..f528865 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -2,13 +2,23 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
Rubis Sur l'Ongle
diff --git a/apps/web/package.json b/apps/web/package.json
index f3a0467..f3650da 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -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",
diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png
new file mode 100644
index 0000000..361d968
Binary files /dev/null and b/apps/web/public/apple-touch-icon.png differ
diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg
index 162a2cd..88d7213 100644
--- a/apps/web/public/favicon.svg
+++ b/apps/web/public/favicon.svg
@@ -1,3 +1,22 @@
-
\ No newline at end of file
+
diff --git a/apps/web/public/icon-192.png b/apps/web/public/icon-192.png
new file mode 100644
index 0000000..487ef8b
Binary files /dev/null and b/apps/web/public/icon-192.png differ
diff --git a/apps/web/public/icon-512.png b/apps/web/public/icon-512.png
new file mode 100644
index 0000000..653ac09
Binary files /dev/null and b/apps/web/public/icon-512.png differ
diff --git a/apps/web/public/icon.svg b/apps/web/public/icon.svg
new file mode 100644
index 0000000..88d7213
--- /dev/null
+++ b/apps/web/public/icon.svg
@@ -0,0 +1,22 @@
+
diff --git a/apps/web/public/site.webmanifest b/apps/web/public/site.webmanifest
new file mode 100644
index 0000000..d59cbcf
--- /dev/null
+++ b/apps/web/public/site.webmanifest
@@ -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"
+ }
+ ]
+}
diff --git a/apps/web/scripts/generate-icons.mjs b/apps/web/scripts/generate-icons.mjs
new file mode 100644
index 0000000..f99cf56
--- /dev/null
+++ b/apps/web/scripts/generate-icons.mjs
@@ -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')
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 10bf62f..46b4607 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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