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>
139 lines
3 KiB
SCSS
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;
|
|
}
|
|
}
|
|
}
|