batchCooking/apps/web/src/components/ui/BottomSheet.tsx
Nicolas 0fd4928efc
All checks were successful
CI / lint (push) Successful in 51s
CI / build (push) Successful in 2m10s
CI / intent-service-test (push) Successful in 3m58s
CI / e2e (push) Successful in 8m26s
CI / test (push) Successful in 16m33s
CI / build-android (push) Has been skipped
feat(layout): socle UI mobile - nav, feuille et dialogs plein ecran
Fondation du chantier UI mobile & tablette (#91) : jusqu'ici, toute
adaptation mobile de ce repo etait du CSS pur sur le meme DOM (aucun
composant ne branchait son JSX selon la largeur/plateforme). Ce ticket
introduit le premier vrai branchement, et les primitifs que les 8
tickets suivants du chantier reutiliseront.

- useIsMobileViewport() (apps/web/src/lib/) : point de verite unique,
  base sur matchMedia, live (reagit a un resize/une rotation).
- _breakpoints.scss : $phone/$tablet Sass + mixins phone/phone-up/
  tablet-down, remplacant les nombres magiques 640/1024 repetes en dur
  dans 6 fichiers (AppLayout, landing, recipes, recipe-picker-dialog,
  planning-page, dialog, tours-page). Les deux occurrences de 860px
  (landing hero, tours-page) sont deliberement laissees telles quelles
  - un collapse de grille 2 colonnes qui n'a rien a voir avec le
  breakpoint tablette du catalogue recettes, les unifier aurait change
  un comportement visuel reel sans raison.
- useNativeDialogBehavior : mecanique (showModal/close event/clic
  exterieur) extraite de Dialog.tsx, partagee avec le nouveau
  BottomSheet.tsx (feuille mobile plein-largeur, meme fondation
  <dialog> natif que Dialog, juste ancree en bas).
- AppLayout : sur telephone, la sidebar n'est plus reflow-ee en CSS en
  barre horizontale (l'ancien traitement) - elle est remplacee par une
  vraie barre d'onglets basse (Planning/Recettes/Liste de courses +
  Plus), le "Plus" ouvrant une BottomSheet combinant Parametres et
  Compte (deux dropdowns flottants separes cote desktop -> un seul
  point d'entree mobile, IA plus simple, un seul emplacement d'onglet).
- dialog.scss : vrai plein-ecran (bord a bord, sans arrondi) sous le
  breakpoint phone au lieu d'une marge simplement reduite.

Verifie en local : pnpm --filter web exec tsc -b --force, biome check,
pnpm --filter web build, suite Cypress e2e complete (140/140) et
composants (63/63) - y compris layout.cy.ts, reecrit pour tester la
nouvelle barre basse (l'ancien test de la barre horizontale n'a plus
de sens, ce traitement n'existe plus).

Closes #91

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:03:43 +02:00

65 lines
2.7 KiB
TypeScript

import type { ReactNode } from "react";
import "./bottom-sheet.scss";
import { useNativeDialogBehavior } from "./useNativeDialogBehavior";
/**
* A native `<dialog>` anchored to the bottom edge instead of centered —
* the mobile-native replacement for a small anchored floating popover
* (a menu, a short list of actions) that a phone-width screen has no good
* place to float. Same open/close mechanics as `Dialog.tsx` (via the
* shared `useNativeDialogBehavior`: `showModal()` on mount, Escape/`close`
* event, tap-outside-to-dismiss) — only the panel's own position/shape
* differs, so both get the browser's focus trap and top-layer stacking for
* free.
*
* First consumer: `AppLayout`'s mobile nav, for the "Plus" tab (Paramètres
* + Compte, previously two separate floating dropdowns that had no
* viewport-edge handling at all — see the mobile/tablet UI chantier's own
* audit). Meant to be reused by the planning popovers (leftovers basket,
* week calendar) in a later ticket of that same chantier — kept generic
* (title + children, no planning-specific assumptions) for that reason.
*
* Not used on desktop: a sheet sliding up from the bottom of a full
* viewport only reads as "anchored to what you tapped" on a narrow screen
* where the tapped control is usually near the bottom already (a bottom
* tab bar, a floating action button). Desktop keeps the existing anchored
* dropdown/popover pattern for the same triggers — callers decide which to
* render (typically via `useIsMobileViewport()`), this component doesn't
* check the viewport itself.
*/
export function BottomSheet({
onClose,
title,
children,
}: {
onClose: () => void;
title?: string;
children: ReactNode;
}) {
const dialogRef = useNativeDialogBehavior(onClose);
return (
<dialog ref={dialogRef} className="bottom-sheet" aria-label={title}>
{/* Purely decorative drag-affordance — this sheet closes via
tap-outside/Escape/the close button, never an actual drag
gesture, but the grabber bar is still the expected visual
convention for "this slid up from the bottom" on both Android and
iOS, so it's worth keeping even without the gesture behind it. */}
<div className="bottom-sheet__grabber" aria-hidden="true" />
{title && (
<div className="bottom-sheet__header">
<h2>{title}</h2>
<button
type="button"
className="bottom-sheet__close"
onClick={() => dialogRef.current?.close()}
aria-label="Fermer"
>
✕
</button>
</div>
)}
<div className="bottom-sheet__body">{children}</div>
</dialog>
);
}