Files
astro-site-cerveau/src/components/vue/CarteOWrapper.vue
Jules Neny 8f8b0c5f4c feat(v13-d): Carte O Option B rectangle central + bandeau sommaire + legende + TMIP relie
- YAML carte-o-source : label central -> 'Une medecine du corps social pour
  ecrire un nouveau contrat social' (phrase pleine 3 lignes)
- YAML : projet TMIP gagne lien_central:true (edge explicite centre <-> projet)
- build-carte-o.js : addEdge accepte opts.central=true pour tagger les edges
  rattachees au noeud central (permet tuning force-link cote Vue)
- carte-o.json regenere : 17 nodes, 20 edges (vs 19 V1.2-O), tous les edges
  central->thematiques + central->tmip portent flag central:true
- CarteO.vue : noeud central rendu en RECT 300x64 fill encre (vs cercle r30),
  label blanc multi-tspan 3 lignes 13px font-weight 500 line-height 1.35
- CarteO.vue : splitCentralLabel reecrit pour wrap intelligent (3 lignes
  ~30 chars), preserve compat ' + ' (V1.2)
- CarteO.vue : force tuning V1.3 -> alphaDecay 0.025, velocityDecay 0.4,
  forceCollide +12 (CENTRAL_COLLIDE_RADIUS=160 pour le rect), forceX/Y
  strength 0.05 rappel cadre, link distance/strength differencies
  (central->projet = 90/0.6, central->essai = 200/0.3)
- CarteO.vue : hover handler selector etendu rect|circle
- CarteOWrapper.vue : CarteEdge gagne champ central?:boolean
- ColCentre.astro : tabs Chatbot retires (ChatbotV2 import retire aussi),
  remplaces par header bandeau 'Sommaire editorial d'architecture
  d'ecologie politique' (gauche, monospace 12px) + legende 3 symboles
  (publie ● / a venir ○ / projet 🟠) en droite

Build SSR : 5 pages prerender, 0 warning, 4.35s.
2026-05-11 20:00:30 +02:00

182 lines
4.3 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import CarteO from './CarteO.vue'
import CarteOContextMenu from './CarteOContextMenu.vue'
interface CarteNode {
id: string
label: string
family: string
niveau?: number
nature?: 'essai' | 'projet'
statut?: 'gestation' | 'edite'
resume?: string | null
intention?: string
slug?: string
theme?: string
}
interface CarteEdge {
source: string
target: string
central?: boolean // V1.3-D : edges au noeud central (tuning force-link)
}
interface CarteData {
meta?: {
nodeCount?: number
edgeCount?: number
familyDistribution?: Record<string, number>
familyColors?: Record<string, string>
}
nodes: CarteNode[]
edges: CarteEdge[]
}
const props = withDefaults(defineProps<{
src?: string
}>(), {
src: '/data/carte-o.json',
})
const data = ref<CarteData | null>(null)
const error = ref<string | null>(null)
const selectedNode = ref<CarteNode | null>(null)
const contextX = ref(0)
const contextY = ref(0)
const isMobileScreen = ref(false)
const familyColors = computed(() =>
data.value?.meta?.familyColors || {
penseur: '#3b82f6',
concept: '#10b981',
methode: '#f59e0b',
collectif: '#ef4444',
ressource: '#8b5cf6',
}
)
function onNodeClick(payload: { node: CarteNode; x: number; y: number }) {
selectedNode.value = payload.node
contextX.value = payload.x
contextY.value = payload.y
}
onMounted(async () => {
isMobileScreen.value = window.innerWidth < 768
try {
const res = await fetch(props.src)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e: any) {
console.error('[CarteO] failed to load', e)
error.value = e?.message || 'Erreur de chargement'
}
window.addEventListener('resize', () => {
isMobileScreen.value = window.innerWidth < 768
})
})
</script>
<template>
<div class="wrapper">
<!-- Mobile fallback (V1) -->
<div v-if="isMobileScreen" class="mobile-fallback">
<div class="mini-map">
<svg viewBox="0 0 200 120" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle cx="100" cy="60" r="8" fill="#3b82f6" />
<circle cx="50" cy="35" r="5" fill="#10b981" />
<circle cx="150" cy="35" r="5" fill="#f59e0b" />
<circle cx="55" cy="90" r="5" fill="#ef4444" />
<circle cx="145" cy="90" r="5" fill="#8b5cf6" />
<line x1="100" y1="60" x2="50" y2="35" stroke="#94a3b8" stroke-width="0.8" opacity="0.5" />
<line x1="100" y1="60" x2="150" y2="35" stroke="#94a3b8" stroke-width="0.8" opacity="0.5" />
<line x1="100" y1="60" x2="55" y2="90" stroke="#94a3b8" stroke-width="0.8" opacity="0.5" />
<line x1="100" y1="60" x2="145" y2="90" stroke="#94a3b8" stroke-width="0.8" opacity="0.5" />
</svg>
</div>
<p class="msg">
Carte O optimisee desktop. Retournez sur grand ecran pour explorer la mindmap interactive.
</p>
</div>
<!-- Loading state -->
<div v-else-if="!data && !error" class="state">
<span>Chargement de la Carte O...</span>
</div>
<!-- Error state -->
<div v-else-if="error" class="state error">
<span>Impossible de charger la Carte O ({{ error }})</span>
</div>
<!-- Carte O -->
<template v-else-if="data">
<CarteO
:nodes="data.nodes"
:edges="data.edges"
:family-colors="familyColors"
@node-click="onNodeClick"
/>
<CarteOContextMenu
:node="selectedNode"
:x="contextX"
:y="contextY"
@close="selectedNode = null"
/>
</template>
</div>
</template>
<style scoped>
.wrapper {
height: 100%;
width: 100%;
position: relative;
}
.state {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
font-size: 0.9rem;
}
.state.error {
color: #dc2626;
}
.mobile-fallback {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem;
text-align: center;
gap: 1rem;
}
.mini-map {
width: 70%;
max-width: 240px;
}
.mini-map svg {
width: 100%;
height: auto;
}
.msg {
color: #6b7280;
font-size: 0.85rem;
line-height: 1.45;
font-style: italic;
max-width: 24rem;
margin: 0;
}
</style>