6 Commits

Author SHA1 Message Date
Jules Neny
a1d6271b65 feat(v12-o): Carte O logos plateforme via Brandfetch CDN (visible zoom>1.5x)
Champ optionnel domain dans YAML carte-o-source : propage vers JSON et
permet d'afficher un logo plateforme en bas-droite de chaque node (cercle
blanc 18px + image clippee circulaire 14px) quand le zoom depasse 1.5x.

V1.2-O par defaut : substack.com sur les 15 thematiques essais. Centre +
projet TMIP gardent leur fill brut (encre / ocre). Toggle visibilite via
callback zoom (opacity 0/1 sur .logo-overlay).

A flagger : CDN Brandfetch retourne 403 en curl server-side avec le client
ID fourni. A revalider en browser (origin trans-former.fr) — le CDN peut
exiger un Origin header autorise. Si bloque, fallback prevu en V1.3
(proxy local ou logos packages dans /public/logos/).

Files:
- public/data/carte-o-source.yaml : +15 champs domain
- scripts/build-carte-o.js : propagation domain -> JSON
- src/components/vue/CarteO.vue : CarteNode.domain + logoUrl helper
  + logo-overlay (circle + image clip-path) + toggle visibilite zoom
- public/data/carte-o.json : regenere (15/17 nodes ont domain)
2026-05-11 18:48:15 +02:00
Jules Neny
e613fc1ca3 merge(v12-q): iframe AEP forcé desktop transform scale 2026-05-11 18:43:06 +02:00
Jules Neny
d12f77554f merge(v12-n): Carte O fusion noeud central + palette minimaliste 2026-05-11 18:43:06 +02:00
Jules Neny
397484a35e merge(v12-m): header bandeau SiteHeader + titre Trans-Former/Jules Neny 2026-05-11 18:42:50 +02:00
Jules Neny
3ba4628818 feat(v12-m): header bandeau Trans-Former / Jules Neny / baseline palette terre 2026-05-11 18:41:51 +02:00
Jules Neny
7791054ca0 feat(v12-q): iframe AEP forcé desktop via transform scale + viewport 1440px 2026-05-11 18:40:50 +02:00
9 changed files with 188 additions and 28 deletions

View File

@@ -4,6 +4,9 @@
# statut: gestation (draft/en cours) | edite (publie)
# nature: essai (texte politique) | projet (projet archi)
# niveau: 0 (centre) | 1 (concepts force) | 2 (thematiques/projets)
# domain (optionnel) : domaine plateforme source pour logo Brandfetch CDN
# - affiche logo en bas-droite du node si zoom > 1.5x
# - V1.2 par defaut : substack.com pour tous les essais AEP
version: "1.1"
@@ -23,76 +26,91 @@ thematiques:
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "pratiques-collectives"
label: "Pratiques collectives"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "art-narration"
label: "Art & narration"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "pouvoir-domination"
label: "Rapport au pouvoir"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "medias-critique"
label: "Medias & pensee critique"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "justice-securite"
label: "Justice & securite"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "sante-globale"
label: "Sante globale"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "agriculture"
label: "Agriculture"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "post-croissance"
label: "Post-croissance"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "anthropocene"
label: "Anthropocene & effondrement"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "education"
label: "Education a la transformation"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "urbanisme"
label: "Urbanisme"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "geopolitique"
label: "Geopolitique & decolonisation"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "ia-technologie"
label: "IA & technologie"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
- id: "spiritualite"
label: "Spiritualite"
niveau: 2
nature: essai
statut: gestation
domain: "substack.com"
projets:
- id: "tmip"

View File

