diff --git a/app/components/Footer.jsx b/app/components/Footer.jsx index f7e2cc3..dabfa4e 100644 --- a/app/components/Footer.jsx +++ b/app/components/Footer.jsx @@ -1,19 +1,25 @@ -"use client" +"use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export default function Footer() { - const [count, setCount] = useState(0); + const [visitCount, setVisitCount] = useState(0); - function handleClick() { - setCount(count + 1); - } + useEffect(() => { + const visits = localStorage.getItem("visitCount"); + const newVisitCount = visits ? parseInt(visits, 10) + 1 : 1; + localStorage.setItem("visitCount", newVisitCount.toString()); + setVisitCount(newVisitCount); + }, []); return ( ); -} \ No newline at end of file +} diff --git a/app/components/ModalGlossaire.tsx b/app/components/ModalGlossaire.tsx index 6ab7a82..9e9e053 100644 --- a/app/components/ModalGlossaire.tsx +++ b/app/components/ModalGlossaire.tsx @@ -1,68 +1,100 @@ -import { useEffect } from "react"; -import { createPortal } from "react-dom"; -import CarouselCompetences from "./CarouselCompetences"; -import { getApiUrl } from "../utils/getApiUrl"; - -interface ImageData { - url: string; - name?: string; - formats?: { - large?: { - url: string; - }; - }; -} - -interface GlossaireMot { - mot_clef: string; - description: string; - images?: ImageData[]; -} - -interface ModalGlossaireProps { - mot: GlossaireMot; - onClose: () => void; -} - -export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) { - const apiUrl = getApiUrl(); - - useEffect(() => { - document.body.classList.add("overflow-hidden"); - return () => { - document.body.classList.remove("overflow-hidden"); - }; - }, []); - - const images = mot.images?.map((img) => ({ - url: `${apiUrl}${img.formats?.large?.url || img.url}`, - alt: img.name || "Illustration", - })) || []; - - return createPortal( -
-
- - -
- {/* Description */} -
-

{mot.mot_clef}

-

{mot.description}

-
- -
- {images.length > 0 ? ( - - ) : ( -

Aucune image disponible.

- )} -
-
-
-
, - document.body - ); -} \ No newline at end of file +"use client"; + +import { useEffect } from "react"; +import { createPortal } from "react-dom"; +import CarouselCompetences from "./CarouselCompetences"; +import { getApiUrl } from "../utils/getApiUrl"; + +interface ImageData { + url: string; + name?: string; + formats?: { + large?: { + url: string; + }; + }; +} + +interface GlossaireMot { + mot_clef: string; + description: string; + images?: ImageData[]; +} + +interface ModalGlossaireProps { + mot: GlossaireMot; + onClose: () => void; +} + +export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) { + const apiUrl = getApiUrl(); + + // Verrouille le scroll du body + ferme sur Esc. + useEffect(() => { + document.body.classList.add("overflow-hidden"); + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", handleKey); + return () => { + document.body.classList.remove("overflow-hidden"); + window.removeEventListener("keydown", handleKey); + }; + }, [onClose]); + + const images = + mot.images?.map((img) => ({ + url: `${apiUrl}${img.formats?.large?.url || img.url}`, + alt: img.name || "Illustration", + })) || []; + + return createPortal( +
+ {/* Carte interne : largeur fluide avec marge, hauteur capée, scroll interne si besoin. + stopPropagation pour ne pas fermer la modale quand on interagit à l'intérieur. */} +
e.stopPropagation()} + > + + +
+
+

+ {mot.mot_clef} +

+

+ {mot.description} +

