feat(outils): Partir de soi en vue fiches, graphe des sept mots (B6-M2)

Le 3e sous-onglet de /outils remplace le rendu provisoire par VueFiches
(état Mixte : liste des mots, graphe SVG statique, fiche d'un mot avec ses
ressources). L'en-tête se compacte sur cet onglet, la vue prend la hauteur.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136n5xMLHuaBDLcPJYCDjRW
This commit is contained in:
Jules Neny
2026-10-08 16:56:18 +02:00
co-authored by Claude Sonnet
parent 11f24c7925
commit dfbac7dd12
3 changed files with 588 additions and 138 deletions
+49 -138
View File
@@ -1,5 +1,5 @@
<template>
<div class="outils-page">
<div class="outils-page" :class="{ 'outils-page--vue': vueFiches }">
<!-- ══════════════════════ EN-TÊTE PAGE ══════════════════════ -->
<header class="outils-header">
@@ -9,9 +9,13 @@
</div>
<div class="outils-header__body">
<h1 class="outils-header__title">Outils</h1>
<EchelleTete echelle="S" :contribuer="false" />
<p v-if="outils?.intro" class="outils-header__intro">{{ outils.intro }}</p>
<!-- Partir de soi : la vue fiches prend la hauteur, l'en-tête se compacte (les sept mots sont dans le graphe) -->
<EchelleTete v-if="!vueFiches" echelle="S" :contribuer="false" />
<p v-if="outils?.intro && !vueFiches" class="outils-header__intro">{{ outils.intro }}</p>
</div>
<NuxtLink v-if="vueFiches && outils?.contribution" :to="outils.contribution.lien" class="outils-header__proposer">
{{ outils.contribution.cta }}
</NuxtLink>
</div>
</header>
@@ -44,6 +48,7 @@
<main
id="outils-panneau"
class="outils-main"
:class="{ 'outils-main--vue': vueFiches }"
role="tabpanel"
:aria-labelledby="`outils-tab-${tab}`"
>
@@ -159,57 +164,11 @@
</section>
</template>
<!-- ══════════════ ONGLET 3 — Partir de soi (rendu provisoire, M2 le remplace par VueFiches) ══════════════ -->
<template v-else>
<p class="outils-intention">{{ outils.partir_de_soi.intention }}</p>
<!-- ══════════════ ONGLET 3 — Partir de soi : la vue fiches (B6-M2) ══════════════ -->
<OutilsPartirDeSoi v-else :data="outils.partir_de_soi" />
<section
v-for="f in outils.partir_de_soi.fiches"
:id="f.id"
:key="f.id"
class="outils-section fiche-s"
:aria-labelledby="`titre-${f.id}`"
>
<h2 :id="`titre-${f.id}`" class="outils-section__title">{{ f.mot }}</h2>
<p class="fiche-s__resume">{{ f.resume }}</p>
<p v-if="f.vide" class="fiche-s__vide">{{ f.vide }}</p>
<ul v-else class="ressources">
<li v-for="r in ressourcesDe(f.id)" :key="r.id" class="ressource">
<p class="ressource__titre">
<a v-if="r.url" :href="r.url" target="_blank" rel="noopener noreferrer">{{ r.titre }}</a>
<template v-else>{{ r.titre }}</template>
</p>
<p class="ressource__meta">{{ r.format }} · {{ r.acces }}</p>
<p class="ressource__pourquoi">{{ r.pourquoi }}</p>
<p v-if="r.urls?.length" class="ressource__liens">
<template v-for="(u, i) in r.urls" :key="u">
<span v-if="i > 0"> · </span>
<a :href="u" target="_blank" rel="noopener noreferrer">{{ u.replace(/^https:\/\//, '') }}</a>
</template>
</p>
<p v-if="r.note" class="ressource__note">{{ r.note }}</p>
<ul v-if="r.episodes?.length" class="ofqa-list">
<li v-for="ep in r.episodes" :key="ep.ep" class="ofqa-list__item">
<component
:is="ep.url ? 'a' : 'span'"
v-bind="ep.url ? { href: ep.url, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="ofqa-list__link"
:class="{ 'ofqa-list__link--disabled': !ep.url }"
>
<span class="ofqa-list__ep">EP/{{ ep.ep }}</span>
<span class="ofqa-list__titre">{{ ep.titre }}</span>
<span class="ofqa-list__personnes">— {{ ep.personnes }}</span>
<span v-if="ep.note" class="ofqa-list__note">({{ ep.note }})</span>
</component>
</li>
</ul>
</li>
</ul>
</section>
</template>
<!-- ══════════════ CONTRIBUTION (les 3 onglets) ══════════════ -->
<footer v-if="outils?.contribution" class="outils-contribution">
<!-- ══════════════ CONTRIBUTION (onglets 1 et 2 ; sur le 3e, le lien est dans l'en-tête) ══════════════ -->
<footer v-if="outils?.contribution && !vueFiches" class="outils-contribution">
<p class="outils-contribution__texte">{{ outils.contribution.texte }}</p>
<NuxtLink :to="outils.contribution.lien" class="outils-contribution__cta">
{{ outils.contribution.cta }}
@@ -242,8 +201,8 @@ watch(() => route.query.tab, (q) => { tab.value = lireTab(q) })
const { data: outils } = await useFetch<any>('/data/outils.json')
const { data: fmhyData, pending: fmhyPending, error: fmhyError } = await useFetch<any>('/data/fmhy-curated.json')
const ressourcesDe = (ficheId: string) =>
(outils.value?.partir_de_soi?.ressources ?? []).filter((r: any) => r.fiches?.includes(ficheId))
// Partir de soi : la vue fiches remplit la hauteur sous la barre d'onglets (pas de défilement de page).
const vueFiches = computed(() => tab.value === 'partir-de-soi' && !!outils.value)
// Flèches ← → (+ Début / Fin) : on se déplace et on active, comme un tablist à activation automatique.
const tablistRef = ref<HTMLElement | null>(null)
@@ -559,89 +518,6 @@ useSeoMeta({
margin: 0.5rem 0 -0.5rem;
}
/* ── Partir de soi (provisoire) ─────────────────────────────── */
.fiche-s { gap: var(--space-sm); }
.fiche-s__resume {
font-size: 0.88rem;
line-height: 1.6;
margin: 0;
max-width: 70ch;
color: color-mix(in srgb, var(--text) 78%, transparent);
}
.fiche-s__vide {
font-size: 0.86rem;
line-height: 1.6;
margin: 0;
padding: 0.75rem 1rem;
border: 1.5px dashed var(--border-strong);
border-radius: var(--radius-md);
max-width: 70ch;
}
.ressources {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.ressource { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ressource p { margin: 0; overflow-wrap: anywhere; }
.ressource__titre { font-size: 0.92rem; font-weight: var(--fw-semibold); }
.ressource__titre a, .ressource__liens a { color: var(--nav-primary-solid); }
.ressource__meta { font-size: 0.78rem; color: color-mix(in srgb, var(--text) 78%, transparent); }
.ressource__pourquoi { font-size: 0.84rem; line-height: 1.5; }
.ressource__liens { font-size: 0.82rem; }
.ressource__note { font-size: 0.78rem; font-style: italic; color: color-mix(in srgb, var(--text) 78%, transparent); }
/* ── Liste OFQA ─────────────────────────────────────────────── */
.ofqa-list {
list-style: none;
padding: 0;
margin: var(--space-xs) 0 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.ofqa-list__link {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
flex-wrap: wrap;
padding: 3px 6px;
border-radius: var(--radius-sm);
font-size: 0.84rem;
text-decoration: none;
color: var(--text);
}
.ofqa-list__link:not(.ofqa-list__link--disabled):hover {
background: var(--bg-alt);
color: var(--nav-primary-solid);
text-decoration: underline;
}
.ofqa-list__link--disabled { cursor: default; }
.ofqa-list__ep {
font-family: var(--font-mono, 'JetBrains Mono', 'Courier New', monospace);
font-size: 0.75rem;
font-weight: var(--fw-semibold);
flex-shrink: 0;
min-width: 4.5rem;
color: color-mix(in srgb, var(--text) 78%, transparent);
}
.ofqa-list__titre { font-weight: var(--fw-medium); }
.ofqa-list__personnes,
.ofqa-list__note { font-size: 0.8rem; color: color-mix(in srgb, var(--text) 78%, transparent); }
.ofqa-list__note { font-style: italic; }
/* ── Logiciels pro (placeholder) ────────────────────────────── */
.outils-section--placeholder { opacity: 0.8; }
@@ -714,10 +590,45 @@ useSeoMeta({
outline-offset: 2px;
}
/* ── Partir de soi : la vue fiches remplit la hauteur du conteneur de défilement (app.vue) ── */
.outils-page--vue {
max-width: none;
margin: 0;
padding: 0;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.outils-page--vue .outils-header {
flex-shrink: 0;
margin: 0;
padding: var(--space-xs) var(--space-md) 0;
}
.outils-page--vue .outils-header__inner { align-items: center; }
.outils-page--vue .outils-header__icon-wrap { width: 2rem; height: 2rem; padding: 0.4rem; }
.outils-page--vue .outils-header__title { font-size: 1.25rem; margin: 0; }
.outils-page--vue .outils-header__body { flex: 1 1 auto; }
.outils-page--vue .outils-tabs { flex-shrink: 0; margin: 0; padding: 0 var(--space-md); }
.outils-main--vue { flex: 1 1 0; min-height: 0; gap: 0; }
.outils-header__proposer {
flex-shrink: 0;
font-size: 0.85rem;
font-weight: var(--fw-semibold);
color: var(--nav-primary-solid);
text-underline-offset: 2px;
}
.outils-header__proposer:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
/* ── Mobile ─────────────────────────────────────────────────── */
@media (max-width: 640px) {
.outils-page { padding: var(--space-lg) var(--space-md) 4rem; }
.outils-header__inner { flex-direction: column; gap: var(--space-sm); }
.outils-page--vue { padding: 0; }
.outils-page--vue .outils-header__inner { flex-direction: row; gap: var(--space-sm); }
.outils-page--vue .outils-header__icon-wrap { display: none; }
.outils-header__title { font-size: 1.4rem; }
.outil-cards-grid { grid-template-columns: 1fr; }
.outils-section--fmhy { padding: 1.25rem 1rem; }