// ============================================================================= // Styles for AppLayout — the shell (sidebar + content area) wrapping every // authenticated page. Colocated next to AppLayout.tsx since nothing else // uses these classes. // ============================================================================= // No `@use` of the theme partial needed here — see HomePage.scss's identical // note: every token below is a CSS custom property, available at runtime. // `height` (not `min-height`): the layout must stay pinned to exactly the // viewport, never grow taller than it — otherwise a tall page scrolls the // whole document, dragging the sidebar out of view with it. `overflow: // hidden` backs that up so nothing can force the document itself to grow; // `.app-content` below is the one place that actually scrolls. .app-layout { height: 100vh; overflow: hidden; display: flex; background: var(--color-background); } // Fixed-width nav rail. Its own surface (not the page background), same // elevation language as a card, so it reads as a distinct, permanent piece // of chrome rather than part of the scrolling content. `width` transitions // so collapsing/expanding (see `.collapsed` below) reads as a deliberate // motion rather than an abrupt layout jump. .app-sidebar { display: flex; flex-direction: column; flex-shrink: 0; width: 15rem; padding: var(--space-lg) var(--space-md); background: var(--color-surface); border-right: 1px solid var(--color-border); transition: width 0.15s ease; // Own scroll, independent of `.app-content` — only matters on a very // short viewport with a long nav, but keeps the rail from ever forcing // `.app-layout` past 100vh. overflow-y: auto; &__top { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--space-sm) var(--space-xl); } &__brand { display: flex; align-items: center; font-family: var(--font-display); font-weight: 700; font-size: var(--font-size-lg); color: var(--color-primary); overflow: hidden; white-space: nowrap; } // Full wordmark by default; swapped for a short monogram when collapsed // (see `.collapsed &__brand-full`/`&__brand-mark` below) — clearer than // an arbitrary icon standing in for the brand itself. &__brand-mark { display: none; } &__collapse-toggle { width: 1.75rem; height: 1.75rem; flex-shrink: 0; display: grid; place-items: center; border: 1px solid var(--color-border); border-radius: var(--radius-base); background: var(--color-surface); color: var(--color-text-muted); cursor: pointer; svg { width: 1rem; height: 1rem; transition: transform 0.15s ease; } &:hover { background: var(--color-surface-alt); color: var(--color-text); } } &__nav { display: flex; flex: 1; flex-direction: column; gap: var(--space-xs); a { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm); border-radius: var(--radius-base); color: var(--color-text); font-weight: 600; font-size: var(--font-size-sm); text-decoration: none; white-space: nowrap; overflow: hidden; svg { flex-shrink: 0; width: 1.25rem; height: 1.25rem; } &:hover { background: var(--color-surface-alt); } // Applied by NavLink itself (see the `className` prop in // AppLayout.tsx), not a router-provided class — plain `.active`. &.active { background: var(--color-primary); color: #fff; } } } // --- "Paramètres" collapsible menu ------------------------------------- &__settings { padding-top: var(--space-md); border-top: 1px solid var(--color-border); } &__settings-toggle { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--space-sm); font-family: var(--font-body); font-size: var(--font-size-sm); font-weight: 600; text-align: left; color: var(--color-text); background: none; border: none; border-radius: var(--radius-base); cursor: pointer; &:hover { background: var(--color-surface-alt); } } &__settings-toggle-left { display: flex; align-items: center; gap: var(--space-sm); overflow: hidden; svg { flex-shrink: 0; width: 1.25rem; height: 1.25rem; } } &__settings-nav { margin-top: var(--space-xs); } // --- Account menu (bottom of the sidebar) ------------------------------- &__footer { position: relative; padding-top: var(--space-md); border-top: 1px solid var(--color-border); } &__account-toggle { display: flex; align-items: center; gap: var(--space-sm); width: 100%; padding: 0.4rem var(--space-sm); font-family: var(--font-body); font-size: var(--font-size-sm); font-weight: 600; text-align: left; cursor: pointer; border-radius: var(--radius-base); border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-text); overflow: hidden; white-space: nowrap; &:hover { background: var(--color-surface-alt); } } &__avatar { flex-shrink: 0; width: 1.5rem; height: 1.5rem; display: grid; place-items: center; border-radius: 50%; background: var(--color-primary); color: #fff; font-size: var(--font-size-xs); font-weight: 700; } // Anchored just above the toggle rather than inline in the flow — it's a // transient overlay, not part of the sidebar's permanent layout. &__account-menu { position: absolute; bottom: calc(100% + var(--space-xs)); left: 0; right: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-base); box-shadow: var(--shadow-md); a, button { padding: var(--space-sm); font-family: var(--font-body); font-size: var(--font-size-sm); font-weight: 600; text-align: left; text-decoration: none; color: var(--color-text); background: none; border: none; cursor: pointer; white-space: nowrap; &:hover { background: var(--color-surface-alt); } } } // App version — a quiet diagnostic footnote below the account menu, not // an interactive element (hence `aria-hidden` on the `