+
+ +
+ {images.length > 0 ? ( + + ) : ( +

+ Aucune image disponible. +

+ )} +
+
+
+
, + document.body + ); +} diff --git a/app/components/NavLink.jsx b/app/components/NavLink.jsx index 19ccd2f..4219d6b 100644 --- a/app/components/NavLink.jsx +++ b/app/components/NavLink.jsx @@ -1,18 +1,47 @@ -"use client" +"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; +/** + * NavLink — lien de navigation avec état actif. + * + * Props : + * - text (string) : libellé affiché. + * - path (string) : URL cible. + * - onClick (fn, optionnel) : handler (ex. fermeture du drawer mobile). + * - className (string, optionnel) : classes utilitaires communes (fond, padding…). + * - activeClassName (string, optionnel) : classes appliquées quand la route courante == path. + * - inactiveClassName (string, optionnel) : classes appliquées quand la route courante != path. + * + * Sans activeClassName / inactiveClassName, on retombe sur le comportement + * historique `opacity-100 / opacity-50 hover:opacity-65` pour ne rien casser + * sur les NavLink du header desktop qui n'en fournissent pas. + */ export default function NavLink(props) { + const { + text, + path, + onClick, + className = "", + activeClassName, + inactiveClassName, + } = props; + const pathname = usePathname(); - const active = pathname === props.path; + const active = pathname === path; + + const stateClass = active + ? activeClassName ?? "opacity-100" + : inactiveClassName ?? "opacity-50 hover:opacity-65"; return ( - {props.text} {/* Texte du lien */} + {text} ); -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index ac4bf32..05bd106 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,14 +1,12 @@ - "use client"; -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Footer from "./components/Footer"; import "./assets/main.css"; import "./globals.css"; import NavLink from "./components/NavLink"; export default function RootLayout({ children }: { children: React.ReactNode }) { - const [visitCount, setVisitCount] = useState(0); const [isMenuOpen, setIsMenuOpen] = useState(false); const menuRef = useRef(null); const burgerRef = useRef(null); @@ -28,17 +26,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) closeTimerRef.current = setTimeout(() => setIsMenuOpen(false), AUTO_CLOSE_MS); }; - const openMenu = () => setIsMenuOpen(true); const closeMenu = () => setIsMenuOpen(false); const toggleMenu = () => setIsMenuOpen((v) => !v); - useEffect(() => { - const visits = localStorage.getItem("visitCount"); - const newVisitCount = visits ? parseInt(visits) + 1 : 1; - localStorage.setItem("visitCount", newVisitCount.toString()); - setVisitCount(newVisitCount); - }, []); - useEffect(() => { if (!isMenuOpen) { clearAutoClose(); @@ -70,86 +60,146 @@ export default function RootLayout({ children }: { children: React.ReactNode }) } }, [isMenuOpen]); + // Classes communes pour les liens du drawer mobile : fond container primaire, + // radius "tile", hover qui inverse vers fond clair + texte primaire (palette Stitch). + const drawerLinkClass = + "block px-4 py-2 rounded-tile bg-primary-container/60 text-white transition-colors duration-200 hover:bg-primary-fixed hover:text-primary"; + const drawerLinkActive = "bg-primary-fixed text-primary"; + + // NavLink desktop : état actif souligné, inactif discret (palette Stitch). + const desktopLinkActive = "text-primary border-b-2 border-primary-fixed pb-0.5"; + const desktopLinkInactive = + "text-on-surface-variant hover:text-primary transition-colors"; + return (
- {/* Conserve le fond en plein écran */} + {/* Wallpaper plein écran (fondation, ne change pas avec la refonte). */}
- {/* Contenu centré avec largeur contrôlée */} -
- - {/* Cercles animés */} -
-
-
+ {/* Cercles animés : repalette en ton indigo-ardoise (Stitch "Digital Atelier"). */} +
+
+
- {/* Header */} -
+ {/* Header "No-Line" : pas de bordure pleine, juste un shift tonal + ombre ambient diffuse. */} +
-

+

Portfolio Gras-Calvet Fernand

