diff --git a/apps/web/src/assets/logo.png b/apps/web/src/assets/logo.png
deleted file mode 100644
index fcd78de..0000000
Binary files a/apps/web/src/assets/logo.png and /dev/null differ
diff --git a/apps/web/src/components/brand/Brand.tsx b/apps/web/src/components/brand/Brand.tsx
index 2079006..e360a35 100644
--- a/apps/web/src/components/brand/Brand.tsx
+++ b/apps/web/src/components/brand/Brand.tsx
@@ -1,22 +1,48 @@
-
import { cn } from "@/lib/utils";
-import logo from "@/assets/logo.png";
+import { Gem } from "./Gem";
+
/**
* Lockup horizontal : ◆ + "Rubis" (+ optionnel "sur l'ongle" en suffixe italique muted).
* À utiliser dans les headers, le sidebar, les emails.
*
+ * Architecture : on s'appuie sur le composant SVG — pas de PNG, pas
+ * de border externe. La pierre EST le logo. Plus rien ne casse à l'export
+ * en taille variable, et le rendu reste identique du SPA aux PDFs aux
+ * favicons (si on génère).
+ *
* Cf. /docs/marque.md §2 et le pattern de la landing.
*/
type BrandProps = {
/** Affiche le suffixe "sur l'ongle" en italique muted. */
withSuffix?: boolean;
- /** Taille du gem (le wordmark s'aligne dessus). */
+ /**
+ * Taille de la gem en pixels. Default 22 (lockup) ou 32 (onlyImage).
+ * Pour les usages héros : passer 56-72.
+ */
gemSize?: number;
- className?: string;
+ /** Mode "logo seul" : que la gem, sans le wordmark. */
onlyImage?: boolean;
+ className?: string;
};
-export function Brand({ withSuffix = false, onlyImage = false, className }: BrandProps) {
+export function Brand({
+ withSuffix = false,
+ onlyImage = false,
+ gemSize,
+ className,
+}: BrandProps) {
+ const resolvedSize = gemSize ?? (onlyImage ? 32 : 22);
+
+ if (onlyImage) {
+ return (
+
+ );
+ }
+
return (
- {onlyImage ? (
-
- ) : (
- <>
-
-
- Rubis
- {withSuffix && (
-
- sur l'ongle
-
+
+
+ Rubis
+ {withSuffix && (
+
+ sur l'ongle
- >
- )}
+ )}
+
);
}
diff --git a/apps/web/src/components/brand/Gem.tsx b/apps/web/src/components/brand/Gem.tsx
index 565a3fd..34a310d 100644
--- a/apps/web/src/components/brand/Gem.tsx
+++ b/apps/web/src/components/brand/Gem.tsx
@@ -2,17 +2,25 @@ import { cn } from "@/lib/utils";
/**
* Le ◆ — gem facetté, signature de la marque.
- * SVG inline (pas une icône Lucide, jamais).
*
- * 4 facettes suggérées + ligne médiane "table" du gem.
- * Couleur : `currentColor` — hérite du contexte. Default text-rubis.
+ * Construction : losange divisé en 4 facettes triangulaires qui se rencontrent
+ * au centre. Chacune a une opacité différente pour suggérer le jeu de lumière
+ * d'une pierre taillée, sans gradient (qui rend pâteux à petite taille).
+ *
+ * - facette haut-gauche : 100% (lumière)
+ * - facette haut-droite : 80%
+ * - facette bas-gauche : 65%
+ * - facette bas-droite : 48% (zone d'ombre)
+ *
+ * Plus un fin contour qui définit le pourtour. Le tout en `currentColor` —
+ * hérite du parent (default rubis).
*
* Cf. /docs/marque.md §2 (logo direction A) et §5 (icônes spéciales).
*/
type GemProps = {
/** Taille en pixels (carré). Default 22. */
size?: number;
- /** Si true, applique un drop-shadow doux rubis pour les héros. */
+ /** Drop-shadow doux rubis — pour les usages héros uniquement. */
glow?: boolean;
className?: string;
"aria-label"?: string;
@@ -23,7 +31,7 @@ export function Gem({ size = 22, glow = false, className, ...props }: GemProps)
);
}
diff --git a/apps/web/src/components/dashboard/RubisHero.tsx b/apps/web/src/components/dashboard/RubisHero.tsx
index 579499c..485adf0 100644
--- a/apps/web/src/components/dashboard/RubisHero.tsx
+++ b/apps/web/src/components/dashboard/RubisHero.tsx
@@ -46,7 +46,7 @@ export function RubisHero({
-
+
diff --git a/apps/web/src/components/layout/AppSidebar.tsx b/apps/web/src/components/layout/AppSidebar.tsx
index b7ca64a..007ca44 100644
--- a/apps/web/src/components/layout/AppSidebar.tsx
+++ b/apps/web/src/components/layout/AppSidebar.tsx
@@ -1,5 +1,9 @@
+import { useEffect, useState } from "react";
import { Link } from "@tanstack/react-router";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
+ ChevronsLeft,
+ ChevronsRight,
LayoutDashboard,
FileText,
ListChecks,
@@ -12,51 +16,183 @@ import { Brand } from "@/components/brand/Brand";
import { Gem } from "@/components/brand/Gem";
import { useAuth } from "@/lib/auth";
import { formatRubisToHours } from "@/lib/format";
+import { cn } from "@/lib/utils";
import { NavLink } from "./NavLink";
+const STORAGE_KEY = "rubis.sidebar.collapsed";
+
/**
- * Sidebar desktop — 240px wide, sticky.
- * - Brand en haut
- * - Nav verticale au centre
- * - Compteur rubis en bas (gratification permanente, cf. wireframe 4.1)
+ * Sidebar desktop — repliable.
*
- * Le compteur rubis dans le sidebar n'est pas négociable : c'est ce qui
- * fait dire au user "putain j'ai gagné 24h ce mois".
+ * - Déployée (240px) : brand + nav avec labels + compteur rubis en bas
+ * - Repliée (64px) : juste les icônes (tooltip au hover) + gem du brand
+ *
+ * Le choix est persisté en localStorage (`rubis.sidebar.collapsed`) pour
+ * survivre aux reloads. Toggle via un petit bouton chevron en bas.
+ *
+ * Le compteur rubis disparaît en mode replié (l'écran est trop étroit pour
+ * en faire quelque chose de lisible) — on le retrouve en dépliant. La gem
+ * en haut reste, donc l'identité de marque ne disparaît jamais.
*/
export function AppSidebar({ rubisThisMonth = 0 }: { rubisThisMonth?: number }) {
const { user: _user } = useAuth();
+ const [collapsed, setCollapsed] = useState(false);
+
+ // Lecture localStorage à l'init (côté client uniquement).
+ useEffect(() => {
+ if (typeof window === "undefined") return;
+ setCollapsed(window.localStorage.getItem(STORAGE_KEY) === "1");
+ }, []);
+
+ const toggle = () => {
+ setCollapsed((prev) => {
+ const next = !prev;
+ if (typeof window !== "undefined") {
+ window.localStorage.setItem(STORAGE_KEY, next ? "1" : "0");
+ }
+ return next;
+ });
+ };
return (
-