All checks were successful
`table.planning-grid` a `height: 100%` mais des hauteurs de ligne pilotées
par le contenu : un créneau avec plusieurs recettes étirait son `<td>`,
donc toute la ligne (les autres cases gagnant la même hauteur morte sous
`vertical-align: top`), puis la table débordait de `.planning-grid-wrapper`
en scroll vertical.
- `tbody tr { height: 1px }` : les cinq lignes se partagent la hauteur du
wrapper à parts égales (une hauteur de ligne est un minimum que le
navigateur étire pour absorber la hauteur restante de la table), au lieu
d'être aussi hautes que la colonne la plus chargée.
- `.meal-cell__recipes` : `max-height: 8.5rem; overflow-y: auto` — au-delà
d'une poignée de recettes la liste scrolle dans la case. Cap en `rem` et
pas en `%` : les hauteurs en pourcentage sur un `<td>` ne se résolvent
pas de façon fiable selon le moteur. `.add-recipe-btn` (frère flex hors
de cette boîte) reste épinglé juste en dessous.
- `min-width: 62rem` laissé tel quel : les 7 colonnes ont besoin d'un
plancher pour rester lisibles, et le scroll horizontal dessous est
couvert par `layout.cy.ts`. Le breakpoint phone (vue `PlanningDayView`)
n'est pas concerné.
Closes #110
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
445 lines
12 KiB
SCSS
445 lines
12 KiB
SCSS
// =============================================================================
|
|
// Styles specific to PlanningPage — colocated next to PlanningPage.tsx since
|
|
// nothing else uses these classes. Ported from the reviewed HTML mockup
|
|
// (see the plan file / PR description) onto the app's real design tokens —
|
|
// no light/dark duplication needed here, unlike the standalone mockup:
|
|
// every `var(--color-*)` below already resolves per-theme globally (see
|
|
// styles/_theme.scss).
|
|
// =============================================================================
|
|
|
|
@use "../../styles/breakpoints" as bp;
|
|
|
|
// `.app-content` (AppLayout.scss) already stretches to the full viewport
|
|
// height (flex item of `.app-layout`, itself `min-height: 100vh` — the
|
|
// same stretch the sidebar relies on to pin its footer at the bottom).
|
|
// `.planning-page` just needs to fill that box and lay out as a column so
|
|
// `.planning-grid-wrapper` can grow to fill whatever's left under the
|
|
// header, instead of the grid being only as tall as its content.
|
|
.planning-page {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
&__header {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-md);
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
&__status {
|
|
color: var(--color-text-muted);
|
|
font-size: var(--font-size-md);
|
|
}
|
|
|
|
&__status--error {
|
|
color: var(--color-error);
|
|
}
|
|
|
|
// "Commencer à cuisiner" — same solid-primary treatment as the recipe
|
|
// picker's confirm button (features/planning/recipe-picker-dialog.scss).
|
|
&__cook-btn {
|
|
background: var(--color-primary);
|
|
color: var(--color-surface);
|
|
border: none;
|
|
border-radius: var(--radius-base);
|
|
padding: var(--space-sm) var(--space-md);
|
|
font-family: var(--font-body);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
background: var(--color-primary-hover);
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- The grid itself --------------------------------------------------------
|
|
// (Week navigator + calendar popover styles now live in
|
|
// features/planning/week-navigator.scss, imported by WeekNavigator.tsx
|
|
// directly — extracted once ShoppingListPage started reusing that
|
|
// component too.)
|
|
.planning-grid-wrapper {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
background: var(--color-surface);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
table.planning-grid {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-width: 62rem;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
|
|
// Share the wrapper's height evenly across the five meal rows instead of
|
|
// letting each row size to its own content. A `height` on a table row is
|
|
// a minimum the browser then stretches to absorb the table's leftover
|
|
// height; an equal tiny value on every row spreads that leftover evenly,
|
|
// so the grid genuinely fills the screen rather than being only as tall
|
|
// as its busiest column (with dead space under the short ones). The
|
|
// per-cell scroll below is what then keeps a heavy slot from pushing its
|
|
// row past that share.
|
|
tbody tr {
|
|
height: 1px;
|
|
}
|
|
|
|
th,
|
|
td {
|
|
border: 1px solid var(--color-border);
|
|
vertical-align: top;
|
|
}
|
|
|
|
thead th {
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--color-surface-alt);
|
|
text-align: left;
|
|
|
|
&:first-child {
|
|
width: 9rem;
|
|
}
|
|
|
|
.day-name {
|
|
display: block;
|
|
font-size: var(--font-size-xs);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--color-text-muted);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.day-date {
|
|
display: block;
|
|
font-size: var(--font-size-md);
|
|
font-weight: 700;
|
|
margin-top: 2px;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
&.today {
|
|
background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface-alt));
|
|
|
|
.day-date {
|
|
color: var(--color-primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
tbody th {
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--color-surface-alt);
|
|
text-align: center;
|
|
vertical-align: middle;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
}
|
|
|
|
td.today {
|
|
background: color-mix(in srgb, var(--color-primary) 4%, var(--color-surface));
|
|
}
|
|
|
|
// Repas groupés par moment de la journée (Matin / Midi / Après-midi /
|
|
// Soir) — piloté uniquement via `border-bottom` (jamais `border-top`) :
|
|
// avec `border-collapse: collapse`, deux bordures différentes qui se
|
|
// rencontrent sur la même arête peuvent fusionner de façon ambiguë selon
|
|
// le navigateur — en désactivant `border-top` sur tbody, chaque arête
|
|
// horizontale n'est plus définie que d'un seul côté, sans ambiguïté
|
|
// possible. Toutes les séparations entre repas sont pleines (même couleur
|
|
// que les séparations de jour) ; seule la frontière entre deux groupes
|
|
// ("band-end", la dernière ligne d'un groupe) se distingue par une
|
|
// épaisseur plus marquée.
|
|
tbody th,
|
|
tbody td {
|
|
border-top: none;
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
tbody tr.band-end th,
|
|
tbody tr.band-end td {
|
|
border-bottom: 2px solid var(--color-border);
|
|
}
|
|
}
|
|
|
|
// --- Case : pastilles de recette + bouton "+" -------------------------------
|
|
.meal-cell {
|
|
padding: var(--space-sm);
|
|
|
|
// Highlighted while a leftover card is dragged over it (@dnd-kit's
|
|
// `isOver`) — see PlanningPage's `MealCell` droppable.
|
|
&--drop-over {
|
|
background: var(--color-tag);
|
|
outline: 2px dashed var(--color-primary);
|
|
outline-offset: -2px;
|
|
}
|
|
}
|
|
|
|
.meal-cell__content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
height: 100%;
|
|
// The content must not be what defines the row's height: a slot with
|
|
// several recipes would otherwise stretch its `<td>`, and with it the
|
|
// whole row (every other day's cell gaining the same dead height under
|
|
// `vertical-align: top`), then the table past the wrapper into a
|
|
// vertical scroll. `min-height: 0` lets this flex column shrink to its
|
|
// share of the row so `.meal-cell__recipes` below can take over the
|
|
// scrolling.
|
|
min-height: 0;
|
|
}
|
|
|
|
.meal-cell__recipes {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
// A slot only ever holds a handful of recipes in real use; past that the
|
|
// list scrolls inside the cell instead of stretching the `<td>` (and
|
|
// with it the whole row, then the table past `.planning-grid-wrapper`
|
|
// into a vertical scroll — issue #110). A `rem` cap rather than a `%` of
|
|
// the cell: percentage heights against a `<td>` don't resolve
|
|
// consistently across engines. `flex: 1` still lets the list use the
|
|
// row's spare height up to that cap. `.add-recipe-btn` is a flex sibling
|
|
// (outside this box), so it stays pinned just below, always reachable.
|
|
flex: 1;
|
|
min-height: 0;
|
|
max-height: 8.5rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.recipe-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
padding: 0.3rem var(--space-sm);
|
|
border-radius: var(--radius-pill);
|
|
background: var(--color-tag);
|
|
color: var(--color-tag-ink);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
|
|
&__name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
// A "leftovers" chip (`sourceItem !== null`) — served from another
|
|
// slot's batch, nothing cooked for it. Dimmer + a ♻️ marker.
|
|
&--leftover {
|
|
background: var(--color-surface-alt);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
&__leftover-badge {
|
|
margin-right: 0.15rem;
|
|
}
|
|
|
|
&__remove {
|
|
flex-shrink: 0;
|
|
width: 1rem;
|
|
height: 1rem;
|
|
display: grid;
|
|
place-items: center;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
color: inherit;
|
|
opacity: 0;
|
|
font-size: 0.65rem;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
&:hover &__remove {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
&__remove:hover {
|
|
opacity: 1;
|
|
background: rgba(0, 0, 0, 0.15);
|
|
}
|
|
}
|
|
|
|
// Pleine largeur, sans bordure (juste un fond au survol — plus épuré qu'un
|
|
// contour pointillé), et toujours collé en haut de la case (juste sous la
|
|
// dernière recette s'il y en a), jamais centré au milieu d'une case vide.
|
|
.add-recipe-btn {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
padding: 0.35rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
border-radius: var(--radius-base);
|
|
background: none;
|
|
color: var(--color-text-muted);
|
|
font-size: var(--font-size-base);
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
color: var(--color-primary);
|
|
background: color-mix(in srgb, var(--color-primary) 8%, transparent);
|
|
}
|
|
}
|
|
|
|
// --- Vue mobile "un jour à la fois" (issue #88) -----------------------------
|
|
// Remplace entièrement `.planning-grid-wrapper`/`table.planning-grid` sous le
|
|
// breakpoint phone (voir `useIsMobileViewport()` dans PlanningPage.tsx) — pas
|
|
// une variante CSS de la même table, un DOM différent (switcher + liste de
|
|
// cartes), le tableau 7 colonnes n'ayant pas de mise en page lisible en
|
|
// dessous de son `min-width: 62rem`. `.meal-cell`/`.meal-cell__content`/
|
|
// `.recipe-chip`/`.add-recipe-btn` ci-dessus restent utilisés tels quels
|
|
// (voir `mealCellBody` partagé côté TSX) — seul l'habillage `.meal-day-row`
|
|
// autour change.
|
|
.planning-day-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.planning-day-view__switcher {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-sm);
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--color-surface);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.planning-day-view__arrow {
|
|
flex-shrink: 0;
|
|
width: 2.25rem;
|
|
height: 2.25rem;
|
|
display: grid;
|
|
place-items: center;
|
|
border: none;
|
|
border-radius: 50%;
|
|
background: none;
|
|
color: var(--color-text);
|
|
font-size: var(--font-size-lg);
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
|
|
&:hover:not(:disabled) {
|
|
background: var(--color-surface-alt);
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
|
|
.planning-day-view__label {
|
|
flex: 1;
|
|
text-align: center;
|
|
font-size: var(--font-size-md);
|
|
font-weight: 700;
|
|
color: var(--color-text);
|
|
|
|
&.today {
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.today-badge {
|
|
display: inline-block;
|
|
margin-left: var(--space-xs);
|
|
padding: 0.1rem 0.5rem;
|
|
border-radius: var(--radius-pill);
|
|
background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
|
|
color: var(--color-primary);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
vertical-align: middle;
|
|
}
|
|
}
|
|
|
|
.planning-day-view__meals {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-sm);
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0 0 var(--space-md);
|
|
|
|
li.band-end {
|
|
margin-bottom: var(--space-sm);
|
|
}
|
|
}
|
|
|
|
.meal-day-row {
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--color-surface);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
&.today {
|
|
box-shadow: 0 0 0 1px var(--color-primary), var(--shadow-sm);
|
|
}
|
|
|
|
&.meal-cell--drop-over {
|
|
background: var(--color-tag);
|
|
outline: 2px dashed var(--color-primary);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
&__label {
|
|
margin: 0 0 var(--space-xs);
|
|
font-size: var(--font-size-xs);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--color-text-muted);
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
// --- Phone (issue #39, responsive pass) -----------------------------------
|
|
// The grid itself already degrades gracefully on a narrow screen — the
|
|
// wrapper's `overflow: auto` + the table's `min-width: 62rem` mean it just
|
|
// scrolls horizontally rather than crushing seven day-columns into
|
|
// illegibility (same philosophy as the sidebar's own fallback). What does
|
|
// NOT survive the switch to touch are the two hover-gated affordances
|
|
// below.
|
|
@include bp.phone {
|
|
// `.recipe-chip__remove` sits at `opacity: 0` until its chip is hovered
|
|
// — invisible, and untappable, on a touch screen where nothing is ever
|
|
// "hovered". Pin it visible so a planned recipe can actually be removed
|
|
// on a phone. Also give it a real tap target.
|
|
.recipe-chip__remove {
|
|
opacity: 0.7;
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
// A slightly taller "+" so adding to a slot is a comfortable tap, not a
|
|
// 5px sliver.
|
|
.add-recipe-btn {
|
|
padding: var(--space-sm);
|
|
}
|
|
}
|