Design validé via une maquette HTML itérée avec l'utilisateur (voir historique de conversation) avant implémentation. - nav-icons.tsx: jeu d'icônes trait fin (24x24, inline SVG) pour les 3 items de nav principaux, les 4 pages de Paramètres et le chevron de repli — pas de librairie d'icônes pour une poignée de glyphes - AppLayout: bouton replier/déplier à côté du logo — la sidebar passe de 15rem à un rail 4.25rem icônes-seules ; un seul toggle de classe CSS sur le <aside> pilote tout (aucun sous-composant n'a besoin de savoir que la sidebar est repliée), état persisté en localStorage - Logo : monogramme "bC" en repli plutôt qu'une icône arbitraire à la place du nom complet - Avatar rond (initiale) dans le menu compte, popover recalée en largeur fixe quand la sidebar est repliée (sinon écrasée à 4.25rem) - title="" sur chaque item pour l'infobulle native une fois replié
340 lines
8.1 KiB
SCSS
340 lines
8.1 KiB
SCSS
// =============================================================================
|
|
// 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.
|
|
|
|
.app-layout {
|
|
min-height: 100vh;
|
|
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;
|
|
|
|
&__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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 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.
|
|
&.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;
|
|
}
|
|
|
|
// 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;
|
|
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.
|
|
@media (max-width: 640px) {
|
|
.app-layout {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
&__nav {
|
|
flex-direction: row;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
&__settings,
|
|
&__footer {
|
|
padding-top: 0;
|
|
border-top: none;
|
|
}
|
|
}
|
|
}
|