@@ -1,6 +1,6 @@
{
"version": "1.1",
"generatedAt": "2026-05-11T16:41:21.600Z",
"generatedAt": "2026-05-11T16:47:45.459Z",
"nodes": [
{
"id": "contrat-social-medecine-corps-social",
@@ -20,7 +20,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "pratiques-collectives",
@@ -30,7 +31,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "art-narration",
@@ -40,7 +42,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "pouvoir-domination",
@@ -50,7 +53,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "medias-critique",
@@ -60,7 +64,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "justice-securite",
@@ -70,7 +75,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "sante-globale",
@@ -80,7 +86,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "agriculture",
@@ -90,7 +97,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "post-croissance",
@@ -100,7 +108,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "anthropocene",
@@ -110,7 +119,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "education",
@@ -120,7 +130,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "urbanisme",
@@ -130,7 +141,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "geopolitique",
@@ -140,7 +152,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "ia-technologie",
@@ -150,7 +163,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "spiritualite",
@@ -160,7 +174,8 @@
"statut": "gestation",
"resume": null,
"radius": 12,
"family": "concept"
"family": "concept",
"domain": "substack.com"
},
{
"id": "tmip",

View File

@@ -43,7 +43,7 @@ async function main() {
}
function addNode(obj) {
nodes.push({
const node = {
id: obj.id,
label: obj.label,
niveau: obj.niveau,
@@ -52,7 +52,10 @@ async function main() {
resume: obj.resume || null,
radius: getRadius(obj.niveau, obj.nature),
family: getFamily(obj.nature),
})
}
// V1.2-O : propage le champ optionnel domain (logo plateforme via Brandfetch CDN)
if (obj.domain) node.domain = obj.domain
nodes.push(node)
}
const centreId = data.centre.id

View File

@@ -6,7 +6,7 @@
<nav
id="mobile-tab-bar"
aria-label="Navigation colonnes"
class="fixed top-0 left-0 right-0 z-30 h-11 bg-white border-b border-neutral-200 flex items-stretch justify-around md:hidden"
class="fixed top-12 left-0 right-0 z-30 h-11 bg-white border-b border-neutral-200 flex items-stretch justify-around md:hidden"
>
<button
type="button"

View File

@@ -0,0 +1,52 @@
---
// SiteHeader.astro - V1.2-M : bandeau header pleine largeur identite site
// Palette terre figee : papier #FAFAF7, encre #0F172A, encre douce #475569
// Composition retenue : 2 lignes hierarchique
// ligne 1 : "Trans-Former" wordmark dominant (semibold tracking serre)
// ligne 2 : "Jules Neny" + baseline italique cote a cote (separateur point median)
// Rationale : le wordmark domine sans ecraser ; la baseline reste lisible ;
// composition adaptee a un manifeste (hierarchie typographique forte).
// Hauteur : ~64px desktop / ~48px mobile (compacte)
// Baseline raccourcie mobile : "architecture politique du vivant"
---
<header
class="site-header w-full border-b border-[#E5E7EB] bg-[#FAFAF7] text-[#0F172A] px-4 md:px-6 flex items-center"
role="banner"
>
<a
href="/"
class="flex flex-col md:flex-row md:items-baseline gap-x-3 gap-y-0 no-underline text-[#0F172A] hover:text-[#0F172A]"
aria-label="trans-former.fr - retour accueil"
>
<!-- Ligne 1 : wordmark dominant -->
<span
class="font-semibold tracking-tight text-[#0F172A] text-[17px] md:text-[20px] leading-none"
>
Trans-Former
</span>
<!-- Ligne 2 (desktop : inline ; mobile : sous wordmark) : Jules Neny + baseline -->
<span class="flex items-baseline gap-2 text-[#475569] leading-none">
<span class="text-[11px] md:text-[13px]">Jules Neny</span>
<span class="text-[#94A3B8]" aria-hidden="true">·</span>
<!-- Baseline longue : desktop / Baseline courte : mobile -->
<span class="italic text-[11px] md:text-[13px] hidden sm:inline">
architecture d'ecologie politique
</span>
<span class="italic text-[11px] inline sm:hidden">
architecture politique du vivant
</span>
</span>
</a>
</header>
<style>
.site-header {
height: 48px;
}
@media (min-width: 768px) {
.site-header {
height: 64px;
}
}
</style>

View File

@@ -19,8 +19,15 @@ interface CarteNode {
slug?: string
theme?: string
path?: string
domain?: string // V1.2-O : domaine plateforme source (logo Brandfetch CDN)
}
// V1.2-O : logos plateforme via Brandfetch CDN (visible zoom > 1.5x seulement)
const BRANDFETCH_CLIENT_ID = '4ae58bd85c8140eab0cee72f40656120'
const LOGO_ZOOM_THRESHOLD = 1.5
const logoUrl = (domain: string) =>
`https://cdn.brandfetch.io/${domain}/w/64/h/64?c=${BRANDFETCH_CLIENT_ID}`
interface CarteEdge {
source: string | CarteNode
target: string | CarteNode
@@ -187,6 +194,11 @@ function initSvg() {
.scaleExtent([0.3, 4])
.on('zoom', (event) => {
gZoom!.attr('transform', event.transform.toString())
// V1.2-O : toggle visibilite logos plateforme selon le niveau de zoom
// (evite la surcharge visuelle au niveau d'ensemble, montre detail au drill-down)
const scale = event.transform.k
gZoom!.selectAll('.logo-overlay')
.style('opacity', scale > LOGO_ZOOM_THRESHOLD ? 1 : 0)
})
svgRoot.call(zoomBehavior as any)
@@ -290,6 +302,32 @@ function render() {
}
})
// V1.2-O : Logo plateforme (visible zoom > 1.5x seulement)
// Cercle blanc 14px en bas-droite du node (offset +60% x/+60% y du radius)
// Image clip-path circle pour avatar style. Hidden par defaut (opacity 0).
const nodeGroupsWithLogo = nodeGroups.filter(d => !!d.domain)
const logoOverlay = nodeGroupsWithLogo.append('g')
.attr('class', 'logo-overlay')
.style('opacity', 0)
.attr('pointer-events', 'none')
logoOverlay.append('circle')
.attr('cx', d => getRadius(d) * 0.6)
.attr('cy', d => getRadius(d) * 0.6)
.attr('r', 9)
.attr('fill', '#FFFFFF')
.attr('stroke', '#0F172A')
.attr('stroke-width', 0.5)
logoOverlay.append('image')
.attr('href', d => logoUrl(d.domain!))
.attr('x', d => getRadius(d) * 0.6 - 7)
.attr('y', d => getRadius(d) * 0.6 - 7)
.attr('width', 14)
.attr('height', 14)
.attr('clip-path', d => `circle(7px at ${getRadius(d) * 0.6}px ${getRadius(d) * 0.6}px)`)
// Tooltip <title>
nodeGroups.append('title')
.text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`)

View File

@@ -14,8 +14,27 @@ interface JournalItem {
const selectedItem = ref<JournalItem | null>(null)
const iframeRef = ref<HTMLIFrameElement | null>(null)
const wrapperRef = ref<HTMLDivElement | null>(null)
const skeletonHidden = ref(false)
// Force rendu desktop de l'iframe AEP : viewport simulée 1440px + scale dynamique
const VIEWPORT_W = 1440
const iframeScale = ref(0.42)
let resizeObs: ResizeObserver | null = null
const updateScale = () => {
if (!wrapperRef.value) return
const w = wrapperRef.value.clientWidth
if (w > 0) iframeScale.value = w / VIEWPORT_W
}
const iframeStyle = computed(() => ({
width: VIEWPORT_W + 'px',
height: (100 / iframeScale.value) + '%',
transform: `scale(${iframeScale.value})`,
transformOrigin: '0 0',
}))
const onJournalItemClick = (e: Event) => {
const ce = e as CustomEvent
if (ce.detail?.item) selectedItem.value = ce.detail.item
@@ -23,9 +42,16 @@ const onJournalItemClick = (e: Event) => {
onMounted(() => {
window.addEventListener('journal-item-click', onJournalItemClick as EventListener)
if (wrapperRef.value && typeof ResizeObserver !== 'undefined') {
updateScale()
resizeObs = new ResizeObserver(updateScale)
resizeObs.observe(wrapperRef.value)
}
})
onUnmounted(() => {
window.removeEventListener('journal-item-click', onJournalItemClick as EventListener)
resizeObs?.disconnect()
resizeObs = null
})
const reset = () => {
@@ -89,18 +115,19 @@ const formatDate = (iso: string) => {
<!-- DEFAULT : iframe AEP (aucun item selectionne) -->
<div v-if="!selectedItem" class="h-full">
<div class="relative h-full bg-neutral-100">
<div ref="wrapperRef" class="relative h-full bg-neutral-100 overflow-hidden">
<div
v-if="!skeletonHidden"
id="embed-skeleton"
class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse"
class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse z-10"
>
<span class="text-neutral-400 text-sm">Chargement de la carte AEP...</span>
</div>
<iframe
src="https://aep.trans-former.fr/agences"
title="Carte AEP"
class="w-full h-full border-0 opacity-0 transition-opacity duration-500"
class="absolute top-0 left-0 border-0 opacity-0 transition-opacity duration-500"
:style="iframeStyle"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
@load="onIframeLoad"
ref="iframeRef"

View File

@@ -1,6 +1,7 @@
---
import '../styles/global.css';
import Footer from '../components/astro/Footer.astro';
import SiteHeader from '../components/astro/SiteHeader.astro';
interface Props {
title?: string;
@@ -28,8 +29,11 @@ const {
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
</head>
<body class="m-0 bg-white text-neutral-900 antialiased">
<slot />
<body class="m-0 bg-white text-neutral-900 antialiased min-h-screen flex flex-col">
<SiteHeader />
<div class="flex-1 flex flex-col min-h-0">
<slot />
</div>
<Footer />
</body>
</html>

View File

@@ -15,15 +15,18 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro';
<MobileTabBar />
<PopupOnboarding />
<!-- Desktop : grid 3 colonnes -->
<div class="hidden md:grid md:grid-cols-[320px_1fr_320px] h-screen overflow-hidden">
<!-- Desktop : grid 3 colonnes (header 64px deja consommes par SiteHeader, on prend le reste) -->
<div
class="hidden md:grid md:grid-cols-[320px_1fr_320px] overflow-hidden"
style="height: calc(100vh - 64px);"
>
<aside class="border-r border-neutral-200 overflow-y-auto"><ColJournal /></aside>
<main class="overflow-hidden"><ColCentre /></main>
<aside class="border-l border-neutral-200 overflow-y-auto"><ColInsta /></aside>
</div>
<!-- Mobile : SwipeContainer Vue island - decale de 44px pour la tabbar -->
<div class="md:hidden overflow-hidden" style="height: calc(100dvh - 44px); margin-top: 44px;">
<!-- Mobile : SwipeContainer Vue island - header 48px + tabbar 44px = 92px reserves -->
<div class="md:hidden overflow-hidden" style="height: calc(100dvh - 48px - 44px); margin-top: 44px;">
<SwipeContainer client:load>
<ColJournal slot="left" />
<ColCentre slot="center" />