Web: sidebar avec icônes + repli/déploiement
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é
This commit is contained in:
parent
a8d897dfab
commit
73bfcd8fac
4 changed files with 350 additions and 25 deletions
|
|
@ -15,7 +15,9 @@
|
||||||
|
|
||||||
// Fixed-width nav rail. Its own surface (not the page background), same
|
// 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
|
// elevation language as a card, so it reads as a distinct, permanent piece
|
||||||
// of chrome rather than part of the scrolling content.
|
// 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 {
|
.app-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -24,13 +26,55 @@
|
||||||
padding: var(--space-lg) var(--space-md);
|
padding: var(--space-lg) var(--space-md);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border-right: 1px solid var(--color-border);
|
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 {
|
&__brand {
|
||||||
padding: 0 var(--space-sm) var(--space-xl);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
color: var(--color-primary);
|
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 {
|
&__nav {
|
||||||
|
|
@ -40,12 +84,23 @@
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
padding: var(--space-sm);
|
padding: var(--space-sm);
|
||||||
border-radius: var(--radius-base);
|
border-radius: var(--radius-base);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-surface-alt);
|
background: var(--color-surface-alt);
|
||||||
|
|
@ -87,6 +142,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__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 {
|
&__settings-nav {
|
||||||
margin-top: var(--space-xs);
|
margin-top: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
@ -99,8 +167,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__account-toggle {
|
&__account-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem var(--space-sm);
|
padding: 0.4rem var(--space-sm);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -110,12 +181,27 @@
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-surface-alt);
|
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
|
// Anchored just above the toggle rather than inline in the flow — it's a
|
||||||
// transient overlay, not part of the sidebar's permanent layout.
|
// transient overlay, not part of the sidebar's permanent layout.
|
||||||
&__account-menu {
|
&__account-menu {
|
||||||
|
|
@ -143,12 +229,68 @@
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-surface-alt);
|
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 {
|
.app-content {
|
||||||
|
|
@ -176,10 +318,14 @@
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
|
||||||
&__brand {
|
&__top {
|
||||||
padding: 0 var(--space-sm) 0 0;
|
padding: 0 var(--space-sm) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__brand {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__nav {
|
&__nav {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,20 @@ import { useTranslation } from "react-i18next";
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../features/auth/AuthContext";
|
import { useAuth } from "../features/auth/AuthContext";
|
||||||
import "./AppLayout.scss";
|
import "./AppLayout.scss";
|
||||||
|
import {
|
||||||
|
AccountIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
DietPreferencesIcon,
|
||||||
|
HouseholdIcon,
|
||||||
|
PlanningIcon,
|
||||||
|
RecipesIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
ShoppingListIcon,
|
||||||
|
UserPreferencesIcon,
|
||||||
|
} from "./nav-icons";
|
||||||
|
|
||||||
|
/** `localStorage` key persisting {@link AppLayout}'s collapsed/expanded sidebar state across reloads. */
|
||||||
|
const SIDEBAR_COLLAPSED_KEY = "batchcooking:sidebarCollapsed";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One entry in the sidebar's main nav. `key` maps to `layout.nav.<key>` in
|
* One entry in the sidebar's main nav. `key` maps to `layout.nav.<key>` in
|
||||||
|
|
@ -10,9 +24,9 @@ import "./AppLayout.scss";
|
||||||
* one locale key, no other file to touch.
|
* one locale key, no other file to touch.
|
||||||
*/
|
*/
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ to: "/", key: "planning" },
|
{ to: "/", key: "planning", Icon: PlanningIcon },
|
||||||
{ to: "/recettes", key: "recipes" },
|
{ to: "/recettes", key: "recipes", Icon: RecipesIcon },
|
||||||
{ to: "/liste-de-courses", key: "shoppingList" },
|
{ to: "/liste-de-courses", key: "shoppingList", Icon: ShoppingListIcon },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,10 +34,10 @@ const NAV_ITEMS = [
|
||||||
* maps to `layout.settings.nav.<key>`.
|
* maps to `layout.settings.nav.<key>`.
|
||||||
*/
|
*/
|
||||||
const SETTINGS_ITEMS = [
|
const SETTINGS_ITEMS = [
|
||||||
{ to: "/parametres/compte", key: "account" },
|
{ to: "/parametres/compte", key: "account", Icon: AccountIcon },
|
||||||
{ to: "/parametres/preferences", key: "preferences" },
|
{ to: "/parametres/preferences", key: "preferences", Icon: DietPreferencesIcon },
|
||||||
{ to: "/parametres/foyer", key: "household" },
|
{ to: "/parametres/foyer", key: "household", Icon: HouseholdIcon },
|
||||||
{ to: "/parametres/preferences-utilisateur", key: "userPreferences" },
|
{ to: "/parametres/preferences-utilisateur", key: "userPreferences", Icon: UserPreferencesIcon },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,20 +45,52 @@ const SETTINGS_ITEMS = [
|
||||||
* collapsible "Paramètres" nav, and the account menu at the bottom) plus a
|
* collapsible "Paramètres" nav, and the account menu at the bottom) plus a
|
||||||
* main content area rendering the matched child route via `<Outlet />`.
|
* main content area rendering the matched child route via `<Outlet />`.
|
||||||
*
|
*
|
||||||
|
* The sidebar itself can be collapsed to an icon-only rail (see
|
||||||
|
* `isCollapsed`/`SIDEBAR_COLLAPSED_KEY`) — every nav item keeps its icon
|
||||||
|
* and gains a `title` tooltip, its label just hides via CSS
|
||||||
|
* (`.app-sidebar.collapsed .label`, see AppLayout.scss), so collapsing is a
|
||||||
|
* single class toggle on the `<aside>`, not something each child component
|
||||||
|
* needs to know about.
|
||||||
|
*
|
||||||
* Mounted once as the parent element of the whole authenticated route
|
* Mounted once as the parent element of the whole authenticated route
|
||||||
* group, itself wrapped in {@link RequireAuth} (see `App.tsx`) — `user` is
|
* group, itself wrapped in {@link RequireAuth} (see `App.tsx`) — `user` is
|
||||||
* therefore guaranteed non-null by the time this renders.
|
* therefore guaranteed non-null by the time this renders.
|
||||||
*/
|
*/
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(
|
||||||
|
() => localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "true",
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
setIsCollapsed((collapsed) => {
|
||||||
|
const next = !collapsed;
|
||||||
|
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(next));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-layout">
|
<div className="app-layout">
|
||||||
<aside className="app-sidebar">
|
<aside className={isCollapsed ? "app-sidebar collapsed" : "app-sidebar"}>
|
||||||
<div className="app-sidebar__brand">batchCooking</div>
|
<div className="app-sidebar__top">
|
||||||
|
<div className="app-sidebar__brand">
|
||||||
|
{/* Collapsed: a short monogram instead of the full wordmark — clearer than an arbitrary icon standing in for the brand itself. */}
|
||||||
|
<span className="app-sidebar__brand-full">batchCooking</span>
|
||||||
|
<span className="app-sidebar__brand-mark">bC</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="app-sidebar__collapse-toggle"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
title={t(isCollapsed ? "layout.sidebar.expand" : "layout.sidebar.collapse")}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav className="app-sidebar__nav">
|
<nav className="app-sidebar__nav">
|
||||||
{NAV_ITEMS.map(({ to, key }) => (
|
{NAV_ITEMS.map(({ to, key, Icon }) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
|
|
@ -54,8 +100,10 @@ export function AppLayout() {
|
||||||
// same way `end` would.
|
// same way `end` would.
|
||||||
end={to === "/"}
|
end={to === "/"}
|
||||||
className={({ isActive }) => (isActive ? "active" : undefined)}
|
className={({ isActive }) => (isActive ? "active" : undefined)}
|
||||||
|
title={t(`layout.nav.${key}`)}
|
||||||
>
|
>
|
||||||
{t(`layout.nav.${key}`)}
|
<Icon />
|
||||||
|
<span className="label">{t(`layout.nav.${key}`)}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -74,11 +122,14 @@ export function AppLayout() {
|
||||||
/**
|
/**
|
||||||
* Collapsible "Paramètres" section revealing the settings pages (Compte /
|
* Collapsible "Paramètres" section revealing the settings pages (Compte /
|
||||||
* Préférences alimentaires / Foyer / Préférences utilisateur — see
|
* Préférences alimentaires / Foyer / Préférences utilisateur — see
|
||||||
* `pages/settings/`). Starts open whenever
|
* `pages/settings/`). Starts open whenever the current route is already
|
||||||
* the current route is already under `/parametres`, so following a link
|
* under `/parametres`, so following a link there (e.g. from
|
||||||
* there (e.g. from {@link AccountMenu}) doesn't land on a collapsed menu;
|
* {@link AccountMenu}) doesn't land on a collapsed menu; otherwise starts
|
||||||
* otherwise starts closed to keep the sidebar's main focus on the primary
|
* closed to keep the sidebar's main focus on the primary nav above it.
|
||||||
* nav above it.
|
*
|
||||||
|
* Independent of the whole-sidebar collapse toggle in {@link AppLayout} —
|
||||||
|
* this menu's own open/closed state persists across that (a rail-collapsed
|
||||||
|
* sidebar can still have "Paramètres" expanded, just showing icons).
|
||||||
*/
|
*/
|
||||||
function SettingsMenu() {
|
function SettingsMenu() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -92,20 +143,28 @@ function SettingsMenu() {
|
||||||
className="app-sidebar__settings-toggle"
|
className="app-sidebar__settings-toggle"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
onClick={() => setIsOpen((open) => !open)}
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
|
title={t("layout.settings.toggle")}
|
||||||
>
|
>
|
||||||
{t("layout.settings.toggle")}
|
<span className="app-sidebar__settings-toggle-left">
|
||||||
<span aria-hidden="true">{isOpen ? "▾" : "▸"}</span>
|
<SettingsIcon />
|
||||||
|
<span className="label">{t("layout.settings.toggle")}</span>
|
||||||
|
</span>
|
||||||
|
<span className="chevron" aria-hidden="true">
|
||||||
|
{isOpen ? "▾" : "▸"}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<nav className="app-sidebar__nav app-sidebar__settings-nav">
|
<nav className="app-sidebar__nav app-sidebar__settings-nav">
|
||||||
{SETTINGS_ITEMS.map(({ to, key }) => (
|
{SETTINGS_ITEMS.map(({ to, key, Icon }) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
className={({ isActive }) => (isActive ? "active" : undefined)}
|
className={({ isActive }) => (isActive ? "active" : undefined)}
|
||||||
|
title={t(`layout.settings.nav.${key}`)}
|
||||||
>
|
>
|
||||||
{t(`layout.settings.nav.${key}`)}
|
<Icon />
|
||||||
|
<span className="label">{t(`layout.settings.nav.${key}`)}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -134,6 +193,8 @@ function AccountMenu() {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initial = user?.firstName?.charAt(0).toUpperCase() ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-sidebar__footer">
|
<div className="app-sidebar__footer">
|
||||||
<button
|
<button
|
||||||
|
|
@ -141,8 +202,12 @@ function AccountMenu() {
|
||||||
className="app-sidebar__account-toggle"
|
className="app-sidebar__account-toggle"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
onClick={() => setIsOpen((open) => !open)}
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
|
title={t("layout.greeting", { firstName: user?.firstName })}
|
||||||
>
|
>
|
||||||
{t("layout.greeting", { firstName: user?.firstName })}
|
<span className="app-sidebar__avatar" aria-hidden="true">
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
<span className="label">{t("layout.greeting", { firstName: user?.firstName })}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|
|
||||||
110
apps/web/src/layouts/nav-icons.tsx
Normal file
110
apps/web/src/layouts/nav-icons.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
// Small, hand-drawn line-icon set for the sidebar nav (24×24 viewBox,
|
||||||
|
// matches the reviewed mockup — see the plan/PR description) rather than
|
||||||
|
// pulling in an icon library for a handful of glyphs. Sized entirely via
|
||||||
|
// CSS (`.app-sidebar__nav svg` etc., see AppLayout.scss) — no width/height
|
||||||
|
// attribute here, so the same markup works at any size the caller picks.
|
||||||
|
// `aria-hidden` on every icon: each one is always paired with visible text
|
||||||
|
// (the nav label, or a `title` tooltip when collapsed) that already
|
||||||
|
// conveys the meaning — the icon itself is decorative.
|
||||||
|
|
||||||
|
function Icon({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlanningIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||||
|
<path d="M16 2v4M8 2v4M3 10h18" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecipesIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||||
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShoppingListIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<circle cx="9" cy="21" r="1" />
|
||||||
|
<circle cx="20" cy="21" r="1" />
|
||||||
|
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DietPreferencesIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z" />
|
||||||
|
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HouseholdIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<path d="M9 22V12h6v10" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserPreferencesIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<circle cx="13.5" cy="6.5" r=".5" />
|
||||||
|
<circle cx="17.5" cy="10.5" r=".5" />
|
||||||
|
<circle cx="8.5" cy="7.5" r=".5" />
|
||||||
|
<circle cx="6.5" cy="12.5" r=".5" />
|
||||||
|
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.9 0 1.5-.7 1.5-1.5 0-.4-.2-.8-.4-1.1-.2-.3-.4-.6-.4-1 0-.8.7-1.5 1.5-1.5H16c3.3 0 6-2.7 6-6 0-4.4-4-8-10-8z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChevronLeftIcon() {
|
||||||
|
return (
|
||||||
|
<Icon>
|
||||||
|
<path d="M15 18l-6-6 6-6" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -59,6 +59,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
|
"sidebar": {
|
||||||
|
"collapse": "Replier le menu",
|
||||||
|
"expand": "Déplier le menu"
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"planning": "Planning",
|
"planning": "Planning",
|
||||||
"recipes": "Recettes",
|
"recipes": "Recettes",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue