feat(nav): onglets S / M / XL et echelleDeRoute dans utils/echelles.ts
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBu7CSLy7PJgL4HJaV5oHP
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3f5ebe1c36
commit
7762b31f1d
@@ -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)
|
||||||
+50
-1
@@ -1,10 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* Les trois échelles du deck du 23/09/2026 (conférence AEP), slides 15 à 18.
|
* 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).
|
* 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'
|
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<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OngletNav {
|
||||||
|
libelle: string
|
||||||
|
to: string
|
||||||
|
actif: (route: RouteNav) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface Echelle {
|
export interface Echelle {
|
||||||
id: EchelleId
|
id: EchelleId
|
||||||
titre: string
|
titre: string
|
||||||
@@ -13,8 +26,13 @@ export interface Echelle {
|
|||||||
phrase: string
|
phrase: string
|
||||||
/** Catégorie de /proposer présélectionnée par le lien de contribution. */
|
/** Catégorie de /proposer présélectionnée par le lien de contribution. */
|
||||||
categorieProposer: 'ecosysteme' | 'reseau' | 'jobs' | 'outils' | 'references'
|
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<EchelleId, Echelle> = {
|
export const ECHELLES: Record<EchelleId, Echelle> = {
|
||||||
S: {
|
S: {
|
||||||
id: 'S',
|
id: 'S',
|
||||||
@@ -31,6 +49,10 @@ export const ECHELLES: Record<EchelleId, Echelle> = {
|
|||||||
],
|
],
|
||||||
phrase: 'Des ressources, des méthodes, des pairs : matériel et mutualisé.',
|
phrase: 'Des ressources, des méthodes, des pairs : matériel et mutualisé.',
|
||||||
categorieProposer: 'outils',
|
categorieProposer: 'outils',
|
||||||
|
onglets: [
|
||||||
|
{ libelle: 'Jobs', to: '/trouver-du-taf', actif: sousChemin('/trouver-du-taf') },
|
||||||
|
{ libelle: 'Outils', to: '/outils', actif: sousChemin('/outils') },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
M: {
|
M: {
|
||||||
id: 'M',
|
id: 'M',
|
||||||
@@ -46,6 +68,11 @@ export const ECHELLES: Record<EchelleId, Echelle> = {
|
|||||||
],
|
],
|
||||||
phrase: "C'est le groupe qui remet un individu debout. L'affinité fait tenir, la confrontation fait avancer.",
|
phrase: "C'est le groupe qui remet un individu debout. L'affinité fait tenir, la confrontation fait avancer.",
|
||||||
categorieProposer: 'ecosysteme',
|
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: {
|
XL: {
|
||||||
id: 'XL',
|
id: 'XL',
|
||||||
@@ -61,8 +88,30 @@ export const ECHELLES: Record<EchelleId, Echelle> = {
|
|||||||
],
|
],
|
||||||
phrase: 'La bibliothèque, les récits, la filiation entre projets.',
|
phrase: 'La bibliothèque, les récits, la filiation entre projets.',
|
||||||
categorieProposer: 'references',
|
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. */
|
/** 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."
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user