feat(outils): sélection open source, sommaire d'ancres et garde-fous (B6-M4)

28 outils vérifiés en 4 rubriques (open_source.selection), affichés en premier
sous un sommaire d'ancres ; grille 2 colonnes dès 1024 px. Le test contrôle
la sélection (champs, https, liste noire) et les doublons d'URL.

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 17:07:37 +02:00
co-authored by Claude Sonnet
parent dfbac7dd12
commit 43ae065e26
3 changed files with 578 additions and 7 deletions
+131 -6
View File
@@ -95,8 +95,38 @@
<template v-else-if="tab === 'open-source'">
<p class="outils-intention">{{ outils.open_source.intention }}</p>
<section class="outils-section" aria-labelledby="sec-recommandes">
<h2 id="sec-recommandes" class="outils-section__title">Recommandés</h2>
<!-- Sommaire d'ancres : tout est déployé d'office, on saute au bloc voulu -->
<nav class="os-sommaire" aria-label="Sommaire de l'onglet Open source">
<ul class="os-sommaire__liste">
<li v-for="a in sommaire" :key="a.id">
<a :href="'#' + a.id" class="os-sommaire__lien" @click="allerAncre($event, a.id)">{{ a.libelle }}</a>
</li>
</ul>
</nav>
<!-- Sélection AEP : outils utiles à un architecte indépendant (B6-M3/M4) -->
<template v-if="outils.open_source.selection">
<p class="outils-section__desc">{{ outils.open_source.selection.intention }}</p>
<section
v-for="r in outils.open_source.selection.rubriques"
:key="r.id"
class="outils-section outils-section--ancre"
:aria-labelledby="'sel-' + r.id"
>
<h2 :id="'sel-' + r.id" class="outils-section__title" tabindex="-1">{{ r.titre }}</h2>
<p v-if="r.note" class="outils-section__desc">{{ r.note }}</p>
<ul class="sel-grid">
<li v-for="l in r.liens" :key="l.url" class="sel-item">
<a :href="l.url" target="_blank" rel="noopener noreferrer" class="sel-item__nom">{{ l.nom }}</a>
<p class="sel-item__desc">{{ l.desc }}</p>
<p class="sel-item__meta">{{ metaSelection(l) }}</p>
</li>
</ul>
</section>
</template>
<section class="outils-section outils-section--ancre" aria-labelledby="sec-recommandes">
<h2 id="sec-recommandes" class="outils-section__title" tabindex="-1">Nos recommandations</h2>
<div class="outil-cards-grid">
<OutilCard
v-for="outil in outils.open_source.recommandes"
@@ -110,8 +140,8 @@
</div>
</section>
<section class="outils-section outils-section--fmhy" aria-labelledby="sec-fmhy">
<h2 id="sec-fmhy" class="outils-section__title">{{ outils.open_source.fmhy.titre }}</h2>
<section class="outils-section outils-section--fmhy outils-section--ancre" aria-labelledby="sec-fmhy">
<h2 id="sec-fmhy" class="outils-section__title" tabindex="-1">{{ outils.open_source.fmhy.titre }}</h2>
<p class="outils-section__desc">{{ outils.open_source.fmhy.texte_intro }}</p>
<div v-for="g in outils.open_source.rubriques" :key="g.id" class="fmhy-usage">
@@ -155,10 +185,10 @@
</div>
</section>
<section v-if="outils.open_source.logiciels_pro" class="outils-section outils-section--placeholder" aria-labelledby="sec-logiciels">
<section v-if="outils.open_source.logiciels_pro" class="outils-section outils-section--placeholder outils-section--ancre" aria-labelledby="sec-logiciels">
<div class="placeholder-block">
<span class="placeholder-block__badge">{{ outils.open_source.logiciels_pro.badge }}</span>
<h2 id="sec-logiciels" class="placeholder-block__title">{{ outils.open_source.logiciels_pro.titre }}</h2>
<h2 id="sec-logiciels" class="placeholder-block__title" tabindex="-1">{{ outils.open_source.logiciels_pro.titre }}</h2>
<p class="placeholder-block__desc">{{ outils.open_source.logiciels_pro.description }}</p>
</div>
</section>
@@ -220,6 +250,31 @@ function onTabKeydown(e: KeyboardEvent) {
nextTick(() => tablistRef.value?.querySelector<HTMLElement>(`#outils-tab-${TABS[j].id}`)?.focus())
}
// Open source : sommaire d'ancres = rubriques de la sélection + les trois autres blocs.
const sommaire = computed(() => {
const os = outils.value?.open_source
if (!os) return []
return [
...(os.selection?.rubriques ?? []).map((r: any) => ({ id: `sel-${r.id}`, libelle: r.titre })),
{ id: 'sec-recommandes', libelle: 'Nos recommandations' },
{ id: 'sec-fmhy', libelle: 'Annuaire fmhy' },
...(os.logiciels_pro ? [{ id: 'sec-logiciels', libelle: 'Logiciels pro' }] : []),
]
})
const metaSelection = (l: any) => [l.licence, (l.plateformes ?? []).join(', ')].filter(Boolean).join(' · ')
// Saut vers une ancre : défilement doux sauf préférence « moins d'animations », focus sur le titre
// (clavier et lecteurs d'écran arrivent au bon endroit). L'URL ne bouge pas (?tab= intact).
function allerAncre(e: Event, id: string) {
e.preventDefault()
const el = document.getElementById(id)
if (!el) return
const reduit = window.matchMedia('(prefers-reduced-motion: reduce)').matches
el.scrollIntoView({ behavior: reduit ? 'auto' : 'smooth', block: 'start' })
el.focus({ preventScroll: true })
}
useSeoMeta({
title: () => `Outils — AEP${tab.value !== TAB_DEFAUT ? ' — ' + TABS.find((t) => t.id === tab.value)!.libelle : ''}`,
description: 'Outils partagés entre architectes : simulateurs, open source, ressources libres pour partir de soi.',
@@ -397,6 +452,76 @@ useSeoMeta({
gap: var(--space-md);
}
/* ── Open source : sommaire d'ancres et sélection ───────────── */
.outils-section--ancre { scroll-margin-top: var(--space-lg); }
.outils-section--ancre :is(h2):focus { outline: none; }
.outils-section--ancre :is(h2):focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.os-sommaire__liste {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: var(--space-xs) var(--space-sm);
}
.os-sommaire__lien {
display: inline-block;
padding: var(--space-xs) var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg-alt);
color: var(--text);
font-size: 0.85rem;
font-weight: var(--fw-medium);
text-decoration: none;
}
.os-sommaire__lien:hover { border-color: var(--nav-primary-solid); }
.os-sommaire__lien:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.sel-grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: 1fr;
gap: var(--space-md);
}
@media (min-width: 1024px) {
.sel-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.sel-item {
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
padding: var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
}
.sel-item__nom {
align-self: flex-start;
font-size: 0.95rem;
font-weight: var(--fw-semibold);
color: var(--nav-primary-solid);
text-underline-offset: 2px;
overflow-wrap: anywhere;
}
.sel-item__nom:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
.sel-item__desc {
margin: 0;
font-size: 0.85rem;
line-height: 1.55;
color: var(--text);
}
.sel-item__meta {
margin: auto 0 0;
padding-top: 0.2rem;
font-size: 0.78rem;
color: color-mix(in srgb, var(--text) 78%, transparent);
}
/* ── Section FMHY ───────────────────────────────────────────── */
.outils-section--fmhy {
background: var(--bg-alt);