6 Commits

Author SHA1 Message Date
Jules Neny
d584d04e3d merge(feat/outils-v1): page Outils V1 + bibliothèque pensées écologiques refonte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:52:42 +02:00
Jules Neny
bd95c0f00d fix(media): RAG visible + refonte interface bibliothèque pensées écologiques
- fix: penseesData chargé en interne dans MediaTabVisuel (bug prop jamais passée)
- feat: onglet renommé '📚 bibliothèque des pensées écologiques', suppression tab LightRAG backend
- feat: 'RAG backend' devient bouton inline dans toolbar → layout mode 'rag-backend'
- feat: fusion boutons 'Bonpote V2' + 'Carte FRACAS PDF' → contrôle unique avec tickbox intégré
- feat: iframe lightrag.trans-former.fr décommentée (DNS propagé)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:52:20 +02:00
Jules Neny
db8f614928 fix(CartePensees): reformatage syntaxe - fichier condensé sur 1 ligne cassait le build Vite/Vue 2026-05-22 11:07:10 +02:00
Jules Neny
59cb81a055 Merge P3 media modif 2026-05-22 11:02:05 +02:00
Jules Neny
91e3466ec6 Merge P2 outils build 2026-05-22 11:01:30 +02:00
Jules Neny
422f45116f feat(outils): page Outils V1 + composants TreeASCII/OutilCard/SimulateurFeature + nav premier onglet 2026-05-22 10:58:39 +02:00
17 changed files with 1665 additions and 180 deletions

View File

@@ -22,6 +22,13 @@
<!-- ── Onglets desktop (≥1024px) — remplace la barre de recherche ── --> <!-- ── Onglets desktop (≥1024px) — remplace la barre de recherche ── -->
<nav class="hidden lg:flex flex-1 justify-center items-end gap-0 mx-6" aria-label="Navigation projets"> <nav class="hidden lg:flex flex-1 justify-center items-end gap-0 mx-6" aria-label="Navigation projets">
<NuxtLink
to="/outils"
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/outils' }"
>
Outils
</NuxtLink>
<NuxtLink <NuxtLink
to="/" to="/"
class="nav-tab" class="nav-tab"
@@ -172,6 +179,7 @@
style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;" style="background: var(--nav-surface); border: 1px solid var(--nav-bg-alt); z-index: 9999;"
@click="hamburgerOpen = false" @click="hamburgerOpen = false"
> >
<NuxtLink to="/outils" class="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="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 Architecture</NuxtLink> <NuxtLink to="/" class="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 Architecture</NuxtLink>
<NuxtLink to="/agences" class="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="/agences" class="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="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="/trouver-du-taf" class="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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,16 +8,6 @@
</p> </p>
</div> </div>
<!-- PLACEHOLDER — DNS en attente
TODO: Décommenter iframe + supprimer placeholder une fois lightrag.trans-former.fr propagé.
DNS A record à créer sur OVH : lightrag → 178.104.106.195 TTL 300
-->
<div style="margin-top: 1.5rem; padding: 2rem; border: 2px dashed var(--nav-bg-alt, #ddd); border-radius: 8px; text-align: center; color: var(--nav-text-muted);">
<p style="font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem;">⏳ Backend en cours d'exposition publique bientôt accessible.</p>
<p style="font-size: 0.85rem;">L'interface LightRAG sera disponible ici dès la mise en place du sous-domaine <code>lightrag.trans-former.fr</code>.</p>
</div>
<!--
<iframe <iframe
src="https://lightrag.trans-former.fr/" src="https://lightrag.trans-former.fr/"
style="width: 100%; height: 70vh; border: 1px solid var(--nav-bg-alt, #ddd); border-radius: 8px; margin-top: 1.5rem;" style="width: 100%; height: 70vh; border: 1px solid var(--nav-bg-alt, #ddd); border-radius: 8px; margin-top: 1.5rem;"
@@ -25,6 +15,5 @@
sandbox="allow-same-origin allow-scripts" sandbox="allow-same-origin allow-scripts"
loading="lazy" loading="lazy"
/> />
-->
</div> </div>
</template> </template>

View File

