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
226 lines
9.4 KiB
Vue
226 lines
9.4 KiB
Vue
<!--
|
|
OutilsPartirDeSoi — B6-M2. Le sous-onglet « Partir de soi » de /outils : les
|
|
sept mots de la slide 17 de la conférence, un mot = une fiche, rendus par
|
|
la vue fiches partagée (état Mixte : liste des mots | graphe | fiche ouverte).
|
|
Troisième page branchée sur VueFiches (DESIGN-SYSTEM §Brancher une troisième
|
|
page). Les données viennent de `partir_de_soi` dans public/data/outils.json.
|
|
|
|
Monté seulement quand `?tab=partir-de-soi` : l'état de la vue (q, filtres,
|
|
fiche…) vit dans l'URL et disparaît avec l'onglet. `tab=` est recopié par la
|
|
vue à chacune de ses écritures (paramètre qu'elle ne gère pas).
|
|
-->
|
|
<template>
|
|
<div class="ops">
|
|
<VueFiches
|
|
:vf="vf"
|
|
intention="Sept mots de la conférence, et pour chacun des ressources libres qui m'ont aidé ou qu'on m'a confiées. Ouvre un mot."
|
|
libelle-recherche="Rechercher une ressource…"
|
|
:libelles-vue="{ carte: 'Graphe' }"
|
|
:lien-page="() => null"
|
|
>
|
|
<template #carte>
|
|
<GrapheMots :vf="vf" :mots="motsGraphe" />
|
|
</template>
|
|
|
|
<template #detail="{ fiche, filtrer }">
|
|
<article v-if="parId.get(String(fiche.id))" class="mot">
|
|
<h2 class="mot__titre">{{ parId.get(String(fiche.id))!.mot }}</h2>
|
|
<p class="mot__resume">{{ parId.get(String(fiche.id))!.resume }}</p>
|
|
|
|
<template v-if="parId.get(String(fiche.id))!.vide">
|
|
<p class="mot__vide">{{ parId.get(String(fiche.id))!.vide }}</p>
|
|
<NuxtLink to="/proposer?categorie=outils" class="mot__proposer">Proposer une ressource</NuxtLink>
|
|
</template>
|
|
|
|
<ul v-else class="ress" :aria-label="`Ressources pour ${parId.get(String(fiche.id))!.mot}`">
|
|
<li v-for="r in ressourcesDe(String(fiche.id))" :key="r.id" class="ress__carte">
|
|
<p class="ress__titre">
|
|
<a v-if="r.url" :href="r.url" target="_blank" rel="noopener noreferrer">{{ r.titre }}<span class="ress__ext" aria-hidden="true"> ↗</span><span class="vf-sr-mot"> (nouvel onglet)</span></a>
|
|
<template v-else>{{ r.titre }}</template>
|
|
</p>
|
|
|
|
<ChipGroup :aria-label="`Format et accès de ${r.titre}`" class="ress__chips">
|
|
<Chip
|
|
:label="r.format"
|
|
variant="fonction"
|
|
:active="(vf.etat.filtres.format ?? []).includes(slugEtiquette(r.format))"
|
|
:aria-label="`Filtrer par format : ${r.format}`"
|
|
@toggle="filtrer('format', slugEtiquette(r.format))"
|
|
/>
|
|
<Chip
|
|
:label="r.acces"
|
|
variant="echelle"
|
|
:active="(vf.etat.filtres.acces ?? []).includes(slugEtiquette(r.acces))"
|
|
:aria-label="`Filtrer par accès : ${r.acces}`"
|
|
@toggle="filtrer('acces', slugEtiquette(r.acces))"
|
|
/>
|
|
</ChipGroup>
|
|
|
|
<p v-if="r.pourquoi" class="ress__pourquoi">{{ r.pourquoi }}</p>
|
|
<p v-if="r.urls?.length" class="ress__liens">
|
|
<template v-for="(u, i) in r.urls" :key="u">
|
|
<span v-if="i > 0" aria-hidden="true"> · </span>
|
|
<a :href="u" target="_blank" rel="noopener noreferrer">{{ u.replace(/^https:\/\//, '') }}<span class="vf-sr-mot"> (nouvel onglet)</span></a>
|
|
</template>
|
|
</p>
|
|
<p v-if="r.note" class="ress__note">{{ r.note }}</p>
|
|
|
|
<ul v-if="r.episodes?.length" class="eps" :aria-label="`Épisodes de ${r.titre}`">
|
|
<li v-for="ep in r.episodes" :key="ep.ep" class="eps__item" :class="{ 'eps__item--muet': !ep.url }">
|
|
<span class="eps__ep">EP/{{ ep.ep }}</span>
|
|
<span class="eps__corps">
|
|
<a v-if="ep.url" :href="ep.url" target="_blank" rel="noopener noreferrer" class="eps__titre">{{ ep.titre }}<span class="vf-sr-mot"> (nouvel onglet)</span></a>
|
|
<span v-else class="eps__titre">{{ ep.titre }}</span>
|
|
<span class="eps__personnes"> — {{ ep.personnes }}</span>
|
|
<span v-if="ep.note" class="eps__note"> ({{ ep.note }})</span>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</article>
|
|
</template>
|
|
</VueFiches>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ConfigVueFiches } from '~/utils/vueFiches'
|
|
import { toMiniMot, valeursRessources, slugEtiquette, type FicheMot, type RessourceMot } from '~/composables/useFicheAdapter'
|
|
|
|
interface RessourceComplete extends RessourceMot {
|
|
url?: string | null
|
|
urls?: string[]
|
|
note?: string
|
|
episodes?: { ep: string; titre: string; personnes: string; url?: string | null; note?: string }[]
|
|
}
|
|
interface DonneesPartirDeSoi {
|
|
fiches: (FicheMot & { vide?: string | null })[]
|
|
ressources: RessourceComplete[]
|
|
}
|
|
|
|
const props = defineProps<{ data: DonneesPartirDeSoi }>()
|
|
|
|
const ressourcesDe = (motId: string) => props.data.ressources.filter((r) => r.fiches.includes(motId))
|
|
const parId = computed(() => new Map(props.data.fiches.map((f) => [f.id, f])))
|
|
|
|
// Étape 1 : adaptateur (un mot → une MiniFiche). L'ordre du tableau est celui de la slide.
|
|
const fichesMini = computed(() => props.data.fiches.map((f) => toMiniMot(f, ressourcesDe(f.id))))
|
|
|
|
// Étape 2 : configuration. Sept fiches, pas de groupe ; tri « ordre de la slide » par défaut.
|
|
const config = computed<ConfigVueFiches>(() => ({
|
|
dimensions: [
|
|
{ cle: 'format', libelle: 'Format', variante: 'fonction', valeurs: valeursRessources(props.data.ressources, 'format') },
|
|
{ cle: 'acces', libelle: 'Accès', variante: 'echelle', valeurs: valeursRessources(props.data.ressources, 'acces') },
|
|
],
|
|
tris: ['ordre', 'pertinence', 'nom'],
|
|
triDefaut: 'ordre',
|
|
libellesTri: { ordre: 'Ordre de la slide' },
|
|
groupes: [],
|
|
etats: ['carte', 'mixte', 'fiches'],
|
|
}))
|
|
|
|
const vf = useVueFiches(fichesMini, config)
|
|
|
|
// Étape 3 : le graphe (slot #carte) lit les mêmes données et l'état de la vue.
|
|
const motsGraphe = computed(() =>
|
|
props.data.fiches.map((f) => ({
|
|
id: f.id,
|
|
mot: f.mot,
|
|
vide: !!f.vide,
|
|
ressources: ressourcesDe(f.id).map((r) => ({ id: r.id, titre: r.titre })),
|
|
})),
|
|
)
|
|
|
|
// Une fiche ouverte donne son nom à l'onglet du navigateur.
|
|
useSeoMeta({
|
|
title: () => (vf.ficheOuverte ? `${vf.ficheOuverte.nom} — Partir de soi — AEP` : 'Outils — AEP — Partir de soi'),
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.ops { height: 100%; min-height: 0; min-width: 0; }
|
|
|
|
/* Lecteurs d'écran seulement */
|
|
.vf-sr-mot {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Fiche d'un mot */
|
|
.mot__titre { margin: 0 0 var(--space-xs); font-size: 1.25rem; font-weight: var(--fw-bold); line-height: 1.25; }
|
|
.mot__resume {
|
|
margin: 0 0 var(--space-md);
|
|
font-size: var(--fs-sm);
|
|
line-height: 1.55;
|
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
|
}
|
|
.mot__vide {
|
|
margin: 0 0 var(--space-sm);
|
|
padding: var(--space-sm);
|
|
border: 1.5px dashed var(--border-strong);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--fs-sm);
|
|
line-height: 1.55;
|
|
}
|
|
.mot__proposer {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 40px;
|
|
padding: 0 var(--space-md);
|
|
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;
|
|
}
|
|
.mot__proposer:hover { opacity: 0.9; }
|
|
.mot__proposer:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
|
|
|
|
/* Ressources */
|
|
.ress { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
|
|
.ress__carte {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
min-width: 0;
|
|
padding: var(--space-sm);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--bg);
|
|
}
|
|
.ress__carte p { margin: 0; overflow-wrap: anywhere; }
|
|
.ress__titre { font-size: var(--fs-sm); font-weight: var(--fw-semibold); line-height: 1.35; }
|
|
.ress__titre a,
|
|
.ress__liens a { color: var(--nav-primary-solid); text-underline-offset: 2px; }
|
|
.ress__titre a:focus-visible,
|
|
.ress__liens a:focus-visible,
|
|
.eps__titre:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
|
|
.ress__pourquoi { font-size: var(--fs-sm); line-height: 1.5; }
|
|
.ress__liens { font-size: var(--fs-xs, 0.75rem); }
|
|
.ress__note { font-size: var(--fs-xs, 0.75rem); font-style: italic; color: color-mix(in srgb, var(--text) 78%, transparent); }
|
|
|
|
/* Épisodes OFQA */
|
|
.eps { list-style: none; margin: var(--space-2xs) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2xs); }
|
|
.eps__item { display: flex; gap: var(--space-xs); align-items: baseline; font-size: var(--fs-sm); line-height: 1.4; }
|
|
.eps__ep {
|
|
flex: 0 0 3.6rem;
|
|
font-family: var(--font-mono, 'JetBrains Mono', 'Courier New', monospace);
|
|
font-size: var(--fs-xs, 0.75rem);
|
|
font-weight: var(--fw-semibold);
|
|
color: color-mix(in srgb, var(--text) 78%, transparent);
|
|
}
|
|
.eps__corps { min-width: 0; overflow-wrap: anywhere; }
|
|
.eps__titre { font-weight: var(--fw-medium); color: var(--text); }
|
|
a.eps__titre { color: var(--nav-primary-solid); }
|
|
.eps__personnes,
|
|
.eps__note { color: color-mix(in srgb, var(--text) 78%, transparent); font-size: var(--fs-xs, 0.75rem); }
|
|
.eps__note { font-style: italic; }
|
|
.eps__item--muet .eps__titre { color: color-mix(in srgb, var(--text) 78%, transparent); }
|
|
</style>
|