Commit graph

19 commits

Author SHA1 Message Date
73ae8169a1 fix(recipes): retire l'import manuel, le planning importe seul
Correction de comportement sur la gestion des recettes de sources
externes — l'implémentation précédente avait dérivé d'une lecture
erronée du besoin :

- Plus aucun bouton d'import nulle part. Parcourir une source
  (RecipesPage, hors planning) ne fait plus jamais que prévisualiser
  — RecipeDetailPanel n'affiche plus de lien "Importer cette
  recette", seulement un bouton icône discret vers la page d'origine
  quand la recette en a une (nouveau .recipe-detail-panel__source-link,
  même emplacement que l'étoile favori).
- Une recette externe n'est importée dans la base qu'au moment où
  quelqu'un l'ajoute effectivement à son planning — jamais avant.
  RecipePickerDialog.handleSelectDraftItem est désormais le seul
  endroit de toute l'appli qui importe quoi que ce soit : cliquer sur
  un item pas encore importé y déclenche une tentative d'import
  transparente (POST /sources/.../import puis POST /planning/items),
  sans écran intermédiaire, dès que rien ne manque
  (tryBuildCompleteImport, nouveau apps/web/src/features/recipes/
  recipe-import-draft.ts). Seul un ingrédient non résolu (ou une
  erreur réseau) fait encore basculer vers l'écran de revue existant
  (ImportRecipePage), pré-rempli, pour compléter ce qui manque.
- RecipeSourcesPanel gagne onSelectDraftItem (remplace planningSlot,
  qui n'a plus de raison d'être puisqu'il n'y a plus de lien d'import
  à qui le transmettre) : quand ce callback est fourni
  (RecipePickerDialog uniquement), un item pas encore importé n'est
  plus prévisualisé sur place, il est remonté tel quel à l'appelant.

Tests :
- planning.feature : le scénario existant retire l'étape "je clique
  le lien Importer cette recette" (redirection désormais automatique
  puisque le draft de test a un ingrédient non résolu) ; nouveau
  scénario pour le chemin transparent (draft entièrement résolu,
  aucun écran de revue).
- recipe-sources.feature : le scénario qui important depuis /recettes
  (hors planning) est supprimé — cette capacité n'existe plus hors
  planning. Le scénario de deep-link vérifie maintenant l'absence du
  bouton d'import et la présence du lien discret.
- pnpm exec tsc -b --force (web) — propre.
- pnpm exec biome check — propre.
- pnpm --filter web build — propre.
- Cypress non exécutable localement sur cette machine (crash GPU
  Electron connu) — scénarios vérifiés par relecture attentive
  contre le markup/les clés i18n réels ; CI (GitHub Actions) fera
  foi à l'exécution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 21:14:10 +02:00
4381e63045 feat(recipes): chaque source activée devient sa propre tab
Nouvelle correction demandée sur cette PR : l'onglet générique
« Sources » (avec un <select> interne quand le foyer en a activé
plusieurs) devient une tab à part entière par source activée — au même
niveau que Favoris/Perso/Foyer/Publique, plus transparent qu'un
sélecteur caché dans un sous-menu.

- `RecipeTabs` accepte désormais `sources: SourceView[]` et rend une
  tab par source (icône propre à la source si elle en a une, sinon
  l'icône générique `SourcesIcon` ; libellé = le nom réel de la
  source, pas une clé i18n). Nouveau type `RecipesPageTab` en
  `RecipeTab | "source:<key>"`, avec `sourceTabValue`/
  `parseSourceTabValue`/`isSourceTab` comme seul point d'assemblage/
  lecture de ce format.
- Nouveau hook partagé `useEnabledSources` (déplacé hors de
  `RecipeSourcesPanel`, maintenant utilisé par `RecipesPage` ET
  `RecipePickerDialog` pour construire leurs tabs).
- `RecipeSourcesPanel` simplifié : `sourceKey` devient une prop requise
  (fournie par la tab elle-même) au lieu d'un état interne avec son
  propre sélecteur — plus de `<select>`, plus de message « aucune
  source activée » (une tab qui n'existe pas ne peut plus être
  cliquée). Remonté via `key={sourceKey}` par l'appelant au changement
  de tab, même convention que `RecipePickerDialog`/`CalendarPopover`
  ailleurs dans l'app.
