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 ? ( - Logo - ) : ( - <> - Logo - - 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) - - {/* Table du gem */} - - {/* Facettes hautes */} - - + {/* Facette haut-gauche — la plus claire (côté lumière). */} + + {/* Facette haut-droite */} + + {/* Facette bas-gauche */} + + {/* Facette bas-droite — la plus sombre (côté ombre). */} + + {/* Contour propre — épaisseur fixe pour rester lisible à 14px. */} + ); } 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 ( -