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>
This commit is contained in:
Nicolas 2026-08-18 11:17:55 +02:00
parent 5890c62462
commit 11b03b0e56
4 changed files with 96 additions and 3 deletions

View file

@ -5,6 +5,7 @@ import {
} from "@batch-cooking/shared"; } from "@batch-cooking/shared";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AllergenIcon, DietPreferencesIcon } from "../../layouts/nav-icons";
import { AllergenBadges } from "./AllergenBadges"; import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges"; import { DietBadges } from "./DietBadges";
import "./recipes.scss"; import "./recipes.scss";
@ -23,6 +24,11 @@ const ALL_CATEGORIES = "ALL" as const;
* rationale as `AllergySelect`/`DietSelect`. `excludeIds` (already-selected * rationale as `AllergySelect`/`DietSelect`. `excludeIds` (already-selected
* ingredients a recipe's ingredient list, or a profile's disliked list) * ingredients a recipe's ingredient list, or a profile's disliked list)
* keeps the same one from being added twice. * keeps the same one from being added twice.
*
* The two toggle buttons trailing the search input show/hide the
* allergen/diet badge rows on every card a display preference local to
* this picker (not persisted), for whoever finds two rows of badges per
* card too noisy while just browsing/searching by name.
*/ */
export function IngredientPicker({ export function IngredientPicker({
ingredients, ingredients,
@ -38,6 +44,13 @@ export function IngredientPicker({
ALL_CATEGORIES, ALL_CATEGORIES,
); );
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
// Purely a display preference for this picker's own card grid — doesn't
// touch which ingredients `visible` includes, only whether their
// allergen/diet badges render. Defaults to shown (the previous, only
// behavior); collapsing them is an opt-in for whoever finds two rows of
// badges per card too noisy while just browsing/searching by name.
const [showAllergens, setShowAllergens] = useState(true);
const [showDiets, setShowDiets] = useState(true);
const normalizedQuery = query.trim().toLowerCase(); const normalizedQuery = query.trim().toLowerCase();
const visible = ingredients.filter((ingredient) => { const visible = ingredients.filter((ingredient) => {
@ -63,6 +76,26 @@ export function IngredientPicker({
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder={t("recipes.form.searchIngredientPlaceholder")} placeholder={t("recipes.form.searchIngredientPlaceholder")}
/> />
<div className="ingredient-picker__search-toggles">
<button
type="button"
className={`ingredient-picker__search-toggle${showDiets ? " active" : ""}`}
onClick={() => setShowDiets((v) => !v)}
aria-pressed={showDiets}
title={t("recipes.form.toggleDiets")}
>
<DietPreferencesIcon />
</button>
<button
type="button"
className={`ingredient-picker__search-toggle${showAllergens ? " active" : ""}`}
onClick={() => setShowAllergens((v) => !v)}
aria-pressed={showAllergens}
title={t("recipes.form.toggleAllergens")}
>
<AllergenIcon />
</button>
</div>
</div> </div>
<div className="ingredient-picker__categories"> <div className="ingredient-picker__categories">
@ -100,8 +133,8 @@ export function IngredientPicker({
{ingredient.icon} {ingredient.icon}
</span> </span>
<span className="ingredient-picker__card-name">{ingredient.name}</span> <span className="ingredient-picker__card-name">{ingredient.name}</span>
<AllergenBadges allergens={ingredient.allergens} /> {showAllergens && <AllergenBadges allergens={ingredient.allergens} />}
<DietBadges diets={ingredient.diets} /> {showDiets && <DietBadges diets={ingredient.diets} />}
</button> </button>
))} ))}
</div> </div>

View file

@ -684,8 +684,16 @@
border-radius: var(--radius-base); border-radius: var(--radius-base);
&__search { &__search {
display: flex;
align-items: center;
gap: var(--space-xs);
input { input {
width: 100%; flex: 1 1 auto;
// Own min-width: 0 (not just relying on the flex row's) — same
// shrink-to-fit gotcha as `.disliked-ingredients-field`, belt and
// suspenders since a text input's natural min width is its content.
min-width: 0;
padding: var(--space-sm); padding: var(--space-sm);
font-family: var(--font-body); font-family: var(--font-body);
font-size: var(--font-size-base); font-size: var(--font-size-base);
@ -696,6 +704,45 @@
} }
} }
// Show/hide toggles for the allergen/diet badge rows on every card below
// trailing the search input, same "icon button with an active state"
// pattern as `FavoriteStarButton`.
&__search-toggles {
display: flex;
flex-shrink: 0;
gap: var(--space-xs);
}
&__search-toggle {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2.25rem;
height: 2.25rem;
padding: 0;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface-alt);
color: var(--color-text-muted);
svg {
width: 1.1rem;
height: 1.1rem;
}
&:hover {
border-color: var(--color-primary);
color: var(--color-text);
}
&.active {
border-color: var(--color-primary);
color: var(--color-primary);
}
}
&__categories { &__categories {
display: flex; display: flex;
gap: var(--space-xs); gap: var(--space-xs);

View file

@ -128,3 +128,14 @@ export function PublicIcon() {
</Icon> </Icon>
); );
} }
/** Allergens — the show/hide allergen-badges toggle on `IngredientPicker`'s search bar. */
export function AllergenIcon() {
return (
<Icon>
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</Icon>
);
}

View file

@ -166,6 +166,8 @@
}, },
"dietsLabel": "Régime(s) associé(s)", "dietsLabel": "Régime(s) associé(s)",
"searchIngredientPlaceholder": "Rechercher un ingrédient…", "searchIngredientPlaceholder": "Rechercher un ingrédient…",
"toggleDiets": "Afficher/masquer les régimes alimentaires",
"toggleAllergens": "Afficher/masquer les allergènes",
"allCategories": "Tout", "allCategories": "Tout",
"noIngredientFound": "Aucun ingrédient trouvé.", "noIngredientFound": "Aucun ingrédient trouvé.",
"category": { "category": {