- Un bug distinct trouvé en écrivant ce changement : passer tel quel
  `initialSelection` (dérivé de l'URL) au panneau nouvellement monté
  en changeant directement de tab source à tab source aurait fait
  prévisualiser l'ancien item contre la nouvelle source. Gardé en ne
  transmettant `initialSelection` que lorsqu'il appartient réellement
  à `activeSourceKey`.

Aucun changement backend.

Tests :
- Vérifié manuellement en local (foyer avec TheMealDB activé) :
  tab dédiée dans /recettes et dans le sélecteur du planning, parcours
  d'un item, aperçu unifié, aucune régression console.
- Cypress : `recipe-sources.feature`/`planning.feature` mis à jour
  (« I click the button "Sources" » → « ... "TheMealDB" »), scénario
  « aucune source activée » réécrit pour vérifier l'absence de tab
  plutôt qu'un message dans un onglet qui n'existe plus.
- `pnpm exec tsc -b --force` (web) — propre.
- `pnpm exec biome check` — propre.
- `pnpm --filter web build` — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 20:31:09 +02:00
368ea08960 feat(recipes): unifie l'affichage des recettes externes avec les recettes réelles
Suite au retour utilisateur sur le plan « onglet Sources » livré
précédemment (#44-#48) : navigation transparente, page recette pour un
item externe, comportement d'ajout au planning déjà importé.

L'onglet « Sources » (RecipesPage) reste un onglet à part (décision
explicite : pas de fusion des listes perso/foyer/publique/externe) —
mais son affichage se comporte désormais « comme si c'était importé » :

- `SourceItemPreviewPanel` est supprimé, fusionné dans
  `RecipeDetailPanel` lui-même (nouvel état `"loaded-draft"`) : un item
  pas encore importé se voit exactement comme une vraie recette — même
  en-tête, même mise en page description/étapes — la seule différence
  étant les actions proposées (« Importer cette recette » là où une
  vraie recette montre Modifier/Supprimer). La liste brute des
  ingrédients et l'indice « non résolu » disparaissent de cette vue :
  cette complexité reste réservée à l'écran de revue d'import
  (ImportRecipePage), pas à un simple aperçu.
- Un item pas encore importé gagne une vraie URL adressable —
  `/recettes/sources/:sourceKey/:externalId` (nouvelle route,
  RecipesPage) — au même titre qu'une vraie recette a `/recettes/:id`.
  Avant, le sélectionner ne changeait que de l'état React local dans
  `RecipeSourcesPanel`, sans URL propre : ni lien direct, ni retour
  arrière/rafraîchissement possibles. `RecipeSourcesPanel` gagne
  `initialSelection`/`onItemSelected` pour rester piloté par cette URL
  sans avoir à connaître le routage lui-même — `RecipePickerDialog`
  (qui prévisualise dans une modale sans URL propre) laisse les deux
  non renseignés et garde son comportement inchangé.
- `onSelectImportedRecipe` (déjà présent) continue de traiter un item
  déjà importé exactement comme une vraie recette — c'est justement ce
  qui rend la navigation transparente pour ce cas.

Le troisième point du retour (vérifier si la recette est déjà en base
avant de l'ajouter au planning, ne rien faire si oui, l'importer sinon)
était déjà le comportement de #48 — inchangé ici, aucune régression:
`RecipePickerDialog` résout un item déjà importé vers sa vraie recette
sans ré-import, et n'importe que les items qui ne le sont pas encore.

Aucun changement backend.

Tests :
- Cypress : nouvelle assertion d'URL dans le scénario « Previews a
  not-yet-imported item » de recipe-sources.feature, et nouveau
  scénario « Deep-links straight to a not-yet-imported item's own
  page » — la CI confirmera.
- `pnpm --filter api test` — 282 tests toujours au vert (aucun
  changement backend).
- `pnpm exec tsc -b --force` (web) — propre.
- `pnpm exec biome check` — propre.
- `pnpm -r build` — propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 19:30:35 +02:00
e4c8d910d4 feat(recipes): écran de revue et finalisation de l'import (étape 3/4)
Backend :
- `createRecipe` refactorisé en fine enveloppe autour d'un nouvel
  helper interne `createRecipeInternal`, paramétré par une source
  d'import optionnelle ; nouvelle fonction exportée
  `createImportedRecipe` qui réutilise toute la validation
  ingrédients/unités/diets et le matching des tech steps, sans
  dupliquer cette logique.
- La locale de l'adaptateur source est propagée jusqu'au chargement
  des `TechStepMapping`, pour que le texte anglais (TheMealDB, etc.)
  soit matché contre le bon jeu de règles au lieu du défaut français.
- Nouvel endpoint `POST /sources/:sourceKey/import/:externalId` —
  valide le payload via `createRecipeSchema` (même schéma qu'une
  création manuelle) et persiste une vraie `Recipe` liée à la source
  (`sourceId`/`externalId`).
- Nouveau code d'erreur `RECIPE_ALREADY_IMPORTED` (4022) quand
  l'item a déjà été importé pour ce foyer.

Frontend :
- `ImportRecipePage` (nouvelle page, `/recettes/importer/:sourceKey/:externalId`) —
  pré-remplit le formulaire depuis `previewSourceItem`, en miroir de
  `RecipeFormPage` (mêmes sous-composants : `IngredientRow`,
  `IngredientPicker`, `StepListEditor`, `DietTagSelect`). Ajoute une
  section dédiée aux lignes d'ingrédients non résolues automatiquement :
  l'utilisateur choisit un ingrédient réel via l'`IngredientPicker`
  existant ou retire la ligne — aucune recette invalide n'est jamais
  soumise, le bouton d'import reste désactivé tant qu'il en reste.
- `SourceItemPreviewPanel` gagne un lien « Importer cette recette »
  vers cet écran.

Tests :
- Mocha (`apps/api/test/sources.test.ts`) : 6 nouveaux tests sur
  `POST /sources/:sourceKey/import/:externalId` (payload valide,
  ingrédient/unité inconnus, déjà importé, deux foyers distincts,
  locale de la source respectée pour les tech steps). 282 tests
  passent au total, aucune régression.
- Cypress : nouveau scénario Gherkin bout-en-bout dans
  `recipe-sources.feature` (parcourir → prévisualiser → importer →
  résoudre un ingrédient non reconnu → confirmer → atterrir sur la
  recette sauvegardée). Steps d'édition d'ingrédients/étapes
  génériques déplacés de `recipe-form.ts` vers
  `cypress/support/step_definitions/common.steps.ts`, réutilisables
  par ce nouveau scénario.

Suite : étape 4 (ajouter au planning déclenche l'import si nécessaire).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:56:06 +02:00
b5a12cf489 feat(recipes): onglet Sources — parcourir les recettes externes (étape 2/4)
Deuxième étape du chantier "onglet Sources" : l'UI de parcours, construite
contre les endpoints backend de l'étape 1 (#45). L'onglet désactivé
placeholder de RecipeTabs devient un vrai onglet fonctionnel.

- RecipeTabs.tsx : nouveau type RecipesPageTab (RecipeTab | "sources") —
  gardé hors du type partagé RecipeTab puisque l'API n'a pas de
  tab=sources à valider. Un prop `tabs` optionnel restreint quels onglets
  s'affichent — RecipePickerDialog (choix d'une recette pour un planning)
  s'y restreint aux 4 onglets réels, parcourir des sources externes en
  plein milieu de ce dialogue n'a pas de sens sans le flux de revue/import.
- Nouveau RecipeSourcesPanel.tsx : contenu de l'onglet "Sources" —
  autonome (son propre master-detail), ne partage pas le fetching
  RecipeTab de RecipesPage puisqu'il parcourt le catalogue *live* d'une
  source (GET /sources/:key/browse), pas la table Recipe sauvegardée.
  Sélecteur de source si le foyer en a activé plusieurs ; sélectionner un
  item déjà importé navigue directement vers la vraie recette
  (SourceItemTable + navigate), un item pas encore importé affiche un
  aperçu en lecture seule (SourceItemPreviewPanel, réutilise
  StepDescription — les tech steps sont donc déjà surlignés dans
  l'aperçu).
- Bug trouvé et corrigé en écrivant le scénario Cypress : cliquer un item
  déjà importé changeait l'URL mais restait affiché sur l'onglet Sources
  (RecipesPage ne rend RecipeDetailPanel/RecipeTable qu'en dehors de
  l'onglet "sources"). RecipeSourcesPanel prend maintenant un callback
  `onViewImportedRecipe` pour repasser sur un onglet réel avant de
  naviguer.

Tests : nouveau recipe-sources.feature (parcours utilisateur complet —
onglet vide, parcours avec items importés/non importés, aperçu avec
surlignage de technique) ; recipes.cy.ts corrigé (assertion obsolète sur
l'ancien placeholder désactivé). Étape suivante (3/4) : écran de revue
(corriger les ingrédients non résolus) + finalisation de l'import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:14:20 +02:00
d0900ad7fd feat(recipes): surligne les tech steps détectés dans les étapes, avec tooltip
Expose côté UI ce que tech-step-matcher.ts détecte déjà à la sauvegarde
(Step.techSteps) mais qui restait backend-only : dans le panneau détail
d'une recette, les mots exacts ayant déclenché une technique sont
surlignés, avec un tooltip (survol/focus clavier) donnant son nom.

- tech-step-matcher.ts : matchTechStepSpans(description, mappings) expose
  désormais {techStepId, start, end} en plus de la simple séquence d'ids
  (déjà calculé en interne, jusqu'ici jeté). matchTechSteps devient un
  wrapper fin dessus — aucun changement à ses ~12 tests existants ni à
  recipe-translation.ts.
- StepTechStep gagne start/end (nullable, pas de backfill — même leçon que
  l'incident de migration ingredient_unit_catalog : NOT NULL sans défaut
  sur une table déjà peuplée casse le déploiement). Une ligne pré-existante
  sans span est simplement omise de la réponse API plutôt que de fuiter un
  null, jusqu'à ce que la recette soit resauvegardée.
- recipe.service.ts : createRecipe/updateRecipe persistent start/end ;
  StepView expose techSteps: { techStep: {id,key}, start, end }[]. Le
  recalcul complet à chaque édition (ajout/modif/suppression d'étape) était
  déjà garanti par le delete-then-recreate existant d'updateRecipe — testé
  explicitement (nouveau test "recomputes techniques from scratch...").
- Frontend : StepDescription.tsx (découpe le texte via
  highlight-tech-steps.ts, pur et testé) remplace le <p> brut dans
  RecipeDetailPanel. Nouveau Tooltip.tsx (composants/ui, CSS pur, aucune
  lib externe — même esprit que Dialog.tsx) : un <button> (focusable
  nativement, pas de tabIndex sur un <mark> non interactif) affiche le nom
  de la technique (catalog.techSteps.<key>) au survol/focus.

Tests : matchTechStepSpans (spans corrects, chevauchement résolu),
recipe.test.ts (forme API + recalcul complet sur modif/ajout/suppression
d'étape, avec vérification que les anciennes lignes StepTechStep sont bien
supprimées), splitDescriptionByTechSteps (tri, bornes invalides ignorées,
chevauchement résiduel ignoré), scénario Cypress recipes.feature
(surlignage + tooltip au focus).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 14:46:15 +02:00
0ff8775cf6 fix(recipes): élargir le sélecteur d'unité pour les labels longs
.ingredient-row__unit était encore dimensionné pour l'ancien champ
texte libre (6rem, suffisant pour "g"/"unité") — trop étroit
maintenant que c'est un <select> sur le catalogue, dont le plus long
label ("cuillère à soupe") était tronqué.
2026-08-19 22:40:02 +02:00
0cb1dccd92 feat(recipes): ajouter le nombre de portions couvertes par une recette
Ajoute Recipe.portions (combien de portions la recette produit telle
qu'écrite) — formulaire de création/édition, fiche détail, migration
Prisma (backfill à 4, même pattern que planning_item.portions).

Le sélecteur de recette du planning pré-remplit désormais son propre
champ "portions" depuis cette valeur au lieu de toujours démarrer à 1
(RecipeSummaryView.portions), tout en gardant PlanningItem.portions
indépendant (une recette peut être mise à l'échelle pour un créneau).

Couverture : tests API (création/édition/validation), scénarios
cypress (formulaire + préchargement en édition).
2026-08-19 21:44:01 +02:00
978fe71a11 feat(recipes): flag les ingrédients faisables maison + suggestion de recherche
Remplace la FK morte `Ingredient.alternateRecipeId` (jamais branchée nulle
part — confirmé par exploration : zéro usage en dehors de schema.prisma)
par un flag booléen `reproducible`, plus simple : pas de liaison
recette↔ingrédient en base, juste une info "ça vaut le coup d'être fait
maison" plus un raccourci de recherche.

- Migration : drop `alternate_recipe` (colonne + FK), ajoute
  `reproducible BOOLEAN NOT NULL DEFAULT false` sur `ingredients`.
- `reference-seed-data.ts` : `IngredientSeed` gagne `reproducible?`,
  threadé dans le flatten + la réconciliation `seedReferenceData`. Premier
  lot de 27 ingrédients marqués (pains, pâtes à cuire, sauces de base,
  bouillons/fonds) — même logique que la curation Ciqual : un lot solide
  plutôt qu'exhaustif sur les 546 ingrédients.
- `IngredientView` (shared) + les deux endroits qui la construisent
  (`reference.service.ts`, `recipe.service.ts`) gagnent `reproducible`.
- `ReproducibleBadge` (nouveau) : pastille "Faisable maison" — simple
  dans `IngredientPicker` (avec son propre toggle d'affichage), lien
  cliquable dans `IngredientRow` vers `/recettes?search=<nom>` ouvert
  dans un nouvel onglet (pour ne jamais perdre le formulaire de recette
  en cours — pas de persistance de brouillon dans `RecipeFormPage`).
- `RecipesPage` lit `?search=` au montage pour permettre ce deep-link.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:09:39 +02:00
kyuno053
c63bc15ea4
fix(web): sidebar reste fixe sur page longue, page Liste de courses centrée (#21)
* fix(web): sidebar reste fixe sur page longue, page Liste de courses centrée

- AppLayout: .app-layout passe de min-height:100vh à height:100vh +
  overflow:hidden, et .app-content devient le vrai conteneur de scroll
  (overflow-y:auto). Avant, une page plus haute que le viewport faisait
  défiler tout le document, entraînant la sidebar avec lui au lieu de la
  garder fixe.
- ComingSoonPage (page "Liste de courses"): ajoute margin:0 auto, comme
  .settings-page, au lieu de rester collée au bord gauche avec un grand
  espace vide à droite sur les écrans larges.

* fix(web): la grille recettes stretche le panneau de détail sur toute sa hauteur

Régression révélée par le fix du layout précédent (AppLayout.scss) : tant
que `.app-layout` grandissait sans limite (`min-height: 100vh`), rien
dans la chaîne de hauteurs (.app-content → .recipes-page →
.recipes-page__catalog → .recipe-detail-panel) n'avait de hauteur
réellement définie, donc `.recipe-detail-panel { height: 100% }` ne
contraignait jamais rien — le panneau grandissait pour tout afficher,
masquant silencieusement que sa grille parente (`&__catalog`, display:
grid) n'a jamais eu de `grid-template-rows` explicite.

Avec `.app-layout` maintenant fixé à `height: 100vh`, `&__catalog` a
enfin une hauteur définie — mais sans `grid-template-rows`, sa seule
ligne implicite reste dimensionnée à son contenu (auto) au lieu de
s'étirer sur toute la hauteur disponible, comme l'intention documentée
en commentaire (« both stretched to the same full height ») le disait
déjà. Résultat concret : le panneau de détail recette rendait plus
court que son propre contenu, qui se retrouvait coupé par son
`overflow-y: auto` — repéré via l'échec CI de recipes.cy.ts sur
"opens a recipe's detail alongside the table" (timeout sur la
description, `<p>` non visible car clippée par un parent en overflow).

Vérifié en réinjectant les mêmes fixtures dans le navigateur (mock
fetch temporaire) : le panneau passe de 296px à 448px de haut, la
description reste entièrement dans ses limites.

* fix(web): restaure le layout table+panneau empilé sous 1024px

Le commit précédent (grid-template-rows sur .recipes-page__catalog)
corrigeait bien le layout desktop (2 colonnes, 1 ligne étirée sur
toute la hauteur), mais la même règle s'appliquait aussi telle quelle
au breakpoint mobile/tablette (@media max-width: 1024px), où la grille
passe à 1 seule colonne — donc à 2 lignes empilées (table puis
panneau), pas 1. Forcer ces 2 lignes dans un seul track `minmax(0, 1fr)`
coupait la hauteur disponible en deux et donnait 0px à la ligne du
tableau, dont le contenu se retrouvait entièrement clippé par son
propre `overflow-y: auto` — repéré via le nouvel échec CI de
recipes.cy.ts (Ratatouille non visible), le viewport par défaut de
Cypress (1000×660) tombant sous ce seuil de 1024px.

Réinitialise `grid-template-rows: auto` dans ce même bloc `@media`, à
côté du `height: auto` qui y était déjà (même logique : plus de ligne
unique à hauteur fixe à ce breakpoint). Revérifié dans le navigateur
aux deux largeurs (1000px et 1600px) avec les mêmes fixtures que le
test.

* test(web): agrandit le viewport du test de détail recette

Dernier morceau de la même régression : la grille catalogue+panneau
(master-detail) est un layout desktop, comme la grille de planning
(voir planning-page.cy.ts's `cy.viewport(1600, 900)`, même
justification). Au viewport par défaut de Cypress (1000×660), le
panneau de détail n'a tout simplement pas assez de hauteur pour
afficher photo + en-tête + ingrédients + étapes sans avoir besoin de
scroller en interne — c'est le comportement voulu de
`.recipe-detail-panel` (`overflow-y: auto`, voir son commentaire dans
recipes.scss), pas un bug. Ce n'était jamais visible avant que
`.app-layout` ait une vraie hauteur bornée (voir les 2 commits
précédents) : tout grandissait sans limite, donc rien n'avait jamais
besoin de scroller pour être visible.

Reproduit en rejouant le flow exact du test dans le navigateur (visite
/recettes, clic sur la ligne Omelette, pas juste une navigation directe
vers /recettes/2 — la simple navigation directe ne reproduisait pas
l'échec) : à 1000×660 la description était bien coupée
(descWithinPanel: false) ; à 1600×900 tout rentre.
2026-08-19 10:42:30 +02:00
655c10ad95 fix(recipes): agrandit l'icône réglages, aligne les cases à cocher à gauche
- .ingredient-picker__display-toggle svg : 1.1rem -> 1.5rem, trop
  petite dans son bouton 2.25rem.
- .ingredient-picker__display-menu : neutralise le margin-left: auto
  du check-mark app-wide (global.scss, pensé pour une grille de chips
  avec coche en bout de ligne) pour ce menu vertical de préférences —
  coche à gauche, libellé à droite, plus classique pour ce type de menu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:47:52 +02:00
8d1c763add fix(recipes): menu d'options d'affichage explicite dans le picker d'ingrédients
Remplace les deux boutons icône seule (régimes/allergènes) par un
bouton réglages unique ouvrant un menu avec deux cases à cocher
labellisées — les icônes seules n'étaient pas assez explicites sur ce
qu'elles activaient/désactivaient.

En chemin, corrige un bug réel découvert pendant l'implémentation : les
cases à cocher rendaient invisibles (la règle globale "selectable
card" de global.scss masque le <input type="checkbox"> natif et
attend un <span class="check-mark"> + une classe is-selected sur le
<label> pour dessiner l'état coché — mes cases n'avaient ni l'un ni
l'autre). Corrigé en suivant exactement le même pattern que
AllergySelect.tsx.

Supprime AllergenIcon (nav-icons.tsx), devenu inutilisé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:35:02 +02:00
84003d2009 fix(recipes): empêche la sélection de texte native sur les chips/cartes du picker
Les boutons de filtre (catégorie/sous-catégorie/toggles) et les cartes
d'ingrédients n'avaient pas user-select: none — un clic qui bouge d'un
pixel (clic rapide, trackpad) est alors lu comme un glisser de
sélection de texte, et le surlignage natif du navigateur s'affiche à
la place de l'état .active attendu. Vu de l'extérieur ça ressemble à
un bug de CSS de sélection flaky ; c'est en fait ce surlignage natif
qui apparaît par intermittence.

Ajouté sur .ingredient-picker (hérité par tous ses boutons enfants),
avec un user-select: text explicite ré-appliqué sur le champ de
recherche pour ne pas casser le copier/coller dedans.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:21:45 +02:00
78e0f89503 feat(recipes): remplace les emoji par des icônes SVG épurées
Deux usages d'emoji supprimés, remplacés par un vrai jeu d'icônes SVG
(style ligne, cohérent avec layouts/nav-icons.tsx) :

- Catégories/sous-catégories du picker (auparavant préfixées d'un
  emoji dans les libellés) : nouvelles icônes CategoryIcon/
  SubcategoryIcon, une par catégorie/sous-catégorie.
- Icône par ingrédient (auparavant un emoji différent pour chacun des
  437 ingrédients) : remplacé par un petit vocabulaire de ~22
  pictogrammes génériques ("un légume", "une bouteille", "un
  fromage"...) réutilisés selon la nature de l'ingrédient plutôt
  qu'un dessin par ingrédient (irréaliste à la main pour 437 items).

- Schéma : nouvel enum IngredientIcon (22 valeurs), colonne
  Ingredient.icon passe de String? (texte libre) à IngredientIcon
  (non nullable, toujours une valeur générique pertinente désormais).
- reference-seed-data.ts : chaque groupe porte un defaultIcon (calqué
  sur sa sous-catégorie), avec override par ingrédient pour les
  exceptions (ex. les fromages dans "produits laitiers", les jus/cafés
  dans "assaisonnements").
- apps/web/src/features/recipes/ingredient-icons.tsx (nouveau) :
  22 icônes SVG + CategoryIcon/SubcategoryIcon (réutilisent le même
  vocabulaire pour représenter chaque catégorie/sous-catégorie).
- packages/shared : IngredientView.icon devient IngredientIcon (union
  de 22 valeurs) au lieu de string | null.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 12:30:25 +02:00
9775cbe38f feat(recipes): refonte de la catégorisation des ingrédients façon rayons de supermarché
Remplace les 18 catégories plates (mélangeant rayons génériques et
cuisines d'origine — un ingrédient pouvait finir dans "légumes" ou
"cuisine italienne" selon l'angle choisi) par une hiérarchie à 2 niveaux
sur le modèle d'un supermarché français :

- 🥦 Produits frais (légumes, fruits, herbes fraîches)
- 🥩 Boucherie & poissonnerie (viandes, volailles, poissons,
  crustacés & fruits de mer)
- 🥫 Épicerie sèche (féculents, légumineuses, graines & fruits secs,
  autres produits secs/conserves)
- 🍞 Boulangerie (pains, pâtes à cuire — 4 nouveaux ingrédients :
  pâte feuilletée/brisée/à pizza/à tarte sablée)
- 🧈 Crémerie & fromage (produits laitiers, œufs, alternatives
  végétales)
- 🧂 Condiments & épices (épices, sauces, assaisonnements)
- 🍳 Aides culinaires (bases, épaississants, sucres)

- Schéma : nouvel enum IngredientCategory (7 valeurs) + nouvel enum
  IngredientSubcategory (22 valeurs) + colonne Ingredient.subcategory.
  Migration appliquée via reset (données de référence, aucune perte
  réelle) car les anciennes valeurs d'enum n'existent plus dans les
  nouvelles.
- reference-seed-data.ts entièrement réorganisé par (catégorie, sous-
  catégorie), tous les 437 ingrédients recatégorisés un par un.
- packages/shared : INGREDIENT_CATEGORIES (7), INGREDIENT_SUBCATEGORIES
  (22) et INGREDIENT_CATEGORY_SUBCATEGORIES (mapping catégorie →
  sous-catégories, pour piloter le picker) remplacent l'ancienne liste
  plate.
- IngredientPicker : drill-down à 2 niveaux — la ligne de sous-
  catégories (couleur --color-tag, visuellement subordonnée) apparaît
  une fois une catégorie choisie, et se réinitialise au changement de
  catégorie.
- i18n : nouvelles clés recipes.form.category.* (7) et
  recipes.form.subcategory.* (22).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:47:47 +02:00
11b03b0e56 feat(recipes): toggles afficher/masquer allergènes et régimes dans le picker
Deux boutons icône en bout de la barre de recherche de l'IngredientPicker
(régimes/feuille, allergènes/triangle), même pattern bouton-icône +
état actif que FavoriteStarButton. Préférence d'affichage locale au
picker (non persistée) — n'affecte pas quels ingrédients apparaissent,
seulement si leurs badges allergènes/régimes sont visibles sur les
cartes. Nouvelle icône AllergenIcon dans nav-icons.tsx, réutilise
DietPreferencesIcon existante.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:17:55 +02:00
5890c62462 fix(recipes): overflow des préférences + régimes liés aux ingrédients
- Fix overflow horizontal sur /parametres/preferences : <fieldset> a un
  min-width: min-content par défaut du navigateur, ce qui empêchait la
  grille de l'IngredientPicker de wrapper (page entière poussée à
  ~3100px de large). Reset min-width: 0 sur .disliked-ingredients-field.
- Recatégorise les laits/crèmes végétaux (coco, amande, avoine) de
  PRODUITS_LAITIERS_OEUFS vers LIQUIDES_BOISSONS — ce ne sont pas des
  produits laitiers.
- Nouveau modèle IngredientDiet (many-to-many ingrédient <-> régime) :
  quels régimes (Végétarien, Végan, Pescétarien) chaque ingrédient
  respecte. Omnivore volontairement absent (trivial) et Sans gluten
  aussi (déjà dérivable de l'allergène Gluten existant).
- reference-seed-data.ts : chaque groupe de catégorie porte un
  defaultDiets, avec dietNames en override pour les exceptions
  (fromages, viandes, poissons, sauces à base d'œuf/poisson...).
- IngredientView.diets exposé par /reference/ingredients et /recipes,
  affiché via DietBadges dans IngredientPicker et IngredientRow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:01:57 +02:00
ea86a4a7f1 feat(recipes): catégories d'ingrédients + nouveau sélecteur
- Prisma: enum IngredientCategory (18 valeurs) + Ingredient.category,
  migration appliquée
- reference-seed-data.ts restructuré en 18 groupes de catégories
- packages/shared: INGREDIENT_CATEGORIES + IngredientView.category
- API: category exposé par /reference/ingredients et /recipes
- Web: nouveau IngredientPicker (chips catégories + recherche + grille
  de cartes) remplaçant IngredientAutocomplete, branché dans le
  formulaire de recette et le champ aliments-pas-aimés
- i18n: clés recipes.form.category.* et libellés du picker
- Fix test reference.test.ts pour la nouvelle forme d'IngredientView

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 10:24:12 +02:00
acab18ac4a feat(recipes): catalogue v2 - visibilité, favoris, régimes et catalogue d'ingrédients exhaustif
Recipe catalog v2:
- Recipe gagne visibility (PERSONAL/HOUSE/PUBLIC), authorId, authorHouseId
- Favoris par utilisateur (RecipeFavorite), régimes associés (RecipeDiet)
- Aliments "pas aimés" par utilisateur (UserProfileDislikedIngredient),
  distinct des allergies médicales
- API: GET /recipes?tab=favoris|perso|foyer|publique avec contrôle d'accès
  complet, POST/DELETE /recipes/:id/favorite, édition/suppression réservées
  à l'auteur (403 NOT_RECIPE_AUTHOR), GET/PATCH /profile/disliked-ingredients
- Frontend: vue maître-détail (onglets + tableau + panneau détail),
  formulaire enrichi (visibilité, régimes), section préférences pour les
  aliments pas aimés

Catalogue d'ingrédients de référence:
- Extension du seed de 39 à ~430 ingrédients (viandes, poissons/fruits de
  mer, légumes, fruits, féculents, condiments/sauces, épices/herbes, pains
  à sandwich, cuisines italienne/asiatique/mexicaine/maghrébine, liquides
  et boissons de cuisine, bouillons/fonds)
- Chaque ingrédient lié à ses allergènes UE (IngredientAllergy) — les 14
  allergènes réglementaires restent tous couverts
- Seeding optimisé en requêtes groupées (createMany/diff ciblé) plutôt
  qu'un upsert par ligne, pour garder resetDatabase() rapide en test

Tests: 102 tests Mocha + 32 scénarios BDD, tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 09:29:11 +02:00