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>
This commit is contained in:
Nicolas 2026-08-18 13:35:02 +02:00
parent 84003d2009
commit 8d1c763add
4 changed files with 71 additions and 41 deletions

View file

@ -7,7 +7,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 { SettingsIcon } from "../../layouts/nav-icons";
import { AllergenBadges } from "./AllergenBadges"; import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges"; import { DietBadges } from "./DietBadges";
import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons"; import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons";
@ -32,10 +32,12 @@ const ALL = "ALL" as const;
* 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 * The settings menu trailing the search input shows/hides the allergen/diet
* allergen/diet badge rows on every card a display preference local to * badge rows on every card a display preference local to this picker (not
* this picker (not persisted), for whoever finds two rows of badges per * persisted), for whoever finds two rows of badges per card too noisy while
* card too noisy while just browsing/searching by name. * just browsing/searching by name. A labeled checkbox menu rather than two
* bare icon-only toggle buttons those turned out too ambiguous on their
* own (unclear what each icon meant without a label attached).
*/ */
export function IngredientPicker({ export function IngredientPicker({
ingredients, ingredients,
@ -57,6 +59,7 @@ export function IngredientPicker({
// badges per card too noisy while just browsing/searching by name. // badges per card too noisy while just browsing/searching by name.
const [showAllergens, setShowAllergens] = useState(true); const [showAllergens, setShowAllergens] = useState(true);
const [showDiets, setShowDiets] = useState(true); const [showDiets, setShowDiets] = useState(true);
const [isDisplayMenuOpen, setIsDisplayMenuOpen] = useState(false);
function selectCategory(next: IngredientCategory | typeof ALL) { function selectCategory(next: IngredientCategory | typeof ALL) {
setCategory(next); setCategory(next);
@ -90,25 +93,38 @@ 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"> <div className="ingredient-picker__display-options">
<button <button
type="button" type="button"
className={`ingredient-picker__search-toggle${showDiets ? " active" : ""}`} className={`ingredient-picker__display-toggle${isDisplayMenuOpen ? " active" : ""}`}
onClick={() => setShowDiets((v) => !v)} onClick={() => setIsDisplayMenuOpen((v) => !v)}
aria-pressed={showDiets} aria-expanded={isDisplayMenuOpen}
title={t("recipes.form.toggleDiets")} title={t("recipes.form.displayOptions")}
> >
<DietPreferencesIcon /> <SettingsIcon />
</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> </button>
{isDisplayMenuOpen && (
<div className="ingredient-picker__display-menu">
<label className={showDiets ? "is-selected" : ""}>
<input
type="checkbox"
checked={showDiets}
onChange={(e) => setShowDiets(e.target.checked)}
/>
<span className="check-mark" aria-hidden="true" />
{t("recipes.form.showDietsLabel")}
</label>
<label className={showAllergens ? "is-selected" : ""}>
<input
type="checkbox"
checked={showAllergens}
onChange={(e) => setShowAllergens(e.target.checked)}
/>
<span className="check-mark" aria-hidden="true" />
{t("recipes.form.showAllergensLabel")}
</label>
</div>
)}
</div> </div>
</div> </div>

View file

@ -726,20 +726,19 @@
} }
} }
// Show/hide toggles for the allergen/diet badge rows on every card below // Settings menu for the allergen/diet badge rows on every card below
// trailing the search input, same "icon button with an active state" // trailing the search input. A single gear button + labeled checkboxes
// pattern as `FavoriteStarButton`. // (not two bare icon-only toggles, the earlier design here) an icon
&__search-toggles { // alone couldn't say clearly enough which badge row it hid.
display: flex; &__display-options {
position: relative;
flex-shrink: 0; flex-shrink: 0;
gap: var(--space-xs);
} }
&__search-toggle { &__display-toggle {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
width: 2.25rem; width: 2.25rem;
height: 2.25rem; height: 2.25rem;
padding: 0; padding: 0;
@ -765,6 +764,31 @@
} }
} }
// Anchored below-right of the toggle same transient-overlay reasoning
// as `.app-sidebar__account-menu` (AppLayout.scss).
&__display-menu {
position: absolute;
z-index: 10;
top: calc(100% + var(--space-xs));
right: 0;
display: flex;
flex-direction: column;
gap: var(--space-sm);
padding: var(--space-sm);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
box-shadow: var(--shadow-md);
// Box/checkmark look comes from the app-wide "selectable card" rule in
// global.scss (`label:has(> input[type="checkbox"])`) this only adds
// what's specific to sitting inside a dropdown menu.
label {
font-size: var(--font-size-sm);
white-space: nowrap;
}
}
&__categories { &__categories {
display: flex; display: flex;
gap: var(--space-xs); gap: var(--space-xs);

View file

@ -128,14 +128,3 @@ 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,8 +166,9 @@
}, },
"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", "displayOptions": "Options d'affichage",
"toggleAllergens": "Afficher/masquer les allergènes", "showDietsLabel": "Régimes alimentaires",
"showAllergensLabel": "Allergènes",
"allCategories": "Tout", "allCategories": "Tout",
"allSubcategories": "Tout", "allSubcategories": "Tout",
"noIngredientFound": "Aucun ingrédient trouvé.", "noIngredientFound": "Aucun ingrédient trouvé.",