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>
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
import type { StepTechStepView } from "@batch-cooking/shared";
|
|
import { Fragment } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Tooltip } from "../../components/ui/Tooltip";
|
|
import { splitDescriptionByTechSteps } from "./highlight-tech-steps";
|
|
|
|
/**
|
|
* A recipe step's description, with every detected technique's exact
|
|
* matched words highlighted and given a {@link Tooltip} naming the
|
|
* technique (e.g. hovering/focusing "hacher" in "Hacher les oignons" shows
|
|
* "Hacher") — `RecipeDetailPanel`'s replacement for a bare `<p>{description}</p>`.
|
|
*
|
|
* `techStep.key` resolves its tooltip label through `catalog.techSteps.<key>`
|
|
* i18n, the same pattern every other reference catalog (diets, units, …)
|
|
* uses for its display text.
|
|
*/
|
|
export function StepDescription({
|
|
description,
|
|
techSteps,
|
|
}: {
|
|
description: string;
|
|
techSteps: StepTechStepView[];
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const segments = splitDescriptionByTechSteps(description, techSteps);
|
|
|
|
return (
|
|
<p>
|
|
{segments.map((segment, index) => {
|
|
// A segment's own text/techStep don't uniquely identify it (the
|
|
// same word can appear twice in one description) — index is the
|
|
// only thing that does, but this list is fully regenerated from
|
|
// `description`/`techSteps` on every render (never reordered or
|
|
// spliced in place), so using it as part of the key is safe here.
|
|
const key = `${index}-${segment.text}`;
|
|
if (!segment.techStep) return <Fragment key={key}>{segment.text}</Fragment>;
|
|
return (
|
|
<Tooltip key={key} content={t(`catalog.techSteps.${segment.techStep.key}`)}>
|
|
{/* A real <button>, not a <mark>, so it's natively focusable
|
|
(keyboard/screen-reader users can reach the tooltip) without
|
|
fighting the "non-interactive element" a11y lint a bare
|
|
tabIndex on <mark> would trip — styled to read as inline
|
|
highlighted text, not as a button (see .step-tech-step). */}
|
|
<button type="button" className="step-tech-step">
|
|
{segment.text}
|
|
</button>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</p>
|
|
);
|
|
}
|