- apps/api: les tests Planning (mocha et cucumber) lisaient l'horloge
systeme (new Date()/DateTime.utc()) pour construire leurs fixtures et
interroger /planning, ce qui les rendait non deterministes. Ajoute
test-support/reference-date.ts (TEST_REFERENCE_DATE, une date UTC
fixe) et l'utilise dans planning.test.ts / planning.steps.ts a la
place du systeme.
- apps/web: nouveau style global pour tous les radio/checkbox de
l'app (theme-select, allergy-select, onboarding) - "carte
selectionnable" : le controle natif reste reel/accessible mais
visuellement cache, toute la ligne devient la surface interactive
(bordure + fond teinte + coche au survol/selection). Corrige au
passage le bug de fond qui causait le desalignement des radios sur
/parametres/preferences-utilisateur (la regle generique
input, select { width: 100% } de profile-forms.scss s'appliquait
aussi aux checkbox/radio) et une regression de font-weight ou les
lignes non selectionnees du theme apparaissaient en gras comme si
elles l'etaient.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
58 lines
2.4 KiB
TypeScript
58 lines
2.4 KiB
TypeScript
import type { AllergyView } from "@batch-cooking/shared";
|
|
import "./profile-forms.scss";
|
|
|
|
interface AllergySelectProps {
|
|
legend: string;
|
|
allergies: AllergyView[];
|
|
value: number[];
|
|
onChange: (allergyIds: number[]) => void;
|
|
}
|
|
|
|
/**
|
|
* Multi-select (checkbox grid, not a native `<select multiple>` — far more
|
|
* discoverable/tappable, especially on the mobile viewport this app is
|
|
* eventually embedded into via Capacitor) for a group of allergens. Used
|
|
* both by the signup wizard's allergens step and the `/parametres/preferences`
|
|
* settings page, and rendered *twice* by each — once for allergies, once for
|
|
* intolerances (`AllergyView.kind` groups them; callers filter and pass
|
|
* two separate lists rather than this component knowing about the split).
|
|
* An empty `value` is a normal, valid state (no declared allergies, or
|
|
* this skippable step was skipped), not an incomplete one.
|
|
*
|
|
* `legend` (not a fixed internal label) — the same component serves both
|
|
* groups, only the heading differs. A `<fieldset>`/`<legend>` (not a bare
|
|
* `<label>`, which only associates with a single control) is the correct
|
|
* semantic label for a group of checkboxes.
|
|
*
|
|
* Receives `allergies` as a prop rather than fetching them itself — same
|
|
* rationale as `DietSelect`.
|
|
*/
|
|
export function AllergySelect({ legend, allergies, value, onChange }: AllergySelectProps) {
|
|
function toggle(id: number) {
|
|
onChange(value.includes(id) ? value.filter((existing) => existing !== id) : [...value, id]);
|
|
}
|
|
|
|
return (
|
|
<fieldset className="allergy-select">
|
|
<legend>{legend}</legend>
|
|
{allergies.map((allergy) => {
|
|
const checked = value.includes(allergy.id);
|
|
return (
|
|
<label
|
|
key={allergy.id}
|
|
// `is-selected` (not a `:has(:checked)` CSS rule) drives the
|
|
// selected look — chaining `:has(...):has(:checked)` to react to
|
|
// a *state* change (rather than a DOM mutation) turned out to be
|
|
// unreliable, so this stays a plain, always-correct React class
|
|
// instead of relying on CSS to derive it.
|
|
className={`allergy-select__option${checked ? " is-selected" : ""}`}
|
|
>
|
|
<input type="checkbox" checked={checked} onChange={() => toggle(allergy.id)} />
|
|
<span className="check-mark" aria-hidden="true" />
|
|
{allergy.name}
|
|
</label>
|
|
);
|
|
})}
|
|
</fieldset>
|
|
);
|
|
}
|