// ============================================================================= // 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 ``, 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 `` (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 `` 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); } }