batchCooking/apps/web/src/features/planning/leftovers-basket.scss
Nicolas 1bc9304cdc
All checks were successful
CI / lint (push) Successful in 4m35s
CI / build (push) Successful in 4m51s
CI / intent-service-test (push) Successful in 5m42s
CI / e2e (push) Successful in 10m29s
CI / test (push) Successful in 17m43s
feat(planning): glisser-déposer un reste sur un autre créneau
Dernière PR (4/4) du chantier « gestion des restes » (issue #34).

`@dnd-kit/core` (+ `@dnd-kit/utilities`) — première et seule utilisation
de DnD de l'app. `PlanningPage` monte un `<DndContext>` (Mouse + Touch +
Keyboard sensors, distance d'activation 5px pour ne pas voler le clic
« Jeter ») autour de la grille et du panier.

- `LeftoversBasket` : chaque ligne a une poignée `⠿` `useDraggable` — le
  nœud draggable est la poignée elle-même (petite), pas toute la `<li>`,
  pour qu'une détection de collision au pointeur (`pointerWithin`, repli
  `closestCenter` pour le clavier) résolve proprement vers la seule case
  sous le curseur. `data` porte `sourceItemId`/`recipeName`/`available`.
- `MealCell` : `useDroppable` (`data: { date, weekDay, meal }`),
  surbrillance `--drop-over` au survol. Un item « restes »
  (`sourceItem !== null`) porte un badge ♻️ sur sa pastille.
- Au drop, `onDragEnd` ouvre `LeftoverAssignDialog` (quantité, défaut =
  disponible, plafonné) → `POST /planning/items/leftover`
  (`apiClient.assignLeftover`) → nouvel item patché dans `planning.items`.

Tests : `planning-leftovers.feature` (Gherkin) — parcours complet drag →
confirmation quantité → pastille ♻️ visible → compteur du panier à jour ;
+ jet d'un reste depuis le panier. Le drag est piloté par événements
souris bruts (mousedown poignée → mousemove ×2 → mouseup cible).
Vérifié localement : 2/2, et aucune régression sur
`leftovers-basket.cy.ts` / `planning-page.cy.ts` / `planning.feature`.

i18n : `planning.leftovers.*` (badge, poignée, dialogue). Docs :
`frontend-architecture.md`.

Closes #34

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:47:49 +02:00

139 lines
3 KiB
SCSS

// Planning header's leftovers basket — an icon button that toggles a
// popover of this week's still-available leftover pools. Same "absolutely
// positioned panel hanging off a relative wrapper" shape as
// `.calendar-popover` (week-navigator.scss).
.leftovers-basket {
position: relative;
&__toggle {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-base);
line-height: 1;
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text);
}
// Small pill showing how many source items still have leftovers.
&__count {
min-width: 1.1rem;
padding: 0 var(--space-xs);
font-size: var(--font-size-xs);
font-weight: 700;
text-align: center;
border-radius: 999px;
background: var(--color-primary);
color: var(--color-surface);
}
&__popover {
position: absolute;
top: calc(100% + var(--space-xs));
right: 0;
z-index: 10;
width: 20rem;
padding: var(--space-md);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
&__heading {
margin: 0 0 var(--space-sm);
font-size: var(--font-size-sm);
font-weight: 700;
}
&__empty {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
&__list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
margin: 0;
padding: 0;
list-style: none;
}
&__line {
display: flex;
align-items: center;
gap: var(--space-sm);
&.is-dragging {
opacity: 0.5;
}
}
// Grip handle — the only part that starts a drag, so a click on "Jeter"
// never gets hijacked. `touch-action: none` lets @dnd-kit's PointerSensor
// own the gesture on touch devices.
&__drag-handle {
flex-shrink: 0;
padding: 0 var(--space-xs);
font-size: var(--font-size-base);
line-height: 1;
cursor: grab;
color: var(--color-text-muted);
background: none;
border: none;
touch-action: none;
&:active {
cursor: grabbing;
}
}
&__meta {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
&__recipe {
font-weight: 600;
}
&__origin {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
&__qty {
flex-shrink: 0;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
&__discard {
flex-shrink: 0;
padding: var(--space-xs) var(--space-sm);
font-size: var(--font-size-sm);
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: var(--radius-base);
background: var(--color-surface);
color: var(--color-text-muted);
&:hover:not(:disabled) {
color: var(--color-error);
border-color: var(--color-error);
}
&:disabled {
opacity: 0.6;
cursor: default;
}
}
}