From 7762b31f1da8a48690e1d2ee5b2a4884dc79df32 Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Fri, 2 Oct 2026 16:48:28 +0200 Subject: [PATCH] feat(nav): onglets S / M / XL et echelleDeRoute dans utils/echelles.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chaque échelle porte ses onglets (libellé, cible, actif(route)) ; echelleDeRoute rend le groupe courant ou null. Source unique de la nav B10. Test node : scripts/test-echelles-nav.mjs (23/23). B10 M1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EBu7CSLy7PJgL4HJaV5oHP --- scripts/test-echelles-nav.mjs | 75 +++++++++++++++++++++++++++++++++++ utils/echelles.ts | 51 +++++++++++++++++++++++- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 scripts/test-echelles-nav.mjs diff --git a/scripts/test-echelles-nav.mjs b/scripts/test-echelles-nav.mjs new file mode 100644 index 0000000..aba8045 --- /dev/null +++ b/scripts/test-echelles-nav.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * Test des données de nav S / M / XL (B10-M1). + * Charge le VRAI fichier utils/echelles.ts via jiti (même patron que test-submit-libre.mjs) : + * chaque route → bon groupe et bon onglet actif, un seul onglet actif par route. + * Usage : node scripts/test-echelles-nav.mjs + */ + +import { createJiti } from 'jiti' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const jiti = createJiti(import.meta.url) + +const { ECHELLES, ORDRE_ECHELLES, ECHELLES_CADRE_COURT, echelleDeRoute } = await jiti.import( + join(__dirname, '..', 'utils', 'echelles.ts'), +) + +const cases = [ + // [path, query, groupe attendu, onglet attendu] + ['/trouver-du-taf', {}, 'S', 'Jobs'], + ['/outils', {}, 'S', 'Outils'], + ['/', {}, 'M', "Écosystème d'entraide"], + ['/', { vue: 'fiches' }, 'M', "Écosystème d'entraide"], + ['/agences', {}, 'M', 'Réseaux AEP'], + ['/agences', { vue: 'fiches' }, 'M', 'Réseaux AEP'], + ['/codev', {}, 'M', 'Codev'], + ['/codev/demo', {}, 'M', 'Codev'], + ['/codev/carto', {}, 'M', 'Codev'], + ['/media', {}, 'XL', 'Recherche média'], + ['/media', { tab: 'visuel' }, 'XL', 'Recherche média'], + ['/media', { tab: 'projets' }, 'XL', 'Publications'], + ['/manifeste', {}, null, null], + ['/a-propos', {}, null, null], + ['/proposer', {}, null, null], + ['/signaler', {}, null, null], + ['/fiche/42', {}, null, null], + ['/outilsXYZ', {}, null, null], + ['/codevelopper', {}, null, null], +] + +let fail = 0 +for (const [path, query, groupe, onglet] of cases) { + const route = { path, query } + const r = echelleDeRoute(route) + const gotG = r?.echelle ?? null + const gotO = r?.onglet.libelle ?? null + // Un seul onglet actif sur tout le site pour une route donnée. + const actifs = ORDRE_ECHELLES.flatMap((id) => ECHELLES[id].onglets.filter((o) => o.actif(route))) + const ok = gotG === groupe && gotO === onglet && actifs.length === (groupe ? 1 : 0) + if (!ok) fail++ + console.log(`${ok ? 'OK ' : 'FAIL'} ${path}${Object.keys(query).length ? '?' + new URLSearchParams(query) : ''} → ${gotG ?? '∅'} / ${gotO ?? '∅'} (actifs : ${actifs.length})`) +} + +// Ordre et contenu des groupes (directive Jules du 29/09 + arbitrage 1A). +const attendu = { + S: ['Jobs', 'Outils'], + M: ["Écosystème d'entraide", 'Réseaux AEP', 'Codev'], + XL: ['Recherche média', 'Publications'], +} +for (const id of ORDRE_ECHELLES) { + const got = ECHELLES[id].onglets.map((o) => o.libelle) + const ok = JSON.stringify(got) === JSON.stringify(attendu[id]) + if (!ok) fail++ + console.log(`${ok ? 'OK ' : 'FAIL'} groupe ${id} = ${got.join(' · ')}`) +} + +const cadreOk = ECHELLES_CADRE_COURT === 'Trois échelles, imbriquées. Entrez par celle qui presse.' +if (!cadreOk) fail++ +console.log(`${cadreOk ? 'OK ' : 'FAIL'} cadre court = « ${ECHELLES_CADRE_COURT} »`) + +const total = cases.length + ORDRE_ECHELLES.length + 1 +console.log(`\n${total - fail}/${total}`) +process.exit(fail ? 1 : 0) diff --git a/utils/echelles.ts b/utils/echelles.ts index 7651ba5..d27ba71 100644 --- a/utils/echelles.ts +++ b/utils/echelles.ts @@ -1,10 +1,23 @@ /** * Les trois échelles du deck du 23/09/2026 (conférence AEP), slides 15 à 18. * Texte repris mot pour mot : c'est la tête de chaque onglet (directive Jules du 25/09, point 2). - * Source unique : la future nav S / M / XL (arbitrage n° 1, table R) lira aussi ce fichier. + * Source unique : la nav S / M / XL (B10 : NavEchelles, NavTiroir, FilEchelle) lit aussi ce fichier. + * Aucun libellé d'échelle ni d'onglet ne se recopie ailleurs. */ export type EchelleId = 'S' | 'M' | 'XL' +/** Ce que la nav lit d'une route : compatible avec `useRoute()` sans en dépendre (testable en node). */ +export interface RouteNav { + path: string + query?: Record +} + +export interface OngletNav { + libelle: string + to: string + actif: (route: RouteNav) => boolean +} + export interface Echelle { id: EchelleId titre: string @@ -13,8 +26,13 @@ export interface Echelle { phrase: string /** Catégorie de /proposer présélectionnée par le lien de contribution. */ categorieProposer: 'ecosysteme' | 'reseau' | 'jobs' | 'outils' | 'references' + /** Onglets du groupe, dans l'ordre de la barre, du menu et du fil d'échelle (B10, directive Jules du 29/09 + arbitrage 1A du 26/09). */ + onglets: OngletNav[] } +const sousChemin = (base: string) => (r: RouteNav) => r.path === base || r.path.startsWith(`${base}/`) +const surPublications = (r: RouteNav) => r.query?.tab === 'projets' + export const ECHELLES: Record = { S: { id: 'S', @@ -31,6 +49,10 @@ export const ECHELLES: Record = { ], phrase: 'Des ressources, des méthodes, des pairs : matériel et mutualisé.', categorieProposer: 'outils', + onglets: [ + { libelle: 'Jobs', to: '/trouver-du-taf', actif: sousChemin('/trouver-du-taf') }, + { libelle: 'Outils', to: '/outils', actif: sousChemin('/outils') }, + ], }, M: { id: 'M', @@ -46,6 +68,11 @@ export const ECHELLES: Record = { ], phrase: "C'est le groupe qui remet un individu debout. L'affinité fait tenir, la confrontation fait avancer.", categorieProposer: 'ecosysteme', + onglets: [ + { libelle: "Écosystème d'entraide", to: '/', actif: (r) => r.path === '/' }, + { libelle: 'Réseaux AEP', to: '/agences', actif: sousChemin('/agences') }, + { libelle: 'Codev', to: '/codev', actif: sousChemin('/codev') }, + ], }, XL: { id: 'XL', @@ -61,8 +88,30 @@ export const ECHELLES: Record = { ], phrase: 'La bibliothèque, les récits, la filiation entre projets.', categorieProposer: 'references', + onglets: [ + { libelle: 'Recherche média', to: '/media', actif: (r) => sousChemin('/media')(r) && !surPublications(r) }, + { libelle: 'Publications', to: '/media?tab=projets', actif: (r) => sousChemin('/media')(r) && surPublications(r) }, + ], }, } +/** Ordre des groupes dans la barre et le menu. */ +export const ORDRE_ECHELLES: EchelleId[] = ['S', 'M', 'XL'] + /** Slide 15, commune aux trois échelles. */ export const ECHELLES_CADRE = "Trois échelles, imbriquées. Pas un escalier : ça circule dans les deux sens. Entrez par celle qui presse." + +/** Version courte de la slide 15 (tête du menu) : première et dernière phrase, tirées du texte ci-dessus. */ +export const ECHELLES_CADRE_COURT = (() => { + const phrases = ECHELLES_CADRE.match(/[^.]+\./g) ?? [ECHELLES_CADRE] + return phrases.length > 1 ? `${phrases[0].trim()} ${phrases[phrases.length - 1].trim()}` : ECHELLES_CADRE +})() + +/** Groupe de la page courante et son onglet actif, ou null hors groupe (Manifeste, À propos, Proposer, fiche…). */ +export function echelleDeRoute(route: RouteNav): { echelle: EchelleId; onglet: OngletNav } | null { + for (const id of ORDRE_ECHELLES) { + const onglet = ECHELLES[id].onglets.find((o) => o.actif(route)) + if (onglet) return { echelle: id, onglet } + } + return null +}