-
Résumé : {{ importResult.summary }}
+
+ -
+
+
{{ exp.title }}
+
{{ exp.company }}
+
+ {{ formatPeriod(exp) }}
+
+
+ {{ exp.description }}
+
+
+
+
+
+
+
+
+
+ Aucune expérience pour l'instant. Ajoute ta première expérience : c'est elle qui
+ nourrira tes CV.
+
-
-
{{ importResult.experiences.length }} expérience(s) détectée(s)
-
- - {{ e.title }} — {{ e.company }}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Compétences
+
+ Uniquement tes compétences réelles : elles font foi pour la génération de tes CV.
+
+
+
+
+
+ Ajouter une compétence
+
-
-
{{ importResult.skills.length }} compétence(s) détectée(s)
-
{{ importResult.skills.map((s) => s.label).join(', ') }}
+
+
+ -
+
+ {{ sk.label }}
+
+ · {{ SKILL_LEVEL_LABELS[sk.level] }}
+
+
+
+
+
+
+
+
+ Aucune compétence pour l'instant. Ajoute celles que tu maîtrises vraiment.
+
-
-
-
-
✓ Profil importé et enregistré.
-
+
+
+
+
+
+
+
Relecture
+
+ Vérifie la suggestion : rien n'est encore enregistré.
+
+
+
+
+ Intitulé : {{ importResult.headline }}
+
+
+ Résumé : {{ importResult.summary }}
+
+
+
+
+ {{ importResult.experiences.length }}
+ {{ importResult.experiences.length > 1 ? 'expériences détectées' : 'expérience détectée' }}
+
+
+ -
+
+ {{ e.title }} — {{ e.company }}
+
+
+
+
+
+
+ {{ importResult.skills.length }}
+ {{ importResult.skills.length > 1 ? 'compétences détectées' : 'compétence détectée' }}
+
+
+
+
+
+
+ Annuler
+
+
+ Confirmer et enregistrer
+
+
+
+
+
+
+
+
+
diff --git a/apps/app/pages/ui.vue b/apps/app/pages/ui.vue
index 3b49a27..9a49dcb 100644
--- a/apps/app/pages/ui.vue
+++ b/apps/app/pages/ui.vue
@@ -1,7 +1,32 @@
- Design system
- Tokens Tailwind + composants UI de base (THI-127).
+ Design system
+
+ Le langage visuel de {{ BRAND }} — tokens, composants et retours d'état (QA visuelle).
+
+
+
+
+
+
brand — indigo profond
+
+
+
+
ink — neutres chauds (stone)
+
+
+
+
+
+
+
+
+
+
+
+
Primaire
@@ -31,9 +219,14 @@ function simulate() {
Small
Medium
Large
+
+
+ Avec icône
+
+
-
+
+
+
+
+ Recommandé
+ Validé
+ À retravailler
+ Manquant
+ Brouillon
+
+
+
+
+
+
+ Toujours via @lucide/vue, stroke-width 1.75–2. Jamais d'émojis
+ ni de SVG inline ad hoc.
+
+
+ -
+
+ {{ item.name }}
+
+
+
+
+
@@ -63,5 +291,74 @@ function simulate() {
+
+
+
+
+ Plusieurs petits skeletons qui épousent la forme du contenu final, plutôt qu'un grand bloc.
+
+
+
+
+
+
+
+ Feedback non bloquant après une action — via useToast().
+
+
+
+ Toast succès
+
+
+ Toast erreur
+
+
+ Toast info
+
+
+
+
+
+
+
+ Pour les actions irréversibles — focus initial sur « Annuler », piège de focus, fermeture
+ avec Échap.
+
+ Supprimer une offre…
+
+
diff --git a/apps/app/public/favicon.svg b/apps/app/public/favicon.svg
new file mode 100644
index 0000000..b7ad32e
--- /dev/null
+++ b/apps/app/public/favicon.svg
@@ -0,0 +1,5 @@
+
diff --git a/apps/app/test/button-classes.spec.ts b/apps/app/test/button-classes.spec.ts
index a066fb3..77e42ef 100644
--- a/apps/app/test/button-classes.spec.ts
+++ b/apps/app/test/button-classes.spec.ts
@@ -9,9 +9,9 @@ describe('buttonClasses', () => {
})
it('maps each variant to its brand-token classes', () => {
- expect(buttonClasses('secondary')).toContain('ring-border')
+ expect(buttonClasses('secondary')).toContain('ring-border-strong')
expect(buttonClasses('ghost')).toContain('bg-transparent')
- expect(buttonClasses('danger')).toContain('bg-danger-500')
+ expect(buttonClasses('danger')).toContain('bg-danger-600')
})
it('maps each size to its dimensions', () => {
diff --git a/apps/app/test/usage.spec.ts b/apps/app/test/usage.spec.ts
index 2088325..bf082a1 100644
--- a/apps/app/test/usage.spec.ts
+++ b/apps/app/test/usage.spec.ts
@@ -81,6 +81,6 @@ describe('buildUsageSummary', () => {
expect(summary.counter.generation).toBe(5)
expect(summary.quotas.map((q) => q.type)).toEqual(['generation', 'export_pdf', 'extraction'])
const gen = summary.quotas.find((q) => q.type === 'generation')!
- expect(gen.exceeded).toBe(true) // 5/5 atteint
+ expect(gen.exceeded).toBe(true) // 5 utilisées ≥ quota gratuit (2)
})
})
diff --git a/packages/shared/src/usage.ts b/packages/shared/src/usage.ts
index 1e2ea80..3bfa86e 100644
--- a/packages/shared/src/usage.ts
+++ b/packages/shared/src/usage.ts
@@ -2,8 +2,9 @@
* Metering & quotas (THI-126) — contrats partagés client ↔ serveur (Nitro).
*
* On mesure l'usage DÈS le MVP même si tout est gratuit, pour préparer le billing
- * freemium (abo ~9–15 €/mois). Pas de paywall Stripe au MVP : les quotas sont
- * paramétrables et appliqués côté serveur, mais aucune facturation n'est branchée.
+ * à crédits (2 générations offertes puis packs — voir config/pricing.ts). Pas de
+ * paywall Stripe au MVP : les quotas sont paramétrables et appliqués côté serveur,
+ * mais aucune facturation n'est branchée.
*
* Les fonctions de ce module sont PURES (aucun accès base / framework) → testables
* unitairement et réutilisables côté front pour afficher l'usage courant.
@@ -58,12 +59,13 @@ export const EMPTY_USAGE_SNAPSHOT: UsageCounterSnapshot = {
export type UsageQuotas = Partial
>
/**
- * Palier gratuit du MVP. Pas de paywall : on autorise quelques générations/exports
- * gratuits par mois. À surcharger via config quand le billing freemium arrivera.
+ * Palier gratuit — modèle acté (2026-06) : 2 générations offertes pour tester,
+ * ensuite achat de crédits (config/pricing.ts). Les exports PDF sont plus larges
+ * (ré-export d'un CV déjà généré) mais bornés pour éviter l'abus de Chromium.
*/
export const FREE_TIER_QUOTAS: UsageQuotas = {
- generation: 5,
- export_pdf: 5,
+ generation: 2,
+ export_pdf: 4,
}
/** État d'un quota pour un type donné, à afficher / à appliquer. */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b3d035e..cb23ad9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
'@cvo/shared':
specifier: workspace:*
version: link:../../packages/shared
+ '@lucide/vue':
+ specifier: ^1.17.0
+ version: 1.17.0(vue@3.5.35(typescript@5.9.3))
'@prisma/client':
specifier: ^6.2.1
version: 6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3)
@@ -51,6 +54,9 @@ importers:
specifier: ^4.4.3
version: 4.4.3
devDependencies:
+ '@nuxt/fonts':
+ specifier: ^0.14.0
+ version: 0.14.0(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
'@tailwindcss/vite':
specifier: ^4.0.0
version: 4.3.0(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
@@ -301,6 +307,10 @@ packages:
commander:
optional: true
+ '@capsizecss/unpack@4.0.0':
+ resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==}
+ engines: {node: '>=18'}
+
'@clack/core@1.4.1':
resolution: {integrity: sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==}
engines: {node: '>= 20.12.0'}
@@ -742,6 +752,11 @@ packages:
'@kwsites/promise-deferred@1.1.1':
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
+ '@lucide/vue@1.17.0':
+ resolution: {integrity: sha512-6Q1ZHgr5FbmJzKWe5BxlNdjLj2lbmuH1zwDtVzUJofX0w9UREwKgq4F4jwKqFYyyIS4Rj3FiJvDi2k6djukmmw==}
+ peerDependencies:
+ vue: '>=3.0.1'
+
'@mapbox/node-pre-gyp@2.0.3':
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
engines: {node: '>=18'}
@@ -805,6 +820,9 @@ packages:
'@vitejs/devtools':
optional: true
+ '@nuxt/fonts@0.14.0':
+ resolution: {integrity: sha512-4uXQl9fa5F4ibdgU8zomoOcyMdnwgdem+Pi8JEqeDYI5yPR32Kam6HnuRr47dTb97CstaepAvXPWQUUHMtjsFQ==}
+
'@nuxt/kit@3.21.8':
resolution: {integrity: sha512-kg63DUPY5AHPn+9XM7u8rYcdWHXjzwfUscgRDuiC5YUciQ+xdLRhdwXelYFxEAx2nxJHossliiQXbMm/Fleivw==}
engines: {node: '>=18.12.0'}
@@ -2718,6 +2736,23 @@ packages:
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+ fontaine@0.8.0:
+ resolution: {integrity: sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg==}
+ engines: {node: '>=18.12.0'}
+
+ fontkitten@1.0.3:
+ resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
+ engines: {node: '>=20'}
+
+ fontless@0.2.1:
+ resolution: {integrity: sha512-mUWZ8w91/mw2KEcZ6gHNoNNmsAq9Wiw2IypIux5lM03nhXm+WSloXGUNuRETNTLqZexMgpt7Aj/v63qqrsWraQ==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ vite: '*'
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
@@ -3139,6 +3174,9 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ magic-regexp@0.10.0:
+ resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==}
+
magic-regexp@0.11.0:
resolution: {integrity: sha512-LG77Z/gVnwz7oaDpD4heX6ryl+lcr4l1B2gnP4MMvt2pGhGC1Dfj7dl1pXpP4ih+VQFLuAadeKVa+lARAzfW+Q==}
@@ -4001,6 +4039,9 @@ packages:
text-decoder@1.2.7:
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
+ tiny-inflate@1.0.3:
+ resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -4114,6 +4155,9 @@ packages:
resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==}
engines: {node: '>=20'}
+ unifont@0.7.4:
+ resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==}
+
unimport@6.3.0:
resolution: {integrity: sha512-M+Dxk5W9WRd+8j56W9tp8lGW/dmMc7g5zj7BWQnEjKQhryBstqsi1V0izb0zHwSkEN8cSYV7K75/bykairV2tA==}
engines: {node: '>=18.12.0'}
@@ -4740,6 +4784,10 @@ snapshots:
cac: 6.7.14
citty: 0.2.2
+ '@capsizecss/unpack@4.0.0':
+ dependencies:
+ fontkitten: 1.0.3
+
'@clack/core@1.4.1':
dependencies:
fast-wrap-ansi: 0.2.2
@@ -5051,6 +5099,10 @@ snapshots:
'@kwsites/promise-deferred@1.1.1': {}
+ '@lucide/vue@1.17.0(vue@3.5.35(typescript@5.9.3))':
+ dependencies:
+ vue: 3.5.35(typescript@5.9.3)
+
'@mapbox/node-pre-gyp@2.0.3':
dependencies:
consola: 3.4.2
@@ -5187,6 +5239,46 @@ snapshots:
- utf-8-validate
- vue
+ '@nuxt/fonts@0.14.0(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))':
+ dependencies:
+ '@nuxt/devtools-kit': 3.2.4(magicast@0.5.3)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ '@nuxt/kit': 4.4.8(magicast@0.5.3)
+ consola: 3.4.2
+ defu: 6.1.7
+ fontless: 0.2.1(db0@0.3.4)(ioredis@5.11.1)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0))
+ h3: 1.15.11
+ magic-regexp: 0.10.0
+ ofetch: 1.5.1
+ pathe: 2.0.3
+ sirv: 3.0.2
+ tinyglobby: 0.2.17
+ ufo: 1.6.4
+ unifont: 0.7.4
+ unplugin: 3.0.0
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ transitivePeerDependencies:
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@netlify/blobs'
+ - '@planetscale/database'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - aws4fetch
+ - db0
+ - idb-keyval
+ - ioredis
+ - magicast
+ - uploadthing
+ - vite
+
'@nuxt/kit@3.21.8(magicast@0.5.3)':
dependencies:
c12: 3.3.4(magicast@0.5.3)
@@ -7098,6 +7190,58 @@ snapshots:
flatted@3.4.2: {}
+ fontaine@0.8.0:
+ dependencies:
+ '@capsizecss/unpack': 4.0.0
+ css-tree: 3.2.1
+ magic-regexp: 0.10.0
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ ufo: 1.6.4
+ unplugin: 2.3.11
+
+ fontkitten@1.0.3:
+ dependencies:
+ tiny-inflate: 1.0.3
+
+ fontless@0.2.1(db0@0.3.4)(ioredis@5.11.1)(vite@7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)):
+ dependencies:
+ consola: 3.4.2
+ css-tree: 3.2.1
+ defu: 6.1.7
+ esbuild: 0.27.7
+ fontaine: 0.8.0
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ ohash: 2.0.11
+ pathe: 2.0.3
+ ufo: 1.6.4
+ unifont: 0.7.4
+ unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
+ optionalDependencies:
+ vite: 7.3.5(@types/node@22.19.20)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0)
+ transitivePeerDependencies:
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@netlify/blobs'
+ - '@planetscale/database'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - aws4fetch
+ - db0
+ - idb-keyval
+ - ioredis
+ - uploadthing
+
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
@@ -7476,6 +7620,16 @@ snapshots:
dependencies:
yallist: 3.1.1
+ magic-regexp@0.10.0:
+ dependencies:
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ mlly: 1.8.2
+ regexp-tree: 0.1.27
+ type-level-regexp: 0.1.17
+ ufo: 1.6.4
+ unplugin: 2.3.11
+
magic-regexp@0.11.0:
dependencies:
magic-string: 0.30.21
@@ -8569,6 +8723,8 @@ snapshots:
transitivePeerDependencies:
- react-native-b4a
+ tiny-inflate@1.0.3: {}
+
tiny-invariant@1.3.3: {}
tinybench@2.9.0: {}
@@ -8659,6 +8815,12 @@ snapshots:
unicorn-magic@0.4.0: {}
+ unifont@0.7.4:
+ dependencies:
+ css-tree: 3.2.1
+ ofetch: 1.5.1
+ ohash: 2.0.11
+
unimport@6.3.0(oxc-parser@0.132.0):
dependencies:
acorn: 8.16.0