` in // AppLayout.tsx). Hidden whenever space is at a premium — the icon-only // rail and the mobile horizontal bar (see the `.collapsed` block and the // `@media (max-width: 640px)` block below). &__version { margin: var(--space-xs) 0 0; padding: 0 var(--space-sm); font-size: var(--font-size-xs); color: var(--color-text-muted); text-align: center; } // --- Collapsed (icon-only rail) state ----------------------------------- // A single class toggle on the root element — every nested rule below // just hides labels/chevrons and re-centers icons via CSS, no child // component needs to know the sidebar is collapsed. // // Scoped to `min-width: 641px` (the exact complement of the mobile // `@media (max-width: 640px)` block below): `isCollapsed` persists in // `localStorage` (`batchcooking:sidebarCollapsed`) independently of // viewport width, so a user who collapsed the desktop rail and then // shrinks the window (or opens the same session on a phone) still carries // that flag. Without this guard, `.app-sidebar.collapsed`'s `width: // 4.25rem`/`flex-direction: column` (specificity 0,2,0) beat the mobile // block's `.app-sidebar` rules (specificity 0,1,0) and crushed the // full-width horizontal bar into a tiny broken rail — see issue #27. @media (min-width: 641px) { &.collapsed { width: 4.25rem; padding-left: var(--space-sm); padding-right: var(--space-sm); .app-sidebar__top { flex-direction: column; gap: var(--space-sm); padding-left: 0; padding-right: 0; } .app-sidebar__brand-full { display: none; } .app-sidebar__brand-mark { display: block; } .app-sidebar__collapse-toggle svg { transform: rotate(180deg); } .app-sidebar__nav a, .app-sidebar__settings-toggle, .app-sidebar__account-toggle { justify-content: center; padding-left: 0; padding-right: 0; } .app-sidebar__settings-toggle-left { gap: 0; } .label, .chevron { display: none; } .app-sidebar__version { display: none; } // The popover would otherwise shrink to the icon rail's own width, // squashing "Mon compte"/"Se déconnecter" — give it a normal, // comfortable width instead, still anchored to the rail's left edge. .app-sidebar__account-menu { left: 0; right: auto; width: 12rem; } } } } .app-content { flex: 1; // Content can scroll independently of the sidebar (e.g. a long planning // table) without the fixed-width rail ever needing to shrink. min-width: 0; // The actual scroll container: `.app-layout` is pinned to `height: 100vh` // above, so a page taller than the viewport scrolls in here instead of // scrolling the document (which would drag the sidebar along with it). min-height: 0; overflow-y: auto; padding: var(--space-xl); } // Below this width the fixed-width rail would eat too much of the // viewport (relevant early given this app is meant to be embedded via // Capacitor later, see the root README) — collapse it into a horizontal // top bar instead of a side rail. // // The primary nav (Planning/Recettes/Liste de courses) must stay fully // legible and tappable at any width — it's the app's main navigation, not // optional chrome. Without `&__nav { min-width: 0 }` + `a { flex-shrink: 0 // }` below, `&__settings`/`&__footer`'s own natural (non-shrinking) width // silently crushed it down to ~16px unlabeled slivers on a narrow phone // (measured on a 375px viewport) — invisible labels, no real tap target. // The fix: `__settings`/`__footer` collapse to icon-only instead (same // look as the desktop rail's `.collapsed` state), freeing width for the // nav, which falls back to horizontal scroll (`overflow-x: auto`) rather // than shrinking if it still doesn't fit. @media (max-width: 640px) { .app-layout { flex-direction: column; } // The desktop `var(--space-xl)` (32px) all around eats a big share of a // ~375px screen — every page then has ~64px less usable width than the // viewport already gives it. Drop to `--space-md` (16px) on phones; the // grid/table pages that still overflow fall back to their own horizontal // scroll (same philosophy as the sidebar below). .app-content { padding: var(--space-md); } .app-sidebar { flex-direction: row; align-items: center; width: 100%; padding: var(--space-sm) var(--space-md); border-right: none; border-bottom: 1px solid var(--color-border); &__top { padding: 0 var(--space-sm) 0 0; } &__brand { padding: 0; } // Nothing to collapse into on a horizontal bar — there's no rail. &__collapse-toggle { display: none; } // No room for this on a horizontal bar either. &__version { display: none; } &__nav { flex: 1 1 auto; min-width: 0; flex-direction: row; overflow-x: auto; a { flex-shrink: 0; } } &__settings, &__footer { position: relative; flex: none; padding-top: 0; border-top: none; } &__settings-toggle, &__account-toggle { padding-left: var(--space-xs); padding-right: var(--space-xs); } &__settings-toggle-left .label, &__account-toggle .label, &__settings-toggle .chevron { display: none; } // Both reveals become floating panels anchored under their icon-only // toggle — on the desktop rail one is inline (settings, stacks fine in // a column) and the other already floats (account); neither can stay // in normal flow on this horizontal bar without breaking the row. // Selector order matters: `&__settings-nav` also carries the plain // `&__nav` class (for the link styling), so this must come after it to // win on `flex-direction`/`overflow-x`. &__settings-nav { position: absolute; top: calc(100% + var(--space-xs)); right: 0; flex-direction: column; width: 14rem; padding: var(--space-xs); overflow-x: visible; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-base); box-shadow: var(--shadow-md); z-index: 10; } // Desktop opens this *upward* (`bottom: calc(100% + ...)`, see the base // rule above) because the footer sits at the bottom of a tall rail — // on this horizontal top bar the footer is near `y: 0`, so "upward" // pushed the menu entirely off-screen above the viewport. Flip it to // open downward here instead. &__account-menu { top: calc(100% + var(--space-xs)); bottom: auto; left: auto; right: 0; width: 12rem; } } }