- {/* Bouton menu burger */} + {/* Burger ghost (Material Symbols) : plus sobre, couleur primaire, hover tonal. */} - {/* Menu desktop */} + {/* Menu desktop : NavLink avec états actif/inactif éditoriaux. */}
- {/* Drawer mobile (tiroir gauche, 70%, fond sombre translucide) */} + {/* Drawer mobile (tiroir gauche, 70 %, fond primaire translucide). */}
- {/* Voile : tap pour fermer */} @@ -160,13 +210,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
- -
- NV : {visitCount} -
); } - diff --git a/docs-site-interne/REFONTE-VISUELLE.md b/docs-site-interne/REFONTE-VISUELLE.md index 7145742..f79abf8 100644 --- a/docs-site-interne/REFONTE-VISUELLE.md +++ b/docs-site-interne/REFONTE-VISUELLE.md @@ -1,7 +1,7 @@ # Refonte visuelle — Direction "Digital Atelier" **Créé :** 2026-04-22 -**Statut :** en cours (étapes 1-3/8 terminées) +**Statut :** en cours (étapes 1-4/8 terminées) **Source d'inspiration :** `stitch_V1/` (design newsletter Stitch — `DESIGN.md` et `code.html`). **Audit préalable :** [`captures/AUDIT-VISUEL.md`](./captures/AUDIT-VISUEL.md). @@ -57,7 +57,7 @@ Chaque étape = un lot cohérent + éventuelle mise à jour de `captures/AUDIT-V | 1 | Fondations : tokens Tailwind + import polices + icônes | `tailwind.config.ts`, `app/globals.css` | **fait** (2026-04-22) | | 2 | Garde-fou doc + mise à jour feuille de route | `docs-site-interne/REFONTE-VISUELLE.md`, `docs-site-interne/feuille-de-route.md` | **fait** (2026-04-22) | | 3 | Migration typographique globale (Orbitron → Manrope / Newsreader) | `app/**/*.{tsx,jsx,js}`, `app/assets/main.css` | **fait** (2026-04-22) | -| 4 | Layout racine : header No-Line, burger ghost, palette cercles, compteur migré, drawer | `app/layout.tsx`, `app/globals.css` | à faire | +| 4 | Layout racine : header No-Line, burger ghost, palette cercles, compteur migré, drawer | `app/layout.tsx`, `app/components/NavLink.jsx`, `app/components/Footer.jsx` | **fait** (2026-04-22) | | 5 | Home : hero vellum, portrait frame, takeaways, pull-quote, CTAs | `app/page.tsx` | à faire | | 6 | Listes portfolio + compétences : grille asymétrique, cartes éditoriales | `app/portfolio/page.jsx`, `app/competences/page.jsx`, composants `Carousel*` | à faire | | 7 | Fiches détail + modale glossaire + GrasBot (jewel flottant) | `app/portfolio/[slug]/page.tsx`, `app/competences/[slug]/page.tsx`, `app/components/ModalGlossaire.tsx`, `app/components/ChatBot.js` | à faire | @@ -78,6 +78,30 @@ Après l'étape 3, retour utilisateur : **couleurs de texte différentes** entre **Leçon retenue (à appliquer aux étapes suivantes)** : quand on supprime un "masque" CSS (comme la couleur forcée d'Orbitron), toujours vérifier que la valeur qui va ré-émerger par héritage est bien la valeur attendue, pas une variable dépendante du contexte d'exécution. +## 4 ter. Correctif urgent modale glossaire (2026-04-22) — blocage mobile + +Après l'étape 4, retour utilisateur sur Samsung S25 Ultra : les mots-clés du glossaire (compétences) ouvrent bien la modale mais **la modale déborde de l'écran**, **la croix de fermeture est hors champ**, impossible de refermer sans recharger la page. + +**Causes identifiées** dans `app/components/ModalGlossaire.tsx` (pré-existantes avant la refonte) : + +- Carte interne en `w-[114vw] max-w-6xl` : force une largeur > viewport sur mobile (114 % de 400 px = 456 px dans une fenêtre de 400 px), et sur desktop la contrainte est masquée par `max-w-6xl`. +- Hauteur figée `h-[72vh]` sans scroll interne : le contenu est simplement tronqué quand il dépasse. +- Aucune fermeture au tap sur le voile, ni à Esc. Seule issue = bouton `✖` en haut à droite, hors champ sur mobile. +- Bouton de fermeture en `text-sm p-1` : zone tactile < 44 px, sous le seuil Material Design pour le tactile. + +**Fix** (anticipe les besoins de l'étape 7) : + +- Carte interne : `w-full max-w-4xl max-h-[90vh]` + padding 4 sur le voile pour la marge latérale sur mobile. +- Contenu intérieur en `overflow-y-auto` pour scroll interne si nécessaire. +- Voile cliquable pour fermer, `stopPropagation` sur la carte pour ne pas fermer en interagissant avec. +- Fermeture `Escape` via `keydown` global. +- Bouton de fermeture rond `h-10 w-10` avec Material Symbol `close`, focus-visible, position `absolute top-3 right-3`. +- Alignement palette Stitch : voile `bg-on-surface/75 backdrop-blur-sm`, carte `bg-surface-container-lowest/95 backdrop-blur-vellum shadow-ambient rounded-sheet`, titre `text-primary`, description en `font-body` serif (Newsreader) pour lisibilité, texte `text-on-surface-variant`. +- Ajout de `"use client"` (manquant). +- `role="dialog" aria-modal="true" aria-label={...}` sur le conteneur, `aria-label` explicite sur le bouton de fermeture. + +Ce correctif concerne uniquement le composant `ModalGlossaire`. L'étape 7 reprendra la refonte globale de cette zone (cohérence visuelle avec les fiches détail) mais le blocage UX mobile est levé dès maintenant. + ## 5. Checklist relecture (à passer à la fin de chaque étape) - [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter). diff --git a/docs-site-interne/feuille-de-route.md b/docs-site-interne/feuille-de-route.md index b76755f..6ab9137 100644 --- a/docs-site-interne/feuille-de-route.md +++ b/docs-site-interne/feuille-de-route.md @@ -8,7 +8,7 @@ Document vivant : ajuster les statuts et dates au fil du travail. | ID | Sujet | Statut | Notes | |----|--------|--------|--------| -| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-3 (tokens + garde-fou + migration typo globale) faites le 2026-04-22. | +| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-4 (tokens + garde-fou + migration typo globale + layout racine) faites le 2026-04-22. | | R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` | | R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent | | R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Évaluer extraction header/footer | @@ -40,3 +40,5 @@ Document vivant : ajuster les statuts et dates au fil du travail. | 2026-04-22 | Refonte visuelle "Digital Atelier" — étape 1 (tokens Tailwind : palette Stitch, `font-headline` Manrope + `font-body` Newsreader, `rounded-sheet/tile`, `shadow-ambient/jewel`) et étape 2 (garde-fou + plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md)). | | 2026-04-22 | Refonte visuelle — étape 3 : migration typographique globale. Toutes les classes `font-orbitron-*` (12 définitions CSS, 29 occurrences dans 11 fichiers) remplacées par `font-headline` Manrope + tailles/poids Tailwind explicites. Import Google Fonts Orbitron supprimé de `app/assets/main.css`. | | 2026-04-22 | Refonte visuelle — correctif post-étape 3 : régression de couleurs texte entre desktop/mobile. Retrait du `@media (prefers-color-scheme: dark)` hérité du template Next (incohérent avec l'arbitrage "light-only"), `--foreground` fixé à `#191c1d` (on-surface Stitch), `body` avec couleur non-dépendante du thème système. 3 classes Tailwind invalides `text-black-500/700` remplacées par `text-gray-700` (`app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`). | +| 2026-04-22 | Refonte visuelle — étape 4 : layout racine. Header "No-Line" (bordure pleine supprimée, `shadow-ambient-sm` + `backdrop-blur-vellum`, titre en `text-primary`). Burger refait en ghost button (Material Symbols `menu`/`close` au lieu des caractères `☰`/`✕`). Cercles animés repeints en `bg-primary/40` + `bg-primary-container/30`. Drawer mobile en `bg-primary/90 backdrop-blur-vellum` + liens éditoriaux (`bg-primary-container/60` → hover `bg-primary-fixed text-primary`). Bug préexistant **corrigé** : `NavLink` ignorait `className` et `onClick` fournis par le drawer mobile → refait avec support `className` / `onClick` / `activeClassName` / `inactiveClassName`, comportement desktop historique préservé. Compteur de visites migré de `layout.tsx` (bloc orphelin `absolute bottom-0 right-0`) vers `Footer.jsx` (ligne discrète `text-[10px] uppercase tracking-[0.3em]`). Nettoyage : state `visitCount` + useEffect déplacés, `div.max-w-5xl` vide retirée, state `count` inutilisé retiré de `Footer.jsx`. | +| 2026-04-22 | Refonte visuelle — correctif urgent `ModalGlossaire` (blocage mobile signalé sur Samsung S25 Ultra). `w-[114vw] max-w-6xl h-[72vh]` → `w-full max-w-4xl max-h-[90vh]` + `overflow-y-auto`. Fermeture ajoutée sur tap-voile et `Escape`. Bouton fermeture rond 40 px Material Symbol `close`. Alignement palette Stitch (`bg-on-surface/75`, `bg-surface-container-lowest/95`, `rounded-sheet`, `text-primary`, description `font-body` Newsreader). `"use client"` + `role="dialog" aria-modal` ajoutés. |