- Récupérés depuis commit vault b700612^ (état pré-chirurgie git) - FicheFamilleModal.vue (284L) — PV2-5g - FicheModalV2.vue (341L) + NavMapV2.vue (243L) — PV2-5 - HashtagFilter.vue (97L) + IntentionBanner.vue (76L) — PV2-5 - GraphView.vue (860L) — PV2-5b+5e+5f+5g complet - ChatbotPlaceholder.vue (423L) — version chatbot-v2 - pages/index.vue (517L) — carte unifiée 3 onglets - types/structure-v2.ts, assets/css/v2-bifurcation.css - server/api/chatbot-v2.post.ts, server/utils/vectorSearch.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
11 KiB
Vue
285 lines
11 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<!-- Backdrop -->
|
|
<Transition name="backdrop">
|
|
<div
|
|
v-if="modelValue && familleId != null"
|
|
class="fixed inset-0 z-[1400]"
|
|
style="background: rgba(26,34,56,0.55);"
|
|
@click="close"
|
|
aria-hidden="true"
|
|
/>
|
|
</Transition>
|
|
|
|
<!-- Modal -->
|
|
<Transition name="modal">
|
|
<div
|
|
v-if="modelValue && familleId != null"
|
|
class="fixed z-[1401] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col"
|
|
style="
|
|
width: min(720px, 94vw);
|
|
max-height: 88vh;
|
|
background: var(--nav-bg);
|
|
border-radius: 16px;
|
|
box-shadow: 0 16px 64px rgba(26,34,56,0.28);
|
|
overflow: hidden;
|
|
"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-label="familleLabel"
|
|
@keydown.esc="close"
|
|
>
|
|
<!-- Header : background couleur famille -->
|
|
<div
|
|
class="flex items-center justify-between px-5 py-4 shrink-0"
|
|
:style="`background: ${familleColor}; color: white;`"
|
|
>
|
|
<div class="flex items-center gap-3">
|
|
<div
|
|
class="w-3 h-3 rounded-full shrink-0"
|
|
style="background: white;"
|
|
/>
|
|
<h2 class="text-lg font-bold" style="color: white;">{{ familleLabel }}</h2>
|
|
</div>
|
|
<button
|
|
@click="close"
|
|
class="w-8 h-8 rounded-lg flex items-center justify-center transition-colors"
|
|
style="background: rgba(255,255,255,0.18); color: white;"
|
|
aria-label="Fermer"
|
|
>
|
|
<svg width="14" height="14" 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>
|
|
|
|
<!-- Body scrollable -->
|
|
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
<!-- Description longue editoriale -->
|
|
<p
|
|
class="text-sm leading-relaxed mb-5"
|
|
style="color: var(--nav-text); white-space: pre-wrap;"
|
|
>{{ familleDescription }}</p>
|
|
|
|
<!-- Separateur -->
|
|
<div style="height: 1px; background: var(--nav-bg-alt); margin-bottom: 16px;" />
|
|
|
|
<!-- Mode fusion : Principal+Secondaire melanges (peu de secondaires) -->
|
|
<template v-if="!showSplit">
|
|
<h3
|
|
class="text-xs font-bold uppercase tracking-wide mb-3"
|
|
style="color: var(--nav-text-muted);"
|
|
>{{ allStructures.length }} structure{{ allStructures.length > 1 ? 's' : '' }}</h3>
|
|
<ul style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px;">
|
|
<li
|
|
v-for="s in allStructures"
|
|
:key="s.id"
|
|
@click="selectStructure(s.id)"
|
|
class="structure-row"
|
|
style="
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 8px 10px; border-radius: 6px;
|
|
cursor: pointer; transition: background 0.1s;
|
|
"
|
|
>
|
|
<span
|
|
style="width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;"
|
|
:style="`background: ${FAMILLE_COLORS[s.famille_principale] ?? '#888'};`"
|
|
:title="`Famille principale : ${FAMILLE_LABELS[s.famille_principale] ?? ''}`"
|
|
/>
|
|
<span class="text-sm font-medium" style="color: var(--nav-text);">{{ s.nom }}</span>
|
|
<span class="text-xs" style="color: var(--nav-text-muted);">{{ s.ville }}</span>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
|
|
<!-- Mode split : Principal / Secondaire separes -->
|
|
<template v-else>
|
|
<div v-if="principalStructures.length" class="mb-5">
|
|
<h3
|
|
class="text-xs font-bold uppercase tracking-wide mb-3"
|
|
style="color: var(--nav-text-muted);"
|
|
>Principal ({{ principalStructures.length }})</h3>
|
|
<ul style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px;">
|
|
<li
|
|
v-for="s in principalStructures"
|
|
:key="s.id"
|
|
@click="selectStructure(s.id)"
|
|
class="structure-row"
|
|
style="
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 8px 10px; border-radius: 6px;
|
|
cursor: pointer; transition: background 0.1s;
|
|
"
|
|
>
|
|
<span
|
|
style="width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;"
|
|
:style="`background: ${FAMILLE_COLORS[s.famille_principale] ?? '#888'};`"
|
|
/>
|
|
<span class="text-sm font-medium" style="color: var(--nav-text);">{{ s.nom }}</span>
|
|
<span class="text-xs" style="color: var(--nav-text-muted);">{{ s.ville }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div v-if="secondaireStructures.length">
|
|
<h3
|
|
class="text-xs font-bold uppercase tracking-wide mb-3"
|
|
style="color: var(--nav-text-muted);"
|
|
>Secondaire ({{ secondaireStructures.length }})</h3>
|
|
<ul style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px;">
|
|
<li
|
|
v-for="s in secondaireStructures"
|
|
:key="s.id"
|
|
@click="selectStructure(s.id)"
|
|
class="structure-row"
|
|
style="
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 8px 10px; border-radius: 6px;
|
|
cursor: pointer; transition: background 0.1s;
|
|
"
|
|
>
|
|
<span
|
|
style="width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;"
|
|
:style="`background: ${FAMILLE_COLORS[s.famille_principale] ?? '#888'};`"
|
|
:title="`Famille principale : ${FAMILLE_LABELS[s.famille_principale] ?? ''}`"
|
|
/>
|
|
<span class="text-sm font-medium" style="color: var(--nav-text);">{{ s.nom }}</span>
|
|
<span class="text-xs" style="color: var(--nav-text-muted);">{{ s.ville }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div
|
|
class="px-5 py-3 shrink-0 text-xs"
|
|
style="border-top: 1px solid var(--nav-bg-alt); color: var(--nav-text-muted); background: var(--nav-surface);"
|
|
>
|
|
Click sur une structure pour ouvrir sa fiche
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ReseauxBifurcationData, StructureV2 } from '~/types/structure-v2'
|
|
|
|
const FAMILLE_COLORS: Record<number, string> = {
|
|
1: '#a85d3e',
|
|
2: '#c4a472',
|
|
3: '#d4a017',
|
|
4: '#5a7a4a',
|
|
5: '#3d6a8c',
|
|
6: '#6b3fa0',
|
|
}
|
|
|
|
const FAMILLE_LABELS: Record<number, string> = {
|
|
1: 'Reemploi & filieres',
|
|
2: 'Frugalite & low-tech',
|
|
3: 'Architecture sociale',
|
|
4: 'Collectifs & AMO',
|
|
5: 'Urbanisme de transition',
|
|
6: 'Recherche-action',
|
|
}
|
|
|
|
const FAMILLE_DESCRIPTIONS: Record<number, string> = {
|
|
1: "Structures dont le geste premier est de travailler avec la matiere existante : deconstruction selective, plateformes de redistribution, filieres biosourcees et geosourcees.",
|
|
2: "Pratiques qui partent du principe qu'on peut faire mieux avec moins. Renovation profonde, materiaux locaux, sobriete choisie.",
|
|
3: "Structures dont le terrain premier est le mal-logement, la precarite, l'hospitalite. Architecture comme reponse a l'urgence sociale.",
|
|
4: "Structures qui accompagnent les projets collectifs : cooperatives d'habitat, ecovillages, accompagnement vers l'autogestion ou la renovation.",
|
|
5: "Demarches a l'echelle du territoire : villes en transition, PLU alternatifs, coalitions territoriales.",
|
|
6: "Recherche-action et production de contre-savoirs (Forensic Architecture, Rural Studio, PEROU, Centrala). Badge transversal aux familles.",
|
|
}
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
familleId: number | null
|
|
data: ReseauxBifurcationData | null
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
'select-structure': [id: string]
|
|
}>()
|
|
|
|
function close() {
|
|
emit('update:modelValue', false)
|
|
}
|
|
|
|
function selectStructure(id: string) {
|
|
// Fermer d'abord pour eviter superposition de modales
|
|
emit('update:modelValue', false)
|
|
emit('select-structure', id)
|
|
}
|
|
|
|
// Fermeture Esc globale
|
|
onMounted(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && props.modelValue) close()
|
|
}
|
|
window.addEventListener('keydown', handler)
|
|
onUnmounted(() => window.removeEventListener('keydown', handler))
|
|
})
|
|
|
|
const familleColor = computed(() =>
|
|
FAMILLE_COLORS[props.familleId ?? 0] ?? '#888'
|
|
)
|
|
|
|
const familleLabel = computed(() =>
|
|
FAMILLE_LABELS[props.familleId ?? 0] ?? ''
|
|
)
|
|
|
|
const familleDescription = computed(() =>
|
|
FAMILLE_DESCRIPTIONS[props.familleId ?? 0] ?? ''
|
|
)
|
|
|
|
const principalStructures = computed<StructureV2[]>(() => {
|
|
if (!props.data || props.familleId == null) return []
|
|
return props.data.structures
|
|
.filter(s => s.famille_principale === props.familleId)
|
|
.sort((a, b) => a.nom.localeCompare(b.nom, 'fr'))
|
|
})
|
|
|
|
const secondaireStructures = computed<StructureV2[]>(() => {
|
|
if (!props.data || props.familleId == null) return []
|
|
return props.data.structures
|
|
.filter(s =>
|
|
s.famille_principale !== props.familleId
|
|
&& (s.familles_secondaires ?? []).includes(props.familleId as number)
|
|
)
|
|
.sort((a, b) => a.nom.localeCompare(b.nom, 'fr'))
|
|
})
|
|
|
|
const allStructures = computed<StructureV2[]>(() => {
|
|
return [...principalStructures.value, ...secondaireStructures.value]
|
|
})
|
|
|
|
// Heuristique : si > 3 secondaires, separer en sections distinctes
|
|
const showSplit = computed(() => secondaireStructures.value.length > 3)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.structure-row:hover {
|
|
background: var(--nav-bg-alt);
|
|
}
|
|
|
|
.backdrop-enter-active, .backdrop-leave-active { transition: opacity 0.2s ease; }
|
|
.backdrop-enter-from, .backdrop-leave-to { opacity: 0; }
|
|
|
|
.modal-enter-active, .modal-leave-active {
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
}
|
|
.modal-enter-from, .modal-leave-to {
|
|
opacity: 0;
|
|
transform: translate(-50%, -52%);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.backdrop-enter-active, .backdrop-leave-active { transition: none; }
|
|
.modal-enter-active, .modal-leave-active { transition: none; }
|
|
}
|
|
</style>
|