@@ -10,7 +10,7 @@
:class="[ :class="[
layoutMode === 'split' ? 'carte-split' : '', layoutMode === 'split' ? 'carte-split' : '',
layoutMode === 'carte-full' ? 'carte-full' : '', layoutMode === 'carte-full' ? 'carte-full' : '',
layoutMode === 'chatbot-full' ? 'carte-hidden' : '', (layoutMode === 'chatbot-full' || layoutMode === 'bonpote' || layoutMode === 'rag-backend') ? 'carte-hidden' : '',
]" ]"
:style="layoutMode === 'split' ? { flexBasis: carteFlexBasis } : {}" :style="layoutMode === 'split' ? { flexBasis: carteFlexBasis } : {}"
style="position: relative;" style="position: relative;"
@@ -80,33 +80,46 @@
</svg> </svg>
Chatbot plein ecran Chatbot plein ecran
</button> </button>
<button
@click="setLayoutMode('bonpote')"
:class="{ active: layoutMode === 'bonpote' }"
class="toggle-btn"
title="A propos de la carte FRACAS Bonpote V2"
style="margin-left: auto;"
>
<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">
<circle cx="12" cy="12" r="10"/><polyline points="12 8 12 12 14 14"/>
</svg>
Bonpote V2
</button>
<!-- Toggle PDF FRACAS --> <!-- Groupe droit : carte des pensées + RAG backend -->
<label class="layer-toggle" title="Superposer la carte FRACAS Bonpote V2 en PDF"> <div style="margin-left: auto; display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
<input type="checkbox" v-model="showFracasPdf" /> <input
📄 Carte FRACAS (PDF) v-if="showFracasPdf"
</label> type="range"
<input min="0"
v-if="showFracasPdf" max="100"
type="range" v-model.number="fracasOpacity"
min="0" class="opacity-slider"
max="100" :title="`Opacité ${fracasOpacity}%`"
v-model.number="fracasOpacity" />
class="opacity-slider" <div class="carte-pensees-ctrl">
:title="`Opacité ${fracasOpacity}%`" <input
/> type="checkbox"
v-model="showFracasPdf"
class="fracas-check"
title="Superposer la carte FRACAS en PDF"
/>
<button
@click="setLayoutMode('bonpote')"
:class="{ active: layoutMode === 'bonpote' }"
class="toggle-btn carte-pensees-btn"
title="Carte des pensées écologiques — référence FRACAS Bonpote V2"
>
📗 carte des pensées écologiques
</button>
</div>
<button
@click="setLayoutMode('rag-backend')"
:class="{ active: layoutMode === 'rag-backend' }"
class="toggle-btn"
title="Interface LightRAG backend"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/>
</svg>
RAG backend
</button>
</div>
</div> </div>
<!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) --> <!-- Poignee draggable (visible uniquement en mode split, pas sur mobile) -->
@@ -125,7 +138,7 @@
:class="[ :class="[
layoutMode === 'split' ? 'chatbot-split' : '', layoutMode === 'split' ? 'chatbot-split' : '',
layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '', layoutMode === 'chatbot-full' ? 'chatbot-full-mode' : '',
layoutMode === 'carte-full' ? 'chatbot-hidden' : '', (layoutMode === 'carte-full' || layoutMode === 'bonpote' || layoutMode === 'rag-backend') ? 'chatbot-hidden' : '',
]" ]"
:style="layoutMode === 'split' ? { flexBasis: chatbotFlexBasis } : {}" :style="layoutMode === 'split' ? { flexBasis: chatbotFlexBasis } : {}"
> >
@@ -189,6 +202,14 @@
</div> </div>
</div> </div>
<!-- Vue RAG backend -->
<div
v-if="layoutMode === 'rag-backend'"
style="flex: 1; overflow: hidden; display: flex; flex-direction: column;"
>
<MediaTabBackend />
</div>
</div> </div>
<!-- Fiche auteur modal --> <!-- Fiche auteur modal -->
@@ -264,7 +285,7 @@ interface LivreRag { slug: string; titre: string; annee: number; couches: string
interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string } interface AuteurData { id: string; nom: string; dates: string; ecoles: string[]; ecole_principale: string; livres_rag: LivreRag[]; theses_cles: string[]; bio_courte: string }
interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] } interface PenseesData { meta: any; ecoles: EcoleData[]; auteurs: AuteurData[] }
type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote' type LayoutMode = 'split' | 'carte-full' | 'chatbot-full' | 'bonpote' | 'rag-backend'
const STORAGE_KEY = 'media-layout-mode' const STORAGE_KEY = 'media-layout-mode'
const SPLIT_RATIO_KEY = 'media-split-ratio' const SPLIT_RATIO_KEY = 'media-split-ratio'
@@ -279,19 +300,15 @@ const chatbotAuteur = ref<string | null>(null)
const layoutMode = ref<LayoutMode>('split') const layoutMode = ref<LayoutMode>('split')
const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null) const cartePenseesRef = ref<{ triggerResize: () => void } | null>(null)
// Toggle PDF FRACAS
const showFracasPdf = ref(false) const showFracasPdf = ref(false)
const fracasOpacity = ref(60) const fracasOpacity = ref(60)
// Props injectées depuis le parent (penseesData) const penseesData = ref<PenseesData | null>(null)
const props = defineProps<{ penseesData: PenseesData | null }>()
// Ratio de la carte vs chatbot en mode split (0.2 a 0.8)
const splitRatio = ref(DEFAULT_SPLIT_RATIO) const splitRatio = ref(DEFAULT_SPLIT_RATIO)
const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`) const carteFlexBasis = computed(() => `${splitRatio.value * 100}%`)
const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`) const chatbotFlexBasis = computed(() => `${(1 - splitRatio.value) * 100}%`)
// Logique poignee draggable
let dragStartY = 0 let dragStartY = 0
let dragStartRatio = DEFAULT_SPLIT_RATIO let dragStartRatio = DEFAULT_SPLIT_RATIO
let containerHeight = 0 let containerHeight = 0
@@ -321,10 +338,10 @@ function onHandleMouseup() {
cartePenseesRef.value?.triggerResize() cartePenseesRef.value?.triggerResize()
} }
onMounted(() => { onMounted(async () => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null const saved = localStorage.getItem(STORAGE_KEY) as LayoutMode | null
if (saved && ['split', 'carte-full', 'chatbot-full', 'bonpote'].includes(saved)) { if (saved && (['split', 'carte-full', 'chatbot-full', 'bonpote', 'rag-backend'] as string[]).includes(saved)) {
layoutMode.value = saved layoutMode.value = saved
} }
const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '') const savedRatio = parseFloat(localStorage.getItem(SPLIT_RATIO_KEY) ?? '')
@@ -336,6 +353,11 @@ onMounted(() => {
localStorage.setItem('rag-fracas-info-seen', '1') localStorage.setItem('rag-fracas-info-seen', '1')
} }
} }
try {
penseesData.value = await $fetch<PenseesData>('/data/auteurs-pensees.json?v=4.2')
} catch (e) {
console.error('Erreur chargement auteurs-pensees.json', e)
}
}) })
function setLayoutMode(mode: LayoutMode) { function setLayoutMode(mode: LayoutMode) {
@@ -343,7 +365,7 @@ function setLayoutMode(mode: LayoutMode) {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, mode) localStorage.setItem(STORAGE_KEY, mode)
} }
if (mode !== 'chatbot-full') { if (mode === 'split' || mode === 'carte-full') {
setTimeout(() => { setTimeout(() => {
cartePenseesRef.value?.triggerResize() cartePenseesRef.value?.triggerResize()
}, 350) }, 350)
@@ -374,14 +396,14 @@ function onSelectAuteurFromEcole(auteurId: string) {
function onInterrogerEcole(ecoleId: string) { function onInterrogerEcole(ecoleId: string) {
ficheEcoleOpen.value = false ficheEcoleOpen.value = false
const ecole = props.penseesData?.ecoles.find(e => e.id === ecoleId) const ecole = penseesData.value?.ecoles.find(e => e.id === ecoleId)
chatbotAuteur.value = ecole?.label ?? null chatbotAuteur.value = ecole?.label ?? null
if (layoutMode.value === 'carte-full') setLayoutMode('split') if (layoutMode.value === 'carte-full') setLayoutMode('split')
} }
function onInterrogerRag(auteurId: string) { function onInterrogerRag(auteurId: string) {
ficheOpen.value = false ficheOpen.value = false
const auteur = props.penseesData?.auteurs.find(a => a.id === auteurId) const auteur = penseesData.value?.auteurs.find(a => a.id === auteurId)
chatbotAuteur.value = auteur?.nom ?? null chatbotAuteur.value = auteur?.nom ?? null
if (layoutMode.value === 'carte-full') { if (layoutMode.value === 'carte-full') {
setLayoutMode('split') setLayoutMode('split')
@@ -481,28 +503,28 @@ function onInterrogerRag(auteurId: string) {
border-color: var(--nav-primary); border-color: var(--nav-primary);
} }
/* --- Toggle layer PDF FRACAS --- */ /* --- Contrôle fusionné carte des pensées + tickbox --- */
.layer-toggle { .carte-pensees-ctrl {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 0;
padding: 4px 10px;
border-radius: 6px; border-radius: 6px;
font-size: 0.75rem; overflow: hidden;
font-weight: 500; border: 1px solid rgba(180, 170, 160, 0.3);
cursor: pointer;
background: var(--nav-bg-alt);
color: var(--nav-text-muted);
border: 1px solid transparent;
user-select: none;
margin-left: 4px;
} }
.layer-toggle input[type="checkbox"] { .fracas-check {
margin: 0; margin: 0 2px 0 7px;
cursor: pointer; cursor: pointer;
accent-color: var(--nav-primary, #3b6ea5);
} }
.carte-pensees-btn {
border-radius: 0;
border: none;
}
/* --- Slider opacité PDF --- */
.opacity-slider { .opacity-slider {
width: 80px; width: 80px;
cursor: pointer; cursor: pointer;

150
components/OutilCard.vue Normal file
View File

@@ -0,0 +1,150 @@
<template>
<component
:is="url ? 'a' : 'div'"
v-bind="url ? { href: url, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="outil-card"
:class="{ 'outil-card--link': !!url, 'outil-card--disabled': !url }"
>
<div class="outil-card__header">
<span class="outil-card__icon" aria-hidden="true">{{ icon }}</span>
<span :class="['outil-card__badge', `outil-card__badge--${tag}`]">{{ tagLabel }}</span>
</div>
<h3 class="outil-card__titre">{{ titre }}</h3>
<p class="outil-card__desc">{{ description }}</p>
<span v-if="cta && url" class="outil-card__cta">{{ cta }}</span>
<span v-else-if="!url" class="outil-card__cta outil-card__cta--disabled">Bientôt disponible</span>
</component>
</template>
<script setup lang="ts">
const props = defineProps<{
icon?: string
titre: string
url?: string | null
description?: string
cta?: string
tag?: string
}>()
const tagLabels: Record<string, string> = {
'outil-aep': 'Outil AEP',
'inspiration-externe': 'Inspiration',
'disponible': 'Disponible',
'recommande': 'Recommandé',
'a-venir': 'À venir',
}
const tagLabel = computed(() => props.tag ? (tagLabels[props.tag] ?? props.tag) : '')
</script>
<style scoped>
.outil-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.25rem;
border-radius: 10px;
border: 1px solid var(--nav-bg-alt);
background: var(--nav-surface);
text-decoration: none;
color: var(--nav-text);
transition: box-shadow 0.15s, border-color 0.15s;
}
.outil-card--link:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border-color: var(--nav-primary-solid);
}
.outil-card--disabled {
opacity: 0.65;
}
.outil-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.outil-card__icon {
font-size: 1.3rem;
line-height: 1;
}
.outil-card__badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 7px;
border-radius: 999px;
}
.outil-card__badge--outil-aep {
background: #d1fae5;
color: #065f46;
}
.outil-card__badge--inspiration-externe {
background: #fef3c7;
color: #92400e;
}
.outil-card__badge--disponible {
background: #d1fae5;
color: #065f46;
}
.outil-card__badge--recommande {
background: #dbeafe;
color: #1e40af;
}
.outil-card__badge--a-venir {
background: var(--nav-bg-alt);
color: var(--nav-text-muted);
}
.outil-card__titre {
font-size: 0.9rem;
font-weight: 600;
color: var(--nav-text);
margin: 0;
line-height: 1.35;
}
.outil-card__desc {
font-size: 0.82rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.5;
}
.outil-card__cta {
font-size: 0.78rem;
font-weight: 600;
color: var(--nav-primary-solid);
margin-top: 0.25rem;
}
.outil-card__cta--disabled {
color: var(--nav-text-muted);
font-weight: 400;
font-style: italic;
}
/* Dark mode badge overrides */
:global(.dark) .outil-card__badge--outil-aep {
background: #064e3b;
color: #a7f3d0;
}
:global(.dark) .outil-card__badge--inspiration-externe {
background: #78350f;
color: #fde68a;
}
:global(.dark) .outil-card__badge--disponible {
background: #064e3b;
color: #a7f3d0;
}
:global(.dark) .outil-card__badge--recommande {
background: #1e3a5f;
color: #93c5fd;
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<component
:is="url ? 'a' : 'div'"
v-bind="url ? { href: url, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="simu-feature"
:class="{ 'simu-feature--link': !!url }"
>
<div class="simu-feature__inner">
<div class="simu-feature__left">
<span class="simu-feature__icon" aria-hidden="true">{{ icon }}</span>
<div class="simu-feature__body">
<div class="simu-feature__header">
<h3 class="simu-feature__titre">{{ titre }}</h3>
<span v-if="tag" :class="['simu-feature__badge', `simu-feature__badge--${tag}`]">{{ tagLabel }}</span>
</div>
<p class="simu-feature__desc">{{ description }}</p>
</div>
</div>
<span v-if="cta && url" class="simu-feature__cta">{{ cta }}</span>
</div>
</component>
</template>
<script setup lang="ts">
const props = defineProps<{
icon?: string
titre: string
url?: string | null
description?: string
cta?: string
tag?: string
}>()
const tagLabels: Record<string, string> = {
'outil-aep': 'Outil AEP',
'inspiration-externe': 'Inspiration externe',
'disponible': 'Disponible',
'recommande': 'Recommandé',
'a-venir': 'À venir',
}
const tagLabel = computed(() => props.tag ? (tagLabels[props.tag] ?? props.tag) : '')
</script>
<style scoped>
.simu-feature {
display: block;
padding: 1.5rem 1.75rem;
border-radius: 14px;
border: 1.5px solid var(--nav-bg-alt);
background: var(--nav-surface);
text-decoration: none;
color: var(--nav-text);
transition: box-shadow 0.2s, border-color 0.2s, transform 0.15s;
}
.simu-feature--link:hover {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border-color: var(--nav-primary-solid);
transform: translateY(-2px);
}
.simu-feature__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
}
.simu-feature__left {
display: flex;
align-items: flex-start;
gap: 1rem;
flex: 1;
min-width: 0;
}
.simu-feature__icon {
font-size: 2rem;
line-height: 1;
flex-shrink: 0;
margin-top: 2px;
}
.simu-feature__body {
flex: 1;
min-width: 0;
}
.simu-feature__header {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 0.4rem;
}
.simu-feature__titre {
font-size: 1.05rem;
font-weight: 700;
color: var(--nav-text);
margin: 0;
line-height: 1.3;
}
.simu-feature__badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 8px;
border-radius: 999px;
background: #d1fae5;
color: #065f46;
}
.simu-feature__badge--inspiration-externe {
background: #fef3c7;
color: #92400e;
}
.simu-feature__desc {
font-size: 0.88rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.55;
}
.simu-feature__cta {
display: inline-flex;
align-items: center;
padding: 0.6rem 1.25rem;
background: var(--nav-primary-solid);
color: var(--nav-text-on-primary);
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
transition: opacity 0.15s;
flex-shrink: 0;
}
.simu-feature--link:hover .simu-feature__cta {
opacity: 0.88;
}
:global(.dark) .simu-feature__badge {
background: #064e3b;
color: #a7f3d0;
}
:global(.dark) .simu-feature__badge--inspiration-externe {
background: #78350f;
color: #fde68a;
}
</style>

201
components/TreeASCII.vue Normal file
View File

@@ -0,0 +1,201 @@
<template>
<ul class="tree-ascii" :class="{ 'tree-ascii--root': depth === 0 }" :style="{ '--depth': depth }">
<li
v-for="(node, i) in tree.children"
:key="i"
class="tree-ascii__node"
>
<!-- Nœud avec enfants : bouton toggle -->
<template v-if="node.children && node.children.length">
<button
class="tree-ascii__branch"
:aria-expanded="!!open[i]"
@click="toggle(i)"
>
<span class="tree-ascii__chevron" aria-hidden="true">{{ open[i] ? '▼' : '▶' }}</span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span class="tree-ascii__count">({{ node.children.length }})</span>
</button>
<TreeASCII
v-if="open[i]"
:tree="node"
:depth="depth + 1"
/>
</template>
<!-- Feuille avec URL : lien cliquable -->
<template v-else-if="node.url">
<a
:href="node.url"
target="_blank"
rel="noopener noreferrer"
class="tree-ascii__leaf tree-ascii__leaf--link"
>
<span class="tree-ascii__prefix" aria-hidden="true"></span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span v-if="node.desc" class="tree-ascii__desc"> {{ node.desc }}</span>
<svg class="tree-ascii__ext" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
</a>
</template>
<!-- Feuille sans URL -->
<template v-else>
<span class="tree-ascii__leaf">
<span class="tree-ascii__prefix" aria-hidden="true"></span>
<span class="tree-ascii__label">{{ node.name }}</span>
<span v-if="node.desc" class="tree-ascii__desc"> {{ node.desc }}</span>
</span>
</template>
</li>
</ul>
</template>
<script setup lang="ts">
export interface TreeNode {
name: string
url?: string
desc?: string
children?: TreeNode[]
}
export interface TreeData {
name?: string
children?: TreeNode[]
}
const props = withDefaults(defineProps<{
tree: TreeData
depth?: number
}>(), {
depth: 0
})
// Toutes les branches fermées par défaut
const open = ref<Record<number, boolean>>({})
function toggle(i: number) {
open.value[i] = !open.value[i]
}
</script>
<style scoped>
.tree-ascii {
list-style: none;
padding: 0;
margin: 0;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.82rem;
padding-left: calc(var(--depth, 0) * 1.25rem + 0.5rem);
}
.tree-ascii--root {
padding-left: 0;
}
.tree-ascii__node {
margin: 2px 0;
line-height: 1.6;
}
/* Bouton branche (nœud avec enfants) */
.tree-ascii__branch {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: none;
border: none;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
color: var(--nav-text);
font-family: inherit;
font-size: inherit;
font-weight: 600;
transition: background 0.1s, color 0.1s;
text-align: left;
}
.tree-ascii__branch:hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
}
.tree-ascii__chevron {
font-size: 0.65rem;
color: var(--nav-text-muted);
width: 12px;
text-align: center;
flex-shrink: 0;
}
.tree-ascii__count {
font-size: 0.7rem;
color: var(--nav-text-muted);
font-weight: 400;
}
/* Feuille */
.tree-ascii__leaf {
display: inline-flex;
align-items: baseline;
gap: 0.25rem;
padding: 1px 6px;
border-radius: 4px;
text-decoration: none;
color: var(--nav-text-muted);
}
.tree-ascii__leaf--link {
color: var(--nav-text);
cursor: pointer;
transition: background 0.1s, color 0.1s;
}
.tree-ascii__leaf--link:hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
text-decoration: underline;
}
.tree-ascii__prefix {
color: var(--nav-text-muted);
opacity: 0.5;
font-size: 0.75rem;
flex-shrink: 0;
}
.tree-ascii__label {
font-weight: 500;
}
.tree-ascii__leaf--link .tree-ascii__label {
font-weight: 400;
}
.tree-ascii__desc {
color: var(--nav-text-muted);
font-size: 0.78rem;
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60ch;
}
.tree-ascii__ext {
opacity: 0.4;
flex-shrink: 0;
margin-left: 2px;
vertical-align: middle;
}
@media (max-width: 640px) {
.tree-ascii__desc {
display: none;
}
}
</style>

View File

@@ -5,24 +5,17 @@
:class="['subtab-btn', { active: tab === 'visuel' }]" :class="['subtab-btn', { active: tab === 'visuel' }]"
@click="tab = 'visuel'" @click="tab = 'visuel'"
> >
🌳 RAG visuel 📚 bibliothèque des pensées écologiques
</button>
<button
:class="['subtab-btn', { active: tab === 'backend' }]"
@click="tab = 'backend'"
>
LightRAG backend
</button> </button>
<button <button
:class="['subtab-btn', { active: tab === 'projets' }]" :class="['subtab-btn', { active: tab === 'projets' }]"
@click="tab = 'projets'" @click="tab = 'projets'"
> >
📚 Projets 📐 Projets
</button> </button>
</nav> </nav>
<MediaTabVisuel v-if="tab === 'visuel'" /> <MediaTabVisuel v-if="tab === 'visuel'" />
<MediaTabBackend v-else-if="tab === 'backend'" />
<MediaTabProjets v-else-if="tab === 'projets'" /> <MediaTabProjets v-else-if="tab === 'projets'" />
</div> </div>
</template> </template>
@@ -31,9 +24,9 @@
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const tab = ref<'visuel' | 'backend' | 'projets'>( const tab = ref<'visuel' | 'projets'>(
(['visuel', 'backend', 'projets'].includes(route.query.tab as string) (['visuel', 'projets'].includes(route.query.tab as string)
? route.query.tab as 'visuel' | 'backend' | 'projets' ? route.query.tab as 'visuel' | 'projets'
: 'visuel') : 'visuel')
) )

533
pages/outils.vue Normal file
View File

@@ -0,0 +1,533 @@
<template>
<div class="outils-page">
<!-- EN-TÊTE PAGE -->
<header class="outils-header">
<div class="outils-header__inner">
<div class="outils-header__icon-wrap" aria-hidden="true">
<img src="/icons/outils-wrench.svg" alt="" class="outils-header__icon" />
</div>
<div>
<h1 class="outils-header__title">Outils</h1>
<p class="outils-header__intro">
En tant qu'architecte, on jongle avec une multitude d'outils simulation, dessin,
calcul, recherche, partage. Les mutualiser, se conseiller dessus, savoir lequel
utiliser quand : c'est une forme d'entraide concrète. Voici ceux que je propose
dans un premier temps. Chacun peut contribuer pour enrichir cette boîte à outils
commune.
</p>
</div>
</div>
</header>
<main class="outils-main">
<!-- SECTION 1 Simulateurs métier -->
<section class="outils-section outils-section--simulateurs" aria-labelledby="sec-simulateurs">
<h2 id="sec-simulateurs" class="outils-section__title">
<span aria-hidden="true">🧮</span> Simulateurs métier
</h2>
<p class="outils-section__subtitle">Créés par AEP outils de calcul situés.</p>
<div class="simu-grid">
<SimulateurFeature
v-for="s in outils?.simulateurs"
:key="s.id"
:icon="s.icon"
:titre="s.titre"
:url="s.url"
:description="s.description"
:cta="s.cta"
:tag="s.tag"
/>
</div>
<!-- Inspirations -->
<div v-if="outils?.simulateurs_inspirations?.length" class="simu-inspirations">
<p class="simu-inspirations__label">Inspiration externe</p>
<div class="outil-cards-grid">
<OutilCard
v-for="s in outils.simulateurs_inspirations"
:key="s.id"
:icon="s.icon"
:titre="s.titre"
:url="s.url"
:description="s.description"
:tag="s.tag"
/>
</div>
</div>
</section>
<!-- SECTION 2 Open source recommandés -->
<section class="outils-section outils-section--opensource" aria-labelledby="sec-opensource">
<h2 id="sec-opensource" class="outils-section__title">
<span aria-hidden="true">🔧</span> Outils tech open source
</h2>
<p class="outils-section__subtitle">Quelques recommandations directes. Le cœur de l'onglet, c'est la section FMHY plus bas.</p>
<div class="outil-cards-grid">
<OutilCard
v-for="outil in outils?.opensource"
:key="outil.id"
:icon="outil.icon"
:titre="outil.titre"
:url="outil.url"
:description="outil.description"
:tag="outil.tag"
/>
</div>
</section>
<!-- SECTION 3 Bifurcation -->
<section class="outils-section" aria-labelledby="sec-bifurcation">
<h2 id="sec-bifurcation" class="outils-section__title">
<span aria-hidden="true">🌿</span> Bifurcation post-études d'archi
</h2>
<p class="outils-section__desc">
{{ outils?.bifurcation?.intro }}
</p>
<!-- 3.1 Vidéos OFQA -->
<div class="bifurcation-block">
<h3 class="bifurcation-block__title">Série vidéo OFQA / ENSA-PB</h3>
<ul class="ofqa-list">
<li
v-for="ep in outils?.bifurcation?.videos_ofqa"
: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>
</div>
<!-- 3.2 Coalition -->
<div v-if="outils?.bifurcation?.coalition_ensa_pb" class="bifurcation-block bifurcation-block--coalition">
<h3 class="bifurcation-block__title">{{ outils.bifurcation.coalition_ensa_pb.titre }}</h3>
<p class="bifurcation-block__desc">{{ outils.bifurcation.coalition_ensa_pb.description }}</p>
</div>
<!-- 3.3 Ressources externes -->
<div v-if="outils?.bifurcation?.ressources_externes?.length" class="bifurcation-block">
<h3 class="bifurcation-block__title">Ressources externes</h3>
<div class="outil-cards-grid">
<OutilCard
v-for="r in outils.bifurcation.ressources_externes"
:key="r.id"
:icon="r.icon"
:titre="r.titre"
:url="r.url"
:description="r.description"
/>
</div>
</div>
</section>
<!-- ══════════════ SECTION 4 — FMHY (cœur de la page) ══════════════ -->
<section class="outils-section outils-section--fmhy" aria-labelledby="sec-fmhy">
<h2 id="sec-fmhy" class="outils-section__title">
<span aria-hidden="true">🌳</span> Bibliothèque de ressources libres
</h2>
<p class="outils-section__desc">
Le vrai trésor de l'onglet Outils. FMHY (Free Media Heck Yeah) est la plus grosse
base communautaire d'outils, services et ressources libres/gratuits du web. J'en ai
curé ~50 entrées pertinentes pour un architecte : IA, lecture, dev, vie privée,
formation, médias. Clique sur les branches pour explorer.
</p>
<div class="fmhy-tree-wrap">
<div v-if="fmhyPending" class="fmhy-loading" aria-label="Chargement…">
<span>Chargement</span>
</div>
<div v-else-if="fmhyError" class="fmhy-error">
Impossible de charger les ressources. <a href="https://fmhy.net/" target="_blank" rel="noopener noreferrer">Explorer fmhy.net </a>
</div>
<TreeASCII v-else-if="fmhyData" :tree="fmhyData" :depth="0" />
</div>
<div class="fmhy-footer">
<a href="https://fmhy.net/" target="_blank" rel="noopener noreferrer" class="fmhy-footer__link">
Explorer tout l'arbre → fmhy.net
</a>
</div>
</section>
<!-- ══════════════ SECTION 5 — Placeholder login ══════════════ -->
<section class="outils-section outils-section--placeholder" aria-labelledby="sec-logiciels">
<div class="placeholder-block">
<span class="placeholder-block__badge">Bientôt — nécessite un compte</span>
<h2 id="sec-logiciels" class="placeholder-block__title">{{ outils?.section_5_placeholder?.titre }}</h2>
<p class="placeholder-block__desc">{{ outils?.section_5_placeholder?.description }}</p>
</div>
</section>
<!-- ══════════════ FOOTER CONTRIBUTION ══════════════ -->
<footer class="outils-footer">
<p class="outils-footer__text">
{{ outils?.footer_contribution }}
</p>
</footer>
</main>
</div>
</template>
<script setup lang="ts">
// Chargement des données
const { data: outils } = await useFetch('/data/outils.json')
const { data: fmhyData, pending: fmhyPending, error: fmhyError } = await useFetch('/data/fmhy-curated.json')
useSeoMeta({
title: 'Outils AEP',
description: 'Outils partagés entre architectes : simulateurs, open source, ressources libres FMHY, bifurcation post-études.'
})
</script>
<style scoped>
/* ── Layout global ──────────────────────────────────────────── */
.outils-page {
max-width: 860px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
color: var(--nav-text);
}
/* ── Header ─────────────────────────────────────────────────── */
.outils-header {
margin-bottom: 2.5rem;
}
.outils-header__inner {
display: flex;
align-items: flex-start;
gap: 1.25rem;
}
.outils-header__icon-wrap {
width: 3rem;
height: 3rem;
border-radius: 10px;
background: var(--nav-bg-alt);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0.6rem;
}
.outils-header__icon {
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0.75;
}
.outils-header__title {
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 0.5rem;
color: var(--nav-text);
}
.outils-header__intro {
font-size: 0.9rem;
color: var(--nav-text-muted);
line-height: 1.65;
margin: 0;
max-width: 70ch;
}
/* ── Sections ───────────────────────────────────────────────── */
.outils-main {
display: flex;
flex-direction: column;
gap: 3rem;
}
.outils-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.outils-section__title {
font-size: 1.15rem;
font-weight: 700;
margin: 0;
color: var(--nav-text);
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1.5px solid var(--nav-bg-alt);
}
.outils-section__subtitle {
font-size: 0.82rem;
color: var(--nav-text-muted);
margin: -0.5rem 0 0;
font-style: italic;
}
.outils-section__desc {
font-size: 0.88rem;
color: var(--nav-text-muted);
line-height: 1.65;
margin: 0;
max-width: 72ch;
}
/* ── Simulateurs ────────────────────────────────────────────── */
.outils-section--simulateurs {
gap: 1.25rem;
}
.simu-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.simu-inspirations {
margin-top: 0.5rem;
}
.simu-inspirations__label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--nav-text-muted);
margin-bottom: 0.5rem;
}
/* ── Cards grid ─────────────────────────────────────────────── */
.outil-cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.75rem;
}
/* ── Section FMHY ───────────────────────────────────────────── */
.outils-section--fmhy {
background: var(--nav-bg-alt);
border-radius: 14px;
padding: 1.75rem;
gap: 1.25rem;
margin: 0 -0.25rem;
}
.fmhy-tree-wrap {
background: var(--nav-surface);
border: 1px solid var(--nav-bg-alt);
border-radius: 10px;
padding: 1.25rem 1.5rem;
overflow-x: auto;
}
.fmhy-loading,
.fmhy-error {
font-size: 0.85rem;
color: var(--nav-text-muted);
padding: 0.5rem 0;
}
.fmhy-error a {
color: var(--nav-primary-solid);
}
.fmhy-footer {
text-align: right;
}
.fmhy-footer__link {
font-size: 0.82rem;
font-weight: 600;
color: var(--nav-primary-solid);
text-decoration: none;
transition: opacity 0.15s;
}
.fmhy-footer__link:hover {
opacity: 0.75;
text-decoration: underline;
}
/* ── Bifurcation ────────────────────────────────────────────── */
.bifurcation-block {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.bifurcation-block__title {
font-size: 0.92rem;
font-weight: 600;
color: var(--nav-text);
margin: 0;
}
.bifurcation-block__desc {
font-size: 0.84rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.55;
}
.bifurcation-block--coalition {
background: var(--nav-bg-alt);
border-radius: 8px;
padding: 0.875rem 1rem;
}
/* ── Liste OFQA ─────────────────────────────────────────────── */
.ofqa-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.ofqa-list__item {
display: flex;
}
.ofqa-list__link {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
flex-wrap: wrap;
padding: 3px 6px;
border-radius: 5px;
font-size: 0.84rem;
text-decoration: none;
color: var(--nav-text);
transition: background 0.1s;
}
.ofqa-list__link:not(.ofqa-list__link--disabled):hover {
background: var(--nav-bg-alt);
color: var(--nav-primary-solid);
text-decoration: underline;
}
.ofqa-list__link--disabled {
color: var(--nav-text-muted);
cursor: default;
}
.ofqa-list__ep {
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.75rem;
font-weight: 600;
color: var(--nav-text-muted);
flex-shrink: 0;
min-width: 4.5rem;
}
.ofqa-list__titre {
font-weight: 500;
}
.ofqa-list__personnes {
color: var(--nav-text-muted);
font-size: 0.82rem;
}
.ofqa-list__note {
color: var(--nav-text-muted);
font-size: 0.78rem;
font-style: italic;
}
/* ── Section 5 placeholder ──────────────────────────────────── */
.outils-section--placeholder {
opacity: 0.6;
}
.placeholder-block {
border: 1.5px dashed var(--nav-bg-alt);
border-radius: 12px;
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.placeholder-block__badge {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--nav-text-muted);
}
.placeholder-block__title {
font-size: 0.95rem;
font-weight: 600;
color: var(--nav-text);
margin: 0;
}
.placeholder-block__desc {
font-size: 0.84rem;
color: var(--nav-text-muted);
margin: 0;
line-height: 1.5;
}
/* ── Footer ─────────────────────────────────────────────────── */
.outils-footer {
padding-top: 1rem;
border-top: 1px solid var(--nav-bg-alt);
text-align: center;
}
.outils-footer__text {
font-size: 0.84rem;
color: var(--nav-text-muted);
margin: 0;
}
/* ── Mobile ─────────────────────────────────────────────────── */
@media (max-width: 640px) {
.outils-page {
padding: 1.25rem 1rem 4rem;
}
.outils-header__inner {
flex-direction: column;
gap: 0.75rem;
}
.outils-header__title {
font-size: 1.4rem;
}
.outil-cards-grid {
grid-template-columns: 1fr;
}
.outils-section--fmhy {
padding: 1.25rem 1rem;
margin: 0 -0.5rem;
}
.fmhy-tree-wrap {
padding: 0.875rem 0.75rem;
}
}
</style>

Binary file not shown.

View File

@@ -1,99 +1,102 @@
{ {
"tree": { "name": "FMHY — Sélection Architecte",
"name": "FMHY — Sélection AEP", "description": "~50 ressources curées depuis FMHY (Free Media Heck Yeah) — pertinentes pour un architecte, un praticien de la transition, un créateur solo.",
"children": [ "children": [
{ {
"name": "IA & LLM", "name": "IA & Outils cognitifs",
"children": [ "children": [
{ "name": "DeepSeek", "url": "https://chat.deepseek.com/", "desc": "DeepSeek V3.2 — modèle libre puissant, sans limite, compte requis" }, { "name": "ChatGPT (OpenAI)", "url": "https://chat.openai.com/", "desc": "LLM généraliste, référence." },
{ "name": "Mistral Chat", "url": "https://chat.mistral.ai/", "desc": "Mistral Large 3, natif français, sans limite" }, { "name": "Claude (Anthropic)", "url": "https://claude.ai/", "desc": "Excellent pour rédaction longue et analyse de documents." },
{ "name": "Google AI Studio", "url": "https://aistudio.google.com/app/prompts/new_chat", "desc": "Gemini 3.1 Pro + API gratuite, idéal pour expérimenter" }, { "name": "Mistral Le Chat", "url": "https://chat.mistral.ai/", "desc": "LLM français, souverain, gratuit." },
{ "name": "Kimi", "url": "https://www.kimi.com/", "desc": "Kimi K2.6 Thinking — raisonnement avancé, login requis" }, { "name": "Perplexity", "url": "https://www.perplexity.ai/", "desc": "Moteur de recherche IA avec sources citées." },
{ "name": "NVIDIA NIM", "url": "https://build.nvidia.com/models", "desc": "Multi-modèles libres (Kimi, Qwen, GLM) sans compte" }, { "name": "Hugging Face", "url": "https://huggingface.co/", "desc": "Hub de modèles open source. Indispensable." },
{ "name": "NotebookLM", "url": "https://notebooklm.google.com/", "desc": "Chatbot sur tes propres documents, prise de notes augmentée" }, { "name": "LM Studio", "url": "https://lmstudio.ai/", "desc": "Faire tourner des LLM localement, sans cloud." }
{ "name": "Ollama", "url": "https://ollama.com/", "desc": "Héberger des modèles IA localement, toutes plateformes" }, ]
{ "name": "Open WebUI", "url": "https://openwebui.com/", "desc": "Interface web pour LLMs locaux (Ollama), self-hostable" }, },
{ "name": "LM Studio", "url": "https://lmstudio.ai/", "desc": "Appli desktop pour modèles IA locaux, sans cloud" }, {
{ "name": "Perplexity", "url": "https://www.perplexity.ai/", "desc": "Moteur de recherche IA avec sources citées" } "name": "Lecture & Documentation",
] "children": [
}, { "name": "Anna's Archive", "url": "https://annas-archive.org/", "desc": "Bibliothèque shadow la plus complète du web. Livres, articles, thèses." },
{ { "name": "Sci-Hub", "url": "https://sci-hub.se/", "desc": "Accès libre aux articles scientifiques payants." },
"name": "Recherche & articles", { "name": "Library Genesis", "url": "https://libgen.is/", "desc": "Livres techniques et académiques en PDF." },
"children": [ { "name": "Z-Library", "url": "https://z-lib.id/", "desc": "Bibliothèque numérique massive, interface soignée." },
{ "name": "SciSpace", "url": "https://scispace.com/", "desc": "Chatbot sur publications scientifiques, explications simplifiées" }, { "name": "OpenLibrary (Internet Archive)", "url": "https://openlibrary.org/", "desc": "Prêt numérique gratuit, millions de livres." },
{ "name": "Alphaxiv", "url": "https://www.alphaxiv.org/", "desc": "Chatbot sur les papiers de recherche arxiv" }, { "name": "Calibre", "url": "https://calibre-ebook.com/", "desc": "Gestion de bibliothèque numérique, convertisseur de formats." }
{ "name": "Unpaywall", "url": "https://unpaywall.org/", "desc": "Accès libre aux articles académiques — extension navigateur" }, ]
{ "name": "Bypass Paywalls Clean", "url": "https://gitflic.ru/project/magnolia1234/bpc_uploads", "desc": "Extension pour bypasser les paywalls d'articles de presse" }, },
{ "name": "Freedium", "url": "https://freedium-mirror.cfd/", "desc": "Accéder aux articles Medium sans abonnement" } {
] "name": "Dessin & Modélisation",
}, "children": [
{ { "name": "FreeCAD", "url": "https://www.freecad.org/", "desc": "Modélisation 3D open source, paramétrique. Alternative à Rhino pour usage simple." },
"name": "Livres & archives", { "name": "Blender", "url": "https://www.blender.org/", "desc": "3D, rendu, animation. La référence open source." },
"children": [ { "name": "Inkscape", "url": "https://inkscape.org/", "desc": "Dessin vectoriel. Alternative à Illustrator." },
{ "name": "Anna's Archive", "url": "https://annas-archive.gl/", "desc": "Meta-search des bibliothèques libres (Z-Lib, LibGen, Sci-Hub)" }, { "name": "GIMP", "url": "https://www.gimp.org/", "desc": "Retouche photo. Alternative à Photoshop." },
{ "name": "Z-Library", "url": "https://z-lib.gd/", "desc": "Bibliothèque de livres/articles, téléchargement direct" }, { "name": "Krita", "url": "https://krita.org/", "desc": "Dessin digital et croquis. Excellent pour les concepts." },
{ "name": "Library Genesis", "url": "https://libgen.li/", "desc": "Livres techniques, universitaires, comics en accès libre" }, { "name": "LibreOffice Draw", "url": "https://www.libreoffice.org/", "desc": "Diagrammes, plans rapides, sans suite Adobe." }
{ "name": "Internet Archive", "url": "https://archive.org/details/texts", "desc": "Livres, magazines, archives historiques numérisés" }, ]
{ "name": "Project Gutenberg", "url": "https://www.gutenberg.org/", "desc": "Classiques du domaine public, 70k+ livres" }, },
{ "name": "Open Library", "url": "https://openlibrary.org/", "desc": "Bibliothèque libre, emprunt numérique de livres" } {
] "name": "Productivité & Texte",
}, "children": [
{ { "name": "Obsidian", "url": "https://obsidian.md/", "desc": "PKM / prise de notes en Markdown. Gratuit pour usage personnel." },
"name": "Cours & formation", { "name": "Logseq", "url": "https://logseq.com/", "desc": "PKM open source, graphe de connaissances." },
"children": [ { "name": "Zotero", "url": "https://www.zotero.org/", "desc": "Gestionnaire de références bibliographiques. Indispensable pour la recherche." },
{ "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours MIT en accès libre — toutes disciplines, niveau universitaire" }, { "name": "Marktext", "url": "https://github.com/marktext/marktext", "desc": "Éditeur Markdown WYSIWYG, open source." },
{ "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Cours gratuits et interactifs, toutes matières, du lycée au supérieur" }, { "name": "Typst", "url": "https://typst.app/", "desc": "Alternative moderne à LaTeX pour la mise en page de documents." },
{ "name": "edX", "url": "https://www.edx.org/", "desc": "MOOCs certifiants des meilleures universités mondiales" }, { "name": "Pandoc", "url": "https://pandoc.org/", "desc": "Conversion universelle entre formats de documents (MD, DOCX, PDF, HTML...)." }
{ "name": "Learn Anything", "url": "https://learn-anything.xyz/", "desc": "Moteur de recherche de parcours d'apprentissage structurés" }, ]
{ "name": "OpenCulture", "url": "https://www.openculture.com/", "desc": "Cours, films, livres audio en accès libre, liens curatés" }, },
{ "name": "Appropedia", "url": "https://www.appropedia.org/", "desc": "Wiki de la durabilité, technologies appropriées, habitat écologique" } {
] "name": "Dev & Infrastructure",
}, "children": [
{ { "name": "VS Code", "url": "https://code.visualstudio.com/", "desc": "Éditeur de code. La référence, gratuit." },
"name": "Outils dev & infra", { "name": "Coolify", "url": "https://coolify.io/", "desc": "Self-hosting simplifié. Alternative à Heroku/Vercel sur son propre VPS." },
"children": [ { "name": "Hetzner Cloud", "url": "https://www.hetzner.com/cloud/", "desc": "VPS européen, tarifs très bas, data centers Allemagne." },
{ "name": "Free for Developers", "url": "https://free-for.dev/", "desc": "Index exhaustif des outils gratuits pour développeurs" }, { "name": "Caddy", "url": "https://caddyserver.com/", "desc": "Serveur web avec HTTPS automatique. Plus simple que Nginx." },
{ "name": "DevDocs", "url": "https://devdocs.io/", "desc": "Documentation développeur unifiée, consultable hors ligne" }, { "name": "n8n", "url": "https://n8n.io/", "desc": "Automatisation open source (comme Zapier mais self-hostable)." },
{ "name": "N8N", "url": "https://n8n.io/", "desc": "Automatisation de workflows open source, self-hostable" }, { "name": "Gitea", "url": "https://gitea.io/", "desc": "Hébergement Git self-hosted. Alternative à GitHub." }
{ "name": "NocoDB", "url": "https://github.com/nocodb/nocodb", "desc": "Base de données no-code open source, alternative Airtable" }, ]
{ "name": "Crontab Guru", "url": "https://crontab.guru/", "desc": "Éditeur de tâches cron avec explication lisible" }, },
{ "name": "Dokploy", "url": "https://github.com/dokploy/dokploy", "desc": "Déploiement d'apps auto-hébergé, alternative Coolify/Heroku" }, {
{ "name": "Open Source Guides", "url": "https://opensource.guide/", "desc": "Guides pour contribuer et gérer des projets open source" } "name": "Vie privée & Sécurité",
] "children": [
}, { "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source. Self-hostable." },
{ { "name": "ProtonMail", "url": "https://proton.me/mail", "desc": "Email chiffré, hébergement Suisse." },
"name": "Texte & documents", { "name": "Signal", "url": "https://signal.org/", "desc": "Messagerie chiffrée E2E. La référence." },
"children": [ { "name": "uBlock Origin", "url": "https://ublockorigin.com/", "desc": "Bloqueur de publicités et trackers, le plus efficace." },
{ "name": "DeepL", "url": "https://www.deepl.com/translator", "desc": "Traduction IA, meilleure qualité disponible en FR/EN/DE" }, { "name": "Mullvad VPN", "url": "https://mullvad.net/", "desc": "VPN respectueux de la vie privée, sans compte email requis." },
{ "name": "Paperless-ngx", "url": "https://docs.paperless-ngx.com/", "desc": "Gestion documentaire self-hosted avec OCR et indexation" }, { "name": "Privacy Guides", "url": "https://www.privacyguides.org/", "desc": "Recommandations d'outils respectueux de la vie privée, par thème." }
{ "name": "Whisper (OpenAI)", "url": "https://github.com/openai/whisper", "desc": "Transcription audio open source, multi-langues, qualité professionnelle" }, ]
{ "name": "GitHub Gists", "url": "https://gist.github.com/", "desc": "Partage de snippets code / notes, versionné, dans l'écosystème git" }, },
{ "name": "PrivateBin", "url": "https://privatebin.net/", "desc": "Pastebin chiffré zero-knowledge, self-hostable" }, {
{ "name": "LibreTranslate", "url": "https://libretranslate.com/", "desc": "Traducteur FOSS, self-hostable, sans tracking" } "name": "Formation & Apprentissage",
] "children": [
}, { "name": "MIT OpenCourseWare", "url": "https://ocw.mit.edu/", "desc": "Cours du MIT en libre accès, toutes disciplines." },
{ { "name": "Khan Academy", "url": "https://www.khanacademy.org/", "desc": "Maths, sciences, programmation — gratuit, pédagogie excellente." },
"name": "Vie privée & sécurité", { "name": "YouTube (canaux techniques)", "url": "https://www.youtube.com/", "desc": "Channals : The Coding Train, Fireship, 3Blue1Brown, etc." },
"children": [ { "name": "freeCodeCamp", "url": "https://www.freecodecamp.org/", "desc": "Apprendre le développement web de zéro, gratuit et certifiant." },
{ "name": "uBlock Origin", "url": "https://github.com/gorhill/uBlock", "desc": "Bloqueur de publicités et trackers — le plus efficace du marché" }, { "name": "Coursera (audit gratuit)", "url": "https://www.coursera.org/", "desc": "Cours universitaires, audit gratuit disponible sur la plupart." }
{ "name": "Bitwarden", "url": "https://bitwarden.com/", "desc": "Gestionnaire de mots de passe open source, cloud ou self-hosted" }, ]
{ "name": "KeePassXC", "url": "https://keepassxc.org/", "desc": "Gestionnaire de mots de passe local, base chiffrée, sans cloud" }, },
{ "name": "Pi-Hole", "url": "https://pi-hole.net/", "desc": "Bloqueur de pub DNS pour tout le réseau domestique, self-hosted" }, {
{ "name": "SponsorBlock", "url": "https://sponsor.ajay.app/", "desc": "Extension pour skipper les segments sponsorisés sur YouTube" }, "name": "Médias & Audio",
{ "name": "AdGuard Home", "url": "https://adguard.com/en/adguard-home/overview.html", "desc": "Alternative Pi-Hole, DNS adblocking self-hosted pour tout le réseau" } "children": [
] { "name": "Audacity", "url": "https://www.audacityteam.org/", "desc": "Enregistrement et édition audio. Référence open source." },
}, { "name": "yt-dlp", "url": "https://github.com/yt-dlp/yt-dlp", "desc": "Télécharger des vidéos/audio depuis YouTube et 1000+ sites." },
{ { "name": "VLC", "url": "https://www.videolan.org/vlc/", "desc": "Lecteur multimédia universel." },
"name": "Documentaires & médias", { "name": "Kdenlive", "url": "https://kdenlive.org/", "desc": "Montage vidéo open source, non linéaire." },
"children": [ { "name": "OBS Studio", "url": "https://obsproject.com/", "desc": "Enregistrement et streaming vidéo. La référence gratuite." }
{ "name": "Films For Action", "url": "https://www.filmsforaction.org/", "desc": "Films documentaires engagés, alternatives sociales et écologiques" }, ]
{ "name": "ARTE (replay)", "url": "https://www.arte.tv/", "desc": "Documentaires et programmes culturels FR/DE, replay gratuit" }, },
{ "name": "Documentary+", "url": "https://www.docplus.com/", "desc": "Plateforme de documentaires en streaming gratuit" }, {
{ "name": "TED Talks", "url": "https://www.ted.com/", "desc": "Conférences inspirantes sur science, société, design, architecture" } "name": "Divers Utiles",
] "children": [
} { "name": "Nextcloud", "url": "https://nextcloud.com/", "desc": "Cloud personnel self-hosted. Alternative à Google Drive/Dropbox." },
] { "name": "Joplin", "url": "https://joplinapp.org/", "desc": "Notes chiffrées, sync Nextcloud, open source." },
}, { "name": "draw.io / diagrams.net", "url": "https://app.diagrams.net/", "desc": "Diagrammes et schémas, gratuit, pas de compte requis." },
"source": "fmhy.net (sélection AEP 2026-05-22)", { "name": "Excalidraw", "url": "https://excalidraw.com/", "desc": "Tableau blanc collaboratif, style hand-drawn, open source." },
"total_entries": 50 { "name": "Fmhy.net (complet)", "url": "https://fmhy.net/", "desc": "L'arbre complet. Des milliers de ressources organisées par thème." }
]
}
]
} }

90
public/data/outils.json Normal file
View File

@@ -0,0 +1,90 @@
{
"simulateurs": [
{
"id": "autonomie",
"icon": "🟢",
"titre": "Simulateur Autonomie",
"url": "https://calculs.trans-former.fr/autonomie/",
"description": "Évaluer le degré d'autonomie d'une famille à un site donné selon les ressources locales (eau, énergie, alimentation).",
"cta": "Lancer le simulateur →",
"tag": "outil-aep"
}
],
"simulateurs_inspirations": [
{
"id": "florquin-prix-m2",
"icon": "💡",
"titre": "Estimation prix au m² — Florquin Studio",
"url": "https://offre.florquinstudio.com/",
"description": "Une agence parisienne qui a construit un système d'estimation au m² assez fin. Pas dans nos outils, mais inspirant pour qui veut industrialiser son chiffrage.",
"tag": "inspiration-externe"
}
],
"opensource": [
{
"id": "dictee-universelle-groq",
"icon": "🎤",
"titre": "Dictée universelle Groq",
"url": "https://github.com/Jayjay-nene/dictee-universelle-groq",
"description": "Appuie sur une touche, parle, le texte apparaît au curseur avec ponctuation et majuscules auto. Transcription Whisper Groq < 1s. Marche dans toutes les applis Windows. Outil par Jayjay-nene.",
"tag": "recommande"
},
{
"id": "atis-voice",
"icon": "🎙",
"titre": "Atis Voice (text-to-speech)",
"url": null,
"description": "Pipeline TTS pour transformer un texte en audio.",
"tag": "disponible"
},
{
"id": "install-vps",
"icon": "🖥",
"titre": "Install VPS open source",
"url": null,
"description": "Setup pas-à-pas pour monter son propre VPS (Hetzner, Coolify, Caddy, Postgres…) en mode reproductible.",
"tag": "a-venir"
},
{
"id": "skills-claude-code",
"icon": "⚙",
"titre": "Skills Claude Code",
"url": null,
"description": "Skills custom pour booster sa pratique avec un agent IA.",
"tag": "a-venir"
}
],
"bifurcation": {
"intro": "Beaucoup de jeunes diplômés en archi cherchent des chemins alternatifs. Cette section rassemble des témoignages, expériences, et ressources sur ce que c'est que de bifurquer.",
"videos_ofqa": [
{ "ep": "01", "titre": "Architectes indépendants", "personnes": "Jules Nény & Imane Fatmi", "url": "https://youtu.be/aMreB5KdNhY" },
{ "ep": "02", "titre": "Artiste & Maçon", "personnes": "Romane Dutour & Maël Canal", "url": "https://youtu.be/9gpjokx2ndI" },
{ "ep": "03", "titre": "Social & BTP", "personnes": "Célia Berdy & Esilda Perrot", "url": null, "note": "vidéo perdue, doc PDF seulement" },
{ "ep": "04", "titre": "Menuisier & Paysagiste", "personnes": "Adel Mohamedi & Julie Bowie", "url": "https://youtu.be/yKaRQhA3Z6g" },
{ "ep": "05", "titre": "Éco-construction", "personnes": "Edouard Vermès", "url": "https://youtu.be/97bDg1BjeuQ" },
{ "ep": "06", "titre": "Musicien & Urbaniste", "personnes": "Ruben Madar & Antoine Troccaz", "url": "https://drive.google.com/drive/folders/14g8YBn5bZAy8aIkHzQlOTrTtnWOaqRO3" },
{ "ep": "07", "titre": "AMO & Réemploi", "personnes": "Domitille Chaigne & Clémence Bondon", "url": "https://drive.google.com/file/d/1Q9Za81CElszmMn5n8dBsG0pJkiWmB32c/view" },
{ "ep": "08", "titre": "Gouvernance école", "personnes": "Solenn Guével", "url": "https://drive.google.com/drive/folders/1UaLsSyQcJydkXyV71klrY1tv9KgAh-mG" },
{ "ep": "bonus", "titre": "PFE — invitation à faire collectif", "personnes": "Jules Nény & Imane Fatmi", "url": "https://youtu.be/4qTEIC2Lmqw" }
],
"coalition_ensa_pb": {
"titre": "Coalition inter-asso ENSA-PB — victoire salle des enseignants",
"description": "Une coalition d'associations étudiantes a obtenu l'usage de la salle des enseignants pour des temps de travail collectif."
},
"ressources_externes": [
{
"id": "drop-the-kutch",
"icon": "🎧",
"titre": "Podcast Drop the Kutch — Sâm Afchar",
"url": "https://podcasts-francais.fr/podcast/drop-the-kutsch",
"description": "Témoignages de bifurcations post études d'archi. Super taf."
}
]
},
"section_5_placeholder": {
"titre": "[V2 — Login] Logiciels pro",
"description": "Logiciels lourds (Adobe, AutoCAD…) — accès mutualisé. Disponible après création de compte AEP.",
"status": "bientot-login"
},
"footer_contribution": "Tu utilises un outil qui mérite d'être ici ? Écris-moi : contact@trans-former.fr"
}

12
public/icons/CREDITS.md Normal file
View File

@@ -0,0 +1,12 @@
# Icons Credits
## outils-wrench.svg
**Source :** Heroicons — `wrench-screwdriver` icon
**Licence :** MIT
**URL :** https://heroicons.com/
**Note :** Fallback Heroicons utilisé car l'API Noun Project a retourné 403 Forbidden au moment du build (2026-05-22). L'icône `wrench-screwdriver` de Heroicons (MIT) est une clé à molette + tournevis, style line art minimaliste, compatible avec l'identité visuelle AEP.
Si tu veux substituer par une icône Noun Project, utilise l'endpoint :
`GET https://api.thenounproject.com/v2/icon?query=wrench&limit=20`
avec les credentials OAuth 1.0a stockées dans `_System/API-credentials.md`.

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l5.654-4.654m5.598-2.337 3.07-3.293a2.25 2.25 0 0 0-3.182-3.182l-3.293 3.07M6.75 12.75l-2.25.75.75-2.25 2.25-.75-.75 2.25Z" />
</svg>

After

Width:  |  Height:  |  Size: 493 B