Compare commits
6
Commits
a2b89489a6
...
5ce431d055
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce431d055 | ||
|
|
8d05873082 | ||
|
|
358c2954c3 | ||
|
|
79424d29cf | ||
|
|
7762b31f1d | ||
|
|
3f5ebe1c36 |
@@ -82,3 +82,29 @@ Le favicon (`public/favicon.svg`, décliné en `favicon-32.png`, `favicon-16.png
|
|||||||
|
|
||||||
1. **Texte sombre sur l'accent safran, jamais blanc.** `--accent`/`--on-accent` : le blanc sur `#f5b342` échoue le contrôle AA (décision DS du 20/07/2026). `--on-accent` est toujours une couleur sombre (`#1a2238` clair, `#111520` sombre). Vérifié au pixel sur la variante `posture` de `Chip.vue`.
|
1. **Texte sombre sur l'accent safran, jamais blanc.** `--accent`/`--on-accent` : le blanc sur `#f5b342` échoue le contrôle AA (décision DS du 20/07/2026). `--on-accent` est toujours une couleur sombre (`#1a2238` clair, `#111520` sombre). Vérifié au pixel sur la variante `posture` de `Chip.vue`.
|
||||||
2. **`--nav-text-on-primary` reste une valeur littérale**, pas un alias — voir l'explication détaillée dans la section tokens ci-dessus. Si un nouveau composant a besoin de texte de contraste sur un fond bleu nuit, vérifie d'abord si ce fond est `--nav-primary` (translucide) ou `--nav-primary-solid` (opaque) avant de choisir la variable de texte : elles n'ont pas la même réponse en mode sombre.
|
2. **`--nav-text-on-primary` reste une valeur littérale**, pas un alias — voir l'explication détaillée dans la section tokens ci-dessus. Si un nouveau composant a besoin de texte de contraste sur un fond bleu nuit, vérifie d'abord si ce fond est `--nav-primary` (translucide) ou `--nav-primary-solid` (opaque) avant de choisir la variable de texte : elles n'ont pas la même réponse en mode sombre.
|
||||||
|
|
||||||
|
## Navigation par échelles S / M / XL (B10, 02/10/2026)
|
||||||
|
|
||||||
|
**Source unique : `utils/echelles.ts`.** Chaque échelle porte ses `onglets` (`libelle`, `to`, `actif(route)`) ; `ORDRE_ECHELLES` fixe l'ordre S → M → XL ; `echelleDeRoute(route)` rend le groupe et l'onglet courants, ou `null` hors groupe (Manifeste, À propos, Proposer, Signaler, fiche, `/rag`). Aucun libellé d'échelle ni d'onglet ne se recopie dans un composant : ajouter un onglet = une ligne dans ce fichier, et `scripts/test-echelles-nav.mjs` (une ligne de cas).
|
||||||
|
|
||||||
|
| Échelle | Accroche | Onglets |
|
||||||
|
|---|---|---|
|
||||||
|
| S | Partir de soi | Jobs (`/trouver-du-taf`) · Outils (`/outils`) |
|
||||||
|
| M | La communauté | Écosystème d'entraide (`/`) · Réseaux AEP (`/agences`) · Codev (`/codev*`) |
|
||||||
|
| XL | Contribuer | Recherche média (`/media`) · Publications (`/media?tab=projets`) |
|
||||||
|
|
||||||
|
**Seuils** (CSS des composants, pas de breakpoint Tailwind : 1280 et 1440 n'en sont pas) :
|
||||||
|
|
||||||
|
| Largeur | En-tête | Sous l'en-tête |
|
||||||
|
|---|---|---|
|
||||||
|
| ≥ 1440 | logo complet · trois groupes · Manifeste · À propos · Soutenir (contour safran) · + Proposer (plein safran) · sombre | — |
|
||||||
|
| 1280-1439 | idem, logo réduit au bloc « AEP » | — |
|
||||||
|
| 1024-1279 | logo complet · groupe courant seul · Manifeste · + Proposer · sombre · « Menu » | — |
|
||||||
|
| 768-1023 | logo réduit · groupe courant seul · Manifeste · + Proposer · sombre · « Menu » | — |
|
||||||
|
| < 768 | logo complet · sombre · + (icône) · ☰ | `FilEchelle` (28 px), sauf hors groupe |
|
||||||
|
|
||||||
|
**Composants** : `NavEchelles.vue` (barre ; chaque groupe est un `<ul>` étiqueté par son accroche via `aria-labelledby`, onglet courant en `aria-current="page"`), `NavTiroir.vue` (menu < 1280 : `role="dialog"`, voile, Échap, focus piégé et rendu à l'élément d'ouverture, fermeture au changement de route et au passage à ≥ 1280), `FilEchelle.vue` (< 768, monté dans `app.vue` entre `<header>` et le conteneur de page, **jamais dedans** : `/` y est en `overflow-hidden`). Le lien de don vit dans `utils/liens.ts` (`SOUTENIR_URL`).
|
||||||
|
|
||||||
|
**Texte atténué** : `color-mix(in srgb, var(--text) 78 %, transparent)`, pas `--text-muted` (3,7:1 sur blanc, sous AA à 0,8 rem) ni `opacity` (qui éteindrait aussi l'anneau de focus).
|
||||||
|
|
||||||
|
**Signaler** n'est plus dans la barre d'ordinateur : il reste dans le tiroir, dans chaque fiche, et en pied d'À propos.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen overflow-hidden" :class="{ dark: isDark }" style="background: var(--nav-bg);">
|
<div class="flex flex-col h-screen overflow-hidden" :class="{ dark: isDark }" style="background: var(--nav-bg);">
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════ TOP NAV GLOBAL -->
|
<!-- ═══════════════════════════════════════ TOP NAV GLOBAL (B10 : échelles S / M / XL) -->
|
||||||
<header
|
<header
|
||||||
class="flex items-center justify-between px-4 py-2.5 shrink-0 relative z-[9999] shadow-sm gap-3"
|
class="app-header flex items-center px-4 shrink-0 relative z-[9999] shadow-sm gap-3"
|
||||||
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);"
|
style="background: var(--nav-surface); border-bottom: 1px solid var(--nav-bg-alt);"
|
||||||
>
|
>
|
||||||
<!-- Logo -->
|
<!-- Logo : texte complet sauf 768-1023 et 1280-1439 (place des onglets) -->
|
||||||
<a href="/" class="logo-link flex items-center gap-2 hover:opacity-90 transition-opacity shrink-0" title="Architecture d'Écologie Politique">
|
<a href="/" class="logo-link flex items-center gap-2 hover:opacity-90 transition-opacity shrink-0" title="Architecture d'Écologie Politique">
|
||||||
<div
|
<div
|
||||||
class="h-8 px-2 rounded-lg flex items-center justify-center shrink-0"
|
class="h-8 px-2 rounded-lg flex items-center justify-center shrink-0"
|
||||||
@@ -20,112 +20,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- ── Onglets desktop (≥1024px) — remplace la barre de recherche ── -->
|
<!-- Échelles : trois groupes ≥ 1280, groupe courant 768-1279, rien < 768 (fil d'échelle) -->
|
||||||
<nav class="hidden lg:flex flex-1 justify-center items-end gap-0 mx-6" aria-label="Navigation projets">
|
<NavEchelles />
|
||||||
<NuxtLink
|
<div class="flex-1 md:hidden" aria-hidden="true" />
|
||||||
to="/outils"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path === '/outils' }"
|
|
||||||
>
|
|
||||||
Outils
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path === '/' }"
|
|
||||||
>
|
|
||||||
Écosystème Entraide
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/agences"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path === '/agences' }"
|
|
||||||
>
|
|
||||||
Réseaux AEP
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/trouver-du-taf"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path === '/trouver-du-taf' }"
|
|
||||||
>
|
|
||||||
Jobs
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/codev"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path.startsWith('/codev') }"
|
|
||||||
>
|
|
||||||
Codev
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/media"
|
|
||||||
class="nav-tab"
|
|
||||||
:class="{ 'nav-tab--active': route.path.startsWith('/media') }"
|
|
||||||
>
|
|
||||||
recherche-média
|
|
||||||
</NuxtLink>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- ── Barre recherche mobile (640px–1024px) — masquée < 640px car accessible dans la sheet -->
|
|
||||||
<div class="hidden sm:flex lg:hidden flex-1 mx-2">
|
|
||||||
<label class="flex items-center gap-2 w-full px-3 py-1.5 rounded-xl border" style="background: var(--nav-bg); border-color: var(--nav-bg-alt);">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="color: var(--nav-text-muted); flex-shrink: 0;">
|
|
||||||
<circle cx="11" cy="11" r="8"/>
|
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
v-model="headerSearch"
|
|
||||||
type="search"
|
|
||||||
placeholder="Rechercher…"
|
|
||||||
class="flex-1 bg-transparent border-0 outline-none text-sm"
|
|
||||||
style="color: var(--nav-text); font-family: var(--nav-font);"
|
|
||||||
autocomplete="off"
|
|
||||||
@input="onHeaderSearch"
|
|
||||||
@keydown.enter="onHeaderSearch"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-if="headerSearch"
|
|
||||||
type="button"
|
|
||||||
@click.stop="clearHeaderSearch"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
style="color: var(--nav-text-muted);"
|
|
||||||
aria-label="Effacer la recherche"
|
|
||||||
>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions droite -->
|
<!-- Actions droite -->
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/manifeste"
|
to="/manifeste"
|
||||||
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-all hover:opacity-80 hidden md:inline-flex items-center gap-1"
|
class="header-lien hidden md:inline-flex"
|
||||||
:style="route.path === '/manifeste' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text-muted);'"
|
:aria-current="route.path === '/manifeste' ? 'page' : undefined"
|
||||||
>
|
>
|
||||||
Manifeste
|
Manifeste
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/a-propos"
|
to="/a-propos"
|
||||||
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-all hover:opacity-80 hidden md:inline-flex items-center gap-1"
|
class="header-lien header-xl"
|
||||||
style="color: var(--nav-text-muted);"
|
:aria-current="route.path === '/a-propos' ? 'page' : undefined"
|
||||||
>
|
>
|
||||||
À propos
|
À propos
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<a
|
||||||
to="/signaler"
|
:href="SOUTENIR_URL"
|
||||||
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-all hover:opacity-80 hidden lg:inline-flex items-center gap-1"
|
target="_blank"
|
||||||
style="color: var(--nav-text-muted);"
|
rel="noopener noreferrer"
|
||||||
|
class="header-soutenir header-xl"
|
||||||
>
|
>
|
||||||
Signaler
|
Soutenir
|
||||||
</NuxtLink>
|
</a>
|
||||||
<!-- Proposer une ressource (5 onglets) -->
|
<!-- Proposer une ressource -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/proposer"
|
to="/proposer"
|
||||||
class="px-3 py-1.5 rounded-lg text-sm font-semibold transition-all hover:opacity-80 hidden sm:inline-flex items-center gap-1"
|
class="header-proposer hidden md:inline-flex"
|
||||||
style="background: var(--nav-accent); color: var(--nav-text);"
|
|
||||||
>
|
>
|
||||||
+ Proposer
|
+ Proposer
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
@@ -153,68 +79,51 @@
|
|||||||
<!-- Mobile : proposer icône -->
|
<!-- Mobile : proposer icône -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/proposer"
|
to="/proposer"
|
||||||
class="sm:hidden p-2 rounded-lg"
|
class="md:hidden p-2 rounded-lg"
|
||||||
style="background: var(--nav-accent); color: var(--nav-text);"
|
style="background: var(--accent); color: var(--on-accent);"
|
||||||
title="Proposer une ressource"
|
title="Proposer une ressource"
|
||||||
aria-label="Proposer"
|
aria-label="Proposer"
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<!-- Menu déroulant — toutes largeurs (les onglets n'y apparaissent qu'en dessous de 1024px) ; porte « Soutenir » -->
|
<!-- Menu (< 1280) : ouvre le tiroir ; le mot est écrit à partir de 768 -->
|
||||||
<div class="relative">
|
<button
|
||||||
<button
|
type="button"
|
||||||
@click="hamburgerOpen = !hamburgerOpen"
|
class="header-menu"
|
||||||
class="p-2 rounded-lg transition-all hover:opacity-80"
|
aria-label="Ouvrir le menu"
|
||||||
style="background: var(--nav-bg-alt); color: var(--nav-text-muted);"
|
aria-haspopup="dialog"
|
||||||
:aria-label="hamburgerOpen ? 'Fermer le menu' : 'Menu'"
|
:aria-expanded="menuOpen"
|
||||||
:aria-expanded="hamburgerOpen"
|
@click="menuOpen = true"
|
||||||
>
|
>
|
||||||
<svg v-if="!hamburgerOpen" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
||||||
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
<span class="header-menu__mot" aria-hidden="true">Menu</span>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
</button>
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="hamburgerOpen"
|
|
||||||
class="absolute right-0 top-full mt-1 rounded-lg shadow-lg min-w-[210px] py-1"
|
|
||||||
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
|
|
||||||
@click="hamburgerOpen = false"
|
|
||||||
>
|
|
||||||
<NuxtLink to="/outils" class="lg:hidden block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/outils' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Outils</NuxtLink>
|
|
||||||
<NuxtLink to="/" class="lg:hidden block px-4 py-2.5 text-sm font-medium transition-opacity hover:opacity-70" :style="route.path === '/' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Écosystème Entraide</NuxtLink>
|
|
||||||
<NuxtLink to="/agences" class="lg:hidden block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/agences' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Réseaux AEP</NuxtLink>
|
|
||||||
<NuxtLink to="/trouver-du-taf" class="lg:hidden block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/trouver-du-taf' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Jobs</NuxtLink>
|
|
||||||
<NuxtLink to="/media" class="lg:hidden block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path.startsWith('/media') ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">recherche-média</NuxtLink>
|
|
||||||
<NuxtLink to="/codev" class="lg:hidden block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path.startsWith('/codev') ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text);'">Codev</NuxtLink>
|
|
||||||
<div class="lg:hidden" style="height: 1px; background: var(--nav-bg-alt); margin: 4px 0;"></div>
|
|
||||||
<NuxtLink to="/manifeste" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" :style="route.path === '/manifeste' ? 'color: var(--nav-primary-solid); font-weight: 700;' : 'color: var(--nav-text-muted);'">Manifeste</NuxtLink>
|
|
||||||
<NuxtLink to="/a-propos" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" style="color: var(--nav-text-muted);">À propos</NuxtLink>
|
|
||||||
<a href="https://liberapay.com/trans-former.fr/donate" target="_blank" rel="noopener noreferrer" class="block mx-3 my-2 px-4 py-2 rounded-lg text-sm font-semibold text-center transition-opacity hover:opacity-80" style="background: var(--nav-accent); color: var(--nav-text);">Soutenir</a>
|
|
||||||
<NuxtLink to="/signaler" class="block px-4 py-2.5 text-sm transition-opacity hover:opacity-70" style="color: var(--nav-text-muted);">Signaler</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Fil d'échelle (< 768) : dans le flux de la colonne, hors du conteneur de page -->
|
||||||
|
<FilEchelle />
|
||||||
|
|
||||||
<!-- Contenu page (flex-1 pour remplir l'espace) -->
|
<!-- Contenu page (flex-1 pour remplir l'espace) -->
|
||||||
<div class="flex-1" :class="route.path === '/' ? 'overflow-hidden' : 'overflow-y-auto'">
|
<div class="flex-1 min-h-0" :class="route.path === '/' ? 'overflow-hidden' : 'overflow-y-auto'">
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<NavTiroir :open="menuOpen" @close="menuOpen = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const router = useRouter()
|
import { SOUTENIR_URL } from '~/utils/liens'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const hamburgerOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
watch(() => route.path, () => { hamburgerOpen.value = false })
|
|
||||||
|
|
||||||
// ── Dark mode ─────────────────────────────────────────────────────────────
|
// ── Dark mode ─────────────────────────────────────────────────────────────
|
||||||
const isDark = ref(false)
|
const isDark = ref(false)
|
||||||
@@ -237,40 +146,14 @@ function toggleDark() {
|
|||||||
localStorage.setItem('aep_theme', 'light')
|
localStorage.setItem('aep_theme', 'light')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Barre de recherche header mobile ─────────────────────────────────────
|
|
||||||
const headerSearch = ref((route.query.q as string) ?? '')
|
|
||||||
|
|
||||||
// Sync depuis URL quand la route change
|
|
||||||
watch(() => route.query.q, (v) => {
|
|
||||||
headerSearch.value = (v as string) ?? ''
|
|
||||||
})
|
|
||||||
|
|
||||||
function onHeaderSearch() {
|
|
||||||
const q = headerSearch.value.trim()
|
|
||||||
if (route.path === '/') {
|
|
||||||
router.replace({ query: q ? { ...route.query, q } : { ...route.query, q: undefined } })
|
|
||||||
} else if (q) {
|
|
||||||
router.push({ path: '/', query: { q } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearHeaderSearch() {
|
|
||||||
headerSearch.value = ''
|
|
||||||
if (route.path === '/') {
|
|
||||||
const q = { ...route.query }
|
|
||||||
delete q.q
|
|
||||||
router.replace({ query: Object.keys(q).length ? q : undefined })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Fiche aléatoire ───────────────────────────────────────────────────────
|
|
||||||
function goRandom() {
|
|
||||||
router.push({ path: '/', query: { random: '1' } })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* ── En-tête ──────────────────────────────────────────────────────────── */
|
||||||
|
.app-header {
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Logo header (texte 2 lignes) ─────────────────────────────────────── */
|
/* ── Logo header (texte 2 lignes) ─────────────────────────────────────── */
|
||||||
.logo-text {
|
.logo-text {
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
@@ -285,39 +168,96 @@ function goRandom() {
|
|||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.logo-line-1, .logo-line-2 { font-size: 0.85rem; }
|
.logo-line-1, .logo-line-2 { font-size: 0.85rem; }
|
||||||
}
|
}
|
||||||
|
/* Le groupe courant (768-1023) et les trois groupes (1280-1439) prennent la place du texte */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px), (min-width: 1280px) and (max-width: 1439px) {
|
||||||
|
.logo-text { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Onglets header desktop ───────────────────────────────────────────── */
|
/* ── Liens et boutons de droite ───────────────────────────────────────── */
|
||||||
.nav-tab {
|
.header-lien {
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 6px 16px 8px;
|
padding: 6px 8px;
|
||||||
font-size: 0.8rem;
|
border-radius: var(--radius-md);
|
||||||
font-weight: 500;
|
font-size: var(--fs-sm);
|
||||||
color: var(--nav-text-muted);
|
font-weight: var(--fw-medium);
|
||||||
|
white-space: nowrap;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.15s;
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
border-bottom: 2px solid transparent;
|
}
|
||||||
gap: 2px;
|
.header-lien:hover,
|
||||||
|
.header-lien[aria-current="page"] {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.header-lien[aria-current="page"] {
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tab:hover {
|
.header-soutenir {
|
||||||
color: var(--nav-text);
|
align-items: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1.5px solid var(--accent);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tab--active {
|
.header-proposer {
|
||||||
color: var(--nav-text);
|
align-items: center;
|
||||||
border-bottom-color: var(--nav-primary-solid);
|
padding: 6px 11px;
|
||||||
font-weight: 600;
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tab-badge {
|
.header-soutenir:hover,
|
||||||
font-size: 0.6rem;
|
.header-proposer:hover {
|
||||||
font-weight: 600;
|
opacity: 0.85;
|
||||||
text-transform: uppercase;
|
}
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: var(--nav-text-muted);
|
.header-lien:focus-visible,
|
||||||
opacity: 0.65;
|
.header-soutenir:focus-visible,
|
||||||
|
.header-proposer:focus-visible,
|
||||||
|
.header-menu:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* À propos et Soutenir : barre complète seulement (≥ 1280), sinon dans le tiroir */
|
||||||
|
.header-xl { display: none; }
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.header-xl { display: inline-flex; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-menu {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-alt);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.header-menu__mot { display: none; }
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.header-menu {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-color: var(--border);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.header-menu__mot { display: inline; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.header-menu { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -150,19 +150,19 @@ function toggle(tag: string) {
|
|||||||
|
|
||||||
.fiches-panel__grid {
|
.fiches-panel__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.fiches-panel__grid {
|
.fiches-panel__grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
.fiches-panel__grid {
|
.fiches-panel__grid {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
FilEchelle — B10-M4
|
||||||
|
< 768 px : une ligne de 28 px sous la barre, « S · Partir de soi · Outils · Jobs » :
|
||||||
|
l'échelle courante, l'onglet courant, puis ses voisins du groupe (liens).
|
||||||
|
Absente sur les pages hors groupe (Manifeste, À propos, Proposer, Signaler, fiche).
|
||||||
|
Montée dans le flux de la colonne d'app.vue, AVANT le conteneur de page : la carte
|
||||||
|
de / perd ces 28 px à vue, jamais en dessous de la ligne de flottaison.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<nav v-if="courant" class="fil-echelle" :class="{ 'fil-echelle--long': voisins.length > 1 }" aria-label="Échelle courante">
|
||||||
|
<span class="fil-echelle__pastille">{{ courant.echelle }}</span>
|
||||||
|
<span class="fil-echelle__titre">{{ ECHELLES[courant.echelle].titre }}</span>
|
||||||
|
<span class="fil-echelle__sep" aria-hidden="true">·</span>
|
||||||
|
<span class="fil-echelle__courant" aria-current="page">{{ courant.onglet.libelle }}</span>
|
||||||
|
<template v-for="o in voisins" :key="o.to">
|
||||||
|
<span class="fil-echelle__sep" aria-hidden="true">·</span>
|
||||||
|
<NuxtLink :to="o.to" class="fil-echelle__lien">{{ o.libelle }}</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ECHELLES, echelleDeRoute } from '~/utils/echelles'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const courant = computed(() => echelleDeRoute(route))
|
||||||
|
const voisins = computed(() =>
|
||||||
|
courant.value ? ECHELLES[courant.value.echelle].onglets.filter((o) => o !== courant.value!.onglet) : [],
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fil-echelle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 var(--space-sm);
|
||||||
|
gap: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1;
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--bg-alt);
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fil-echelle::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fil-echelle__pastille {
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fil-echelle__courant {
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fil-echelle__lien {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--border);
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fil-echelle__lien:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Groupe à trois onglets (M) sous 400 px : l'accroche se replie sur sa pastille
|
||||||
|
(lue quand même par les lecteurs d'écran), sinon « Codev » sort de l'écran. */
|
||||||
|
@media (max-width: 399px) {
|
||||||
|
.fil-echelle--long .fil-echelle__titre {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
}
|
||||||
|
.fil-echelle--long .fil-echelle__titre + .fil-echelle__sep {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.fil-echelle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="media-tab-projets" style="padding: 1.5rem; overflow-y: auto;">
|
<div class="media-tab-projets" style="padding: 1.5rem; overflow-y: auto;">
|
||||||
<div style="max-width: 70ch; margin-bottom: 1.5rem;">
|
<div style="max-width: 70ch; margin-bottom: 1.5rem;">
|
||||||
<h2 style="font-weight: 700; font-size: 1.1rem; margin-bottom: 0.5rem; color: var(--nav-text);">PFE engagés</h2>
|
<h2 style="font-weight: 700; font-size: 1.1rem; margin-bottom: 0.5rem; color: var(--nav-text);">Publications</h2>
|
||||||
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--nav-text);">
|
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--nav-text);">
|
||||||
Mutualiser le savoir. Voici les PFE engagés publiés en ligne dont nous avons connaissance.
|
Mutualiser le savoir. Voici les PFE engagés publiés en ligne dont nous avons connaissance.
|
||||||
Partage-nous le lien de ton travail si tu veux participer à cette initiative.
|
Partage-nous le lien de ton travail si tu veux participer à cette initiative.
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<!--
|
||||||
|
NavEchelles — B10-M2
|
||||||
|
Barre des trois échelles S / M / XL, lue depuis utils/echelles.ts (source unique).
|
||||||
|
≥ 1280 px : les trois groupes ; 768-1279 px : le seul groupe de la page courante ;
|
||||||
|
< 768 px : rien (FilEchelle + NavTiroir prennent le relais).
|
||||||
|
Chaque groupe = <ul> étiqueté par son accroche (pastille + titre de l'échelle).
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<nav class="nav-echelles" aria-label="Échelles">
|
||||||
|
<div
|
||||||
|
v-for="id in ORDRE_ECHELLES"
|
||||||
|
:key="id"
|
||||||
|
class="nav-echelles__grp"
|
||||||
|
:class="{ 'nav-echelles__grp--courant': courant?.echelle === id }"
|
||||||
|
>
|
||||||
|
<span :id="`nav-echelle-${id}`" class="nav-echelles__kick">
|
||||||
|
<span class="nav-echelles__pastille">{{ id }}</span>{{ ECHELLES[id].titre }}
|
||||||
|
</span>
|
||||||
|
<ul class="nav-echelles__tabs" :aria-labelledby="`nav-echelle-${id}`">
|
||||||
|
<li v-for="o in ECHELLES[id].onglets" :key="o.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="o.to"
|
||||||
|
class="nav-echelles__tab"
|
||||||
|
:class="{ 'nav-echelles__tab--actif': o.actif(route) }"
|
||||||
|
:aria-current="o.actif(route) ? 'page' : undefined"
|
||||||
|
>{{ o.libelle }}</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ECHELLES, ORDRE_ECHELLES, echelleDeRoute } from '~/utils/echelles'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const courant = computed(() => echelleDeRoute(route))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-echelles {
|
||||||
|
display: none;
|
||||||
|
align-self: stretch;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__grp {
|
||||||
|
position: relative;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__kick {
|
||||||
|
padding: 0 var(--space-xs);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1.3;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__grp--courant .nav-echelles__kick {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__pastille {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__tabs {
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__tab {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 9px 9px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: color var(--dur-fast) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__tab:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__tab:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: -2px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-echelles__tab--actif {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
border-bottom-color: var(--nav-primary-solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 768-1279 : le seul groupe courant, aligné à gauche après le logo */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.nav-echelles {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.nav-echelles__grp--courant {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ≥ 1280 : les trois groupes, centrés, séparés par un filet */
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.nav-echelles {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.nav-echelles__grp {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.nav-echelles__grp + .nav-echelles__grp::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--space-lg) / -2);
|
||||||
|
top: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.nav-echelles__tab { transition: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<!--
|
||||||
|
NavTiroir — B10-M3
|
||||||
|
Menu < 1280 px : tiroir pleine hauteur à droite, voile, Échap, focus piégé,
|
||||||
|
focus rendu à l'élément qui l'a ouvert. Les trois échelles dans l'ordre de la
|
||||||
|
barre (utils/echelles.ts), puis Manifeste, À propos, Signaler, et Soutenir.
|
||||||
|
Se ferme seul au changement de route et si la fenêtre passe à ≥ 1280 px.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="nav-tiroir">
|
||||||
|
<div v-if="open" class="nav-tiroir" @keydown="onKeydown">
|
||||||
|
<div class="nav-tiroir__voile" aria-hidden="true" @click="fermer" />
|
||||||
|
<div
|
||||||
|
ref="panneau"
|
||||||
|
class="nav-tiroir__panneau"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="nav-tiroir-titre"
|
||||||
|
>
|
||||||
|
<div class="nav-tiroir__tete">
|
||||||
|
<h2 id="nav-tiroir-titre" class="nav-tiroir__titre">Menu</h2>
|
||||||
|
<button type="button" class="nav-tiroir__fermer" aria-label="Fermer le menu" @click="fermer">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-tiroir__corps" aria-label="Échelles et pages">
|
||||||
|
<p class="nav-tiroir__cadre">{{ ECHELLES_CADRE_COURT }}</p>
|
||||||
|
|
||||||
|
<div v-for="id in ORDRE_ECHELLES" :key="id" class="nav-tiroir__grp">
|
||||||
|
<span :id="`nav-tiroir-echelle-${id}`" class="nav-tiroir__kick">
|
||||||
|
<span class="nav-tiroir__pastille">{{ id }}</span>{{ ECHELLES[id].titre }}
|
||||||
|
</span>
|
||||||
|
<ul class="nav-tiroir__liste" :aria-labelledby="`nav-tiroir-echelle-${id}`">
|
||||||
|
<li v-for="o in ECHELLES[id].onglets" :key="o.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="o.to"
|
||||||
|
class="nav-tiroir__lien"
|
||||||
|
:class="{ 'nav-tiroir__lien--actif': o.actif(route) }"
|
||||||
|
:aria-current="o.actif(route) ? 'page' : undefined"
|
||||||
|
@click="fermer"
|
||||||
|
>{{ o.libelle }}</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="nav-tiroir__sep" />
|
||||||
|
|
||||||
|
<ul class="nav-tiroir__liste">
|
||||||
|
<li v-for="p in PAGES" :key="p.to">
|
||||||
|
<NuxtLink
|
||||||
|
:to="p.to"
|
||||||
|
class="nav-tiroir__second"
|
||||||
|
:aria-current="route.path === p.to ? 'page' : undefined"
|
||||||
|
@click="fermer"
|
||||||
|
>{{ p.libelle }}</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a :href="SOUTENIR_URL" target="_blank" rel="noopener noreferrer" class="nav-tiroir__soutenir">Soutenir</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ECHELLES, ECHELLES_CADRE_COURT, ORDRE_ECHELLES } from '~/utils/echelles'
|
||||||
|
import { SOUTENIR_URL } from '~/utils/liens'
|
||||||
|
|
||||||
|
const props = defineProps<{ open: boolean }>()
|
||||||
|
const emit = defineEmits<{ close: [] }>()
|
||||||
|
|
||||||
|
const PAGES = [
|
||||||
|
{ libelle: 'Manifeste', to: '/manifeste' },
|
||||||
|
{ libelle: 'À propos', to: '/a-propos' },
|
||||||
|
{ libelle: 'Signaler', to: '/signaler' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const panneau = ref<HTMLElement | null>(null)
|
||||||
|
let retourFocus: HTMLElement | null = null
|
||||||
|
|
||||||
|
function fermer() {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusables(): HTMLElement[] {
|
||||||
|
if (!panneau.value) return []
|
||||||
|
return [...panneau.value.querySelectorAll<HTMLElement>('a[href], button:not([disabled])')]
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
fermer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key !== 'Tab') return
|
||||||
|
const els = focusables()
|
||||||
|
if (!els.length) return
|
||||||
|
const premier = els[0]
|
||||||
|
const dernier = els[els.length - 1]
|
||||||
|
if (e.shiftKey && document.activeElement === premier) {
|
||||||
|
e.preventDefault()
|
||||||
|
dernier.focus()
|
||||||
|
} else if (!e.shiftKey && document.activeElement === dernier) {
|
||||||
|
e.preventDefault()
|
||||||
|
premier.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, async (ouvert) => {
|
||||||
|
if (ouvert) {
|
||||||
|
retourFocus = document.activeElement as HTMLElement | null
|
||||||
|
await nextTick()
|
||||||
|
focusables()[0]?.focus()
|
||||||
|
} else if (retourFocus) {
|
||||||
|
// Le bouton d'ouverture peut ne plus être visible (fenêtre élargie) : focus() est alors sans effet.
|
||||||
|
retourFocus.focus()
|
||||||
|
retourFocus = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => route.fullPath, () => { if (props.open) fermer() })
|
||||||
|
|
||||||
|
let mq: MediaQueryList | null = null
|
||||||
|
function onLarge(e: MediaQueryListEvent) { if (e.matches && props.open) fermer() }
|
||||||
|
onMounted(() => {
|
||||||
|
mq = window.matchMedia('(min-width: 1280px)')
|
||||||
|
mq.addEventListener('change', onLarge)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => mq?.removeEventListener('change', onLarge))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-tiroir {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__voile {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(var(--nav-primary-raw), 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__panneau {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(320px, 88vw);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__tete {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: var(--space-xs) var(--space-sm) var(--space-xs) var(--space-md);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__titre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__fermer {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__corps {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-sm) var(--space-md) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__cadre {
|
||||||
|
margin: 0 0 var(--space-xs);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
line-height: var(--lh-base);
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__grp {
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__kick {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--space-2xs);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__pastille {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 6px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__liste {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__lien,
|
||||||
|
.nav-tiroir__second {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 var(--space-xs);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__lien {
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__lien--actif {
|
||||||
|
background: var(--bg-alt);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__second {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__lien:focus-visible,
|
||||||
|
.nav-tiroir__second:focus-visible,
|
||||||
|
.nav-tiroir__soutenir:focus-visible,
|
||||||
|
.nav-tiroir__fermer:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__sep {
|
||||||
|
margin: var(--space-sm) 0;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir__soutenir {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--on-accent);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tiroir-enter-active,
|
||||||
|
.nav-tiroir-leave-active {
|
||||||
|
transition: opacity var(--dur-base) var(--ease-out);
|
||||||
|
}
|
||||||
|
.nav-tiroir-enter-active .nav-tiroir__panneau,
|
||||||
|
.nav-tiroir-leave-active .nav-tiroir__panneau {
|
||||||
|
transition: transform var(--dur-base) var(--ease-out);
|
||||||
|
}
|
||||||
|
.nav-tiroir-enter-from,
|
||||||
|
.nav-tiroir-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.nav-tiroir-enter-from .nav-tiroir__panneau,
|
||||||
|
.nav-tiroir-leave-to .nav-tiroir__panneau {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.nav-tiroir-enter-active,
|
||||||
|
.nav-tiroir-leave-active,
|
||||||
|
.nav-tiroir-enter-active .nav-tiroir__panneau,
|
||||||
|
.nav-tiroir-leave-active .nav-tiroir__panneau {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
|
htmlAttrs: { lang: 'fr' },
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
||||||
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32.png' },
|
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32.png' },
|
||||||
|
|||||||
@@ -185,6 +185,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- B10-M5 : Signaler a quitté la barre d'ordinateur ; il reste ici, dans le menu et dans chaque fiche -->
|
||||||
|
<p class="section-text apropos-pied">
|
||||||
|
Une fiche fausse, un lien mort, un contenu à retirer ?
|
||||||
|
<NuxtLink to="/signaler">Signaler un problème</NuxtLink>
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -270,6 +276,18 @@ function copyApiUrl() {
|
|||||||
margin: 0 0 1rem;
|
margin: 0 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.apropos-pied {
|
||||||
|
padding-top: var(--space-md);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.apropos-pied a {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.section-intro {
|
.section-intro {
|
||||||
color: var(--nav-text-muted);
|
color: var(--nav-text-muted);
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
|
|||||||
+7
-40
@@ -1,56 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- B10-M5 : plus de sous-onglets ici, la nav XL porte « Recherche média » et « Publications » (?tab=projets) -->
|
||||||
<div class="media-page" style="background: var(--nav-bg);">
|
<div class="media-page" style="background: var(--nav-bg);">
|
||||||
<nav class="subtabs" style="display:flex; gap:0; border-bottom: 1px solid var(--nav-bg-alt); background: var(--nav-surface); padding: 0 1rem;">
|
|
||||||
<button
|
|
||||||
:class="['subtab-btn', { active: tab === 'visuel' }]"
|
|
||||||
@click="tab = 'visuel'"
|
|
||||||
>
|
|
||||||
📚 bibliothèque des pensées écologiques
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
:class="['subtab-btn', { active: tab === 'projets' }]"
|
|
||||||
@click="tab = 'projets'"
|
|
||||||
>
|
|
||||||
📐 Projets
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<MediaTabVisuel v-if="tab === 'visuel'" />
|
<MediaTabVisuel v-if="tab === 'visuel'" />
|
||||||
<MediaTabProjets v-else-if="tab === 'projets'" />
|
<MediaTabProjets v-else />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const tab = ref<'visuel' | 'projets'>(
|
// Lu à chaque changement de ?tab= (un lien /media → /media?tab=projets ne remonte pas la page).
|
||||||
(['visuel', 'projets'].includes(route.query.tab as string)
|
const tab = ref<'visuel' | 'projets'>(route.query.tab === 'projets' ? 'projets' : 'visuel')
|
||||||
? route.query.tab as 'visuel' | 'projets'
|
watch(() => route.query.tab, (t) => {
|
||||||
: 'visuel')
|
tab.value = t === 'projets' ? 'projets' : 'visuel'
|
||||||
)
|
|
||||||
|
|
||||||
watch(tab, (newTab) => {
|
|
||||||
router.replace({ query: { ...route.query, tab: newTab } })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({ title: 'AEP - Media' })
|
useHead(() => ({ title: tab.value === 'projets' ? 'AEP - Publications' : 'AEP - Recherche média' }))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.media-page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
.media-page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||||
.subtabs { display: flex; gap: 0; flex-shrink: 0; }
|
|
||||||
.subtab-btn {
|
|
||||||
padding: 10px 18px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--nav-text-muted);
|
|
||||||
transition: color 0.15s, border-color 0.15s;
|
|
||||||
}
|
|
||||||
.subtab-btn:hover { color: var(--nav-text); }
|
|
||||||
.subtab-btn.active { color: var(--nav-primary-solid); border-bottom-color: var(--nav-primary-solid); font-weight: 600; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/** Lien de don (Liberapay), partagé par la barre, le tiroir et À propos. */
|
||||||
|
export const SOUTENIR_URL = 'https://liberapay.com/trans-former.fr/donate'
|
||||||
Reference in New Issue
Block a user