6 Commits

Author SHA1 Message Date
Jules Neny
aa410ce7aa feat(v13-bg): layout 1 ecran fixe + hamburger desktop hide + categorie Pro 2026-05-11 20:00:16 +02:00
Jules Neny
e43ec60ecf merge(v13-p0): iframe AEP skeleton timeout fallback fix loading stuck 2026-05-11 19:53:54 +02:00
Jules Neny
1033099663 fix(v13-p0): iframe AEP skeleton timeout fallback (resolves loading stuck)
Le @load event ne fire pas (ou tardivement) sur l'iframe AEP enfermee
dans un wrapper avec transform scale(0.42) + viewport simulee 1440px.
Resultat : skeleton 'Chargement de la carte AEP...' reste affiche
indefiniment, masquant l'iframe meme si elle se charge.

Fix :
- setTimeout 2.5s dans onMounted qui force revealIframe() inconditionnellement
- onIframeLoad clear le timer si l'event fire dans les temps (cas nominal)
- retrait du z-10 sur le skeleton (defense en profondeur : si bug residuel,
  l'iframe sera quand meme visible derriere)
- factorisation revealIframe() partagee entre @load et fallback
- cleanup du timer dans onUnmounted

Build SSR : 5 pages, 0 warning, ~4s.
Tests browser manuels a faire par Jules pour confirmer disparition skeleton.
2026-05-11 19:52:57 +02:00
Jules Neny
9bb55bc311 merge(v12-p): preview article 3 zones + colonnes scrollables indep 2026-05-11 18:52:59 +02:00
Jules Neny
7ec0efdeb5 merge(v12-o): Carte O logos Brandfetch overlay (zoom>1.5x) 2026-05-11 18:52:58 +02:00
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
13 changed files with 154 additions and 45 deletions

View File

@@ -4,6 +4,9 @@
# statut: gestation (draft/en cours) | edite (publie) # statut: gestation (draft/en cours) | edite (publie)
# nature: essai (texte politique) | projet (projet archi) # nature: essai (texte politique) | projet (projet archi)
# niveau: 0 (centre) | 1 (concepts force) | 2 (thematiques/projets) # 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" version: "1.1"
@@ -23,76 +26,91 @@ thematiques:
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "pratiques-collectives" - id: "pratiques-collectives"
label: "Pratiques collectives" label: "Pratiques collectives"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "art-narration" - id: "art-narration"
label: "Art & narration" label: "Art & narration"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "pouvoir-domination" - id: "pouvoir-domination"
label: "Rapport au pouvoir" label: "Rapport au pouvoir"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "medias-critique" - id: "medias-critique"
label: "Medias & pensee critique" label: "Medias & pensee critique"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "justice-securite" - id: "justice-securite"
label: "Justice & securite" label: "Justice & securite"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "sante-globale" - id: "sante-globale"
label: "Sante globale" label: "Sante globale"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "agriculture" - id: "agriculture"
label: "Agriculture" label: "Agriculture"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "post-croissance" - id: "post-croissance"
label: "Post-croissance" label: "Post-croissance"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "anthropocene" - id: "anthropocene"
label: "Anthropocene & effondrement" label: "Anthropocene & effondrement"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "education" - id: "education"
label: "Education a la transformation" label: "Education a la transformation"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "urbanisme" - id: "urbanisme"
label: "Urbanisme" label: "Urbanisme"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "geopolitique" - id: "geopolitique"
label: "Geopolitique & decolonisation" label: "Geopolitique & decolonisation"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "ia-technologie" - id: "ia-technologie"
label: "IA & technologie" label: "IA & technologie"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
- id: "spiritualite" - id: "spiritualite"
label: "Spiritualite" label: "Spiritualite"
niveau: 2 niveau: 2
nature: essai nature: essai
statut: gestation statut: gestation
domain: "substack.com"
projets: projets:
- id: "tmip" - id: "tmip"

View File

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

View File

@@ -43,7 +43,7 @@ async function main() {
} }
function addNode(obj) { function addNode(obj) {
nodes.push({ const node = {
id: obj.id, id: obj.id,
label: obj.label, label: obj.label,
niveau: obj.niveau, niveau: obj.niveau,
@@ -52,7 +52,10 @@ async function main() {
resume: obj.resume || null, resume: obj.resume || null,
radius: getRadius(obj.niveau, obj.nature), radius: getRadius(obj.niveau, obj.nature),
family: getFamily(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 const centreId = data.centre.id

View File

@@ -34,6 +34,16 @@ const categories = [
], ],
hasSelector: false, hasSelector: false,
}, },
{
id: 'pro',
label: 'Pro',
color: '#0F172A',
hashtags: ['#building-public', '#pro'],
plateformes: [
{ id: 'linkedin', label: 'LinkedIn', url: 'https://www.linkedin.com/in/jules-neny/' },
],
hasSelector: false,
},
]; ];
--- ---
<div class="h-full flex flex-col p-4 pt-20 md:pt-6 gap-5"> <div class="h-full flex flex-col p-4 pt-20 md:pt-6 gap-5">
@@ -42,7 +52,7 @@ const categories = [
<details id="hashtags-accordion" class="border-t border-neutral-200 pt-4"> <details id="hashtags-accordion" class="border-t border-neutral-200 pt-4">
<summary class="font-semibold cursor-pointer select-none flex items-center justify-between"> <summary class="font-semibold cursor-pointer select-none flex items-center justify-between">
<span>Hashtags</span> <span>Hashtags</span>
<span class="text-xs text-neutral-400 font-normal">3 categories</span> <span class="text-xs text-neutral-400 font-normal">4 categories</span>
</summary> </summary>
<div class="mt-3 flex flex-wrap gap-2" id="category-badges"> <div class="mt-3 flex flex-wrap gap-2" id="category-badges">
@@ -126,7 +136,7 @@ const categories = [
const PLATFORM_KEY = 'tf-platform-filter'; const PLATFORM_KEY = 'tf-platform-filter';
// Active state : map categoryId -> boolean // Active state : map categoryId -> boolean
const activeCategories: Record<string, boolean> = { politique: true, art: true, outils: true }; const activeCategories: Record<string, boolean> = { politique: true, art: true, outils: true, pro: true };
// Platform filter : map categoryId -> platformId | null // Platform filter : map categoryId -> platformId | null
const platformFilters: Record<string, string | null> = { politique: null }; const platformFilters: Record<string, string | null> = { politique: null };
@@ -138,15 +148,18 @@ const categories = [
const politiqueHashtags = ['#politique', '#aep-politique']; const politiqueHashtags = ['#politique', '#aep-politique'];
const artHashtags = ['#peinture', '#art']; const artHashtags = ['#peinture', '#art'];
const outilsHashtags = ['#stack', '#building-public']; const outilsHashtags = ['#stack', '#building-public'];
const proHashtags = ['#building-public', '#pro'];
const allPolitique = politiqueHashtags.every(h => storedHashtags[h] !== false); const allPolitique = politiqueHashtags.every(h => storedHashtags[h] !== false);
const allArt = artHashtags.every(h => storedHashtags[h] !== false); const allArt = artHashtags.every(h => storedHashtags[h] !== false);
const allOutils = outilsHashtags.every(h => storedHashtags[h] !== false); const allOutils = outilsHashtags.every(h => storedHashtags[h] !== false);
const allPro = proHashtags.every(h => storedHashtags[h] !== false);
if (Object.keys(storedHashtags).length > 0) { if (Object.keys(storedHashtags).length > 0) {
activeCategories['politique'] = allPolitique; activeCategories['politique'] = allPolitique;
activeCategories['art'] = allArt; activeCategories['art'] = allArt;
activeCategories['outils'] = allOutils; activeCategories['outils'] = allOutils;
activeCategories['pro'] = allPro;
} }
} catch { /* mode prive */ } } catch { /* mode prive */ }
@@ -163,6 +176,7 @@ const categories = [
politique: ['#politique', '#aep-politique'], politique: ['#politique', '#aep-politique'],
art: ['#peinture', '#art'], art: ['#peinture', '#art'],
outils: ['#stack', '#building-public'], outils: ['#stack', '#building-public'],
pro: ['#building-public', '#pro'],
}; };
for (const [catId, tags] of Object.entries(catHashtags)) { for (const [catId, tags] of Object.entries(catHashtags)) {
const active = activeCategories[catId] ?? true; const active = activeCategories[catId] ?? true;

View File

@@ -5,7 +5,7 @@
<button <button
id="hamburger-trigger" id="hamburger-trigger"
type="button" type="button"
class="fixed top-4 right-4 z-50 p-3 bg-white/95 border border-neutral-200 rounded-lg shadow-md hover:bg-white transition-colors md:top-6 md:right-6" class="fixed top-4 right-4 z-50 p-3 bg-white/95 border border-neutral-200 rounded-lg shadow-md hover:bg-white transition-colors md:hidden"
aria-label="Ouvrir le menu" aria-label="Ouvrir le menu"
aria-expanded="false" aria-expanded="false"
aria-controls="hamburger-drawer" aria-controls="hamburger-drawer"

View File

@@ -19,8 +19,15 @@ interface CarteNode {
slug?: string slug?: string
theme?: string theme?: string
path?: 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 { interface CarteEdge {
source: string | CarteNode source: string | CarteNode
target: string | CarteNode target: string | CarteNode
@@ -187,6 +194,11 @@ function initSvg() {
.scaleExtent([0.3, 4]) .scaleExtent([0.3, 4])
.on('zoom', (event) => { .on('zoom', (event) => {
gZoom!.attr('transform', event.transform.toString()) 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) 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> // Tooltip <title>
nodeGroups.append('title') nodeGroups.append('title')
.text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`) .text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`)

View File

@@ -23,24 +23,43 @@ const iframeStyle = computed(() => ({
transformOrigin: '0 0', transformOrigin: '0 0',
})) }))
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
const revealIframe = () => {
if (iframeRef.value) {
iframeRef.value.classList.remove('opacity-0')
iframeRef.value.classList.add('opacity-100')
}
skeletonHidden.value = true
}
onMounted(() => { onMounted(() => {
if (wrapperRef.value && typeof ResizeObserver !== 'undefined') { if (wrapperRef.value && typeof ResizeObserver !== 'undefined') {
updateScale() updateScale()
resizeObs = new ResizeObserver(updateScale) resizeObs = new ResizeObserver(updateScale)
resizeObs.observe(wrapperRef.value) resizeObs.observe(wrapperRef.value)
} }
// Fallback : si @load ne fire pas dans 2.5s (transform/scale peut bloquer l'event),
// on revele quand meme l'iframe pour ne pas laisser le skeleton infini.
fallbackTimer = setTimeout(() => {
revealIframe()
}, 2500)
}) })
onUnmounted(() => { onUnmounted(() => {
resizeObs?.disconnect() resizeObs?.disconnect()
resizeObs = null resizeObs = null
if (fallbackTimer) {
clearTimeout(fallbackTimer)
fallbackTimer = null
}
}) })
const onIframeLoad = () => { const onIframeLoad = () => {
if (iframeRef.value) { if (fallbackTimer) {
iframeRef.value.classList.remove('opacity-0') clearTimeout(fallbackTimer)
iframeRef.value.classList.add('opacity-100') fallbackTimer = null
} }
skeletonHidden.value = true revealIframe()
} }
</script> </script>
@@ -51,7 +70,7 @@ const onIframeLoad = () => {
<div <div
v-if="!skeletonHidden" v-if="!skeletonHidden"
id="embed-skeleton" id="embed-skeleton"
class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse z-10" class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse"
> >
<span class="text-neutral-400 text-sm">Chargement de la carte AEP...</span> <span class="text-neutral-400 text-sm">Chargement de la carte AEP...</span>
</div> </div>

View File

@@ -14,7 +14,7 @@ const {
} = Astro.props; } = Astro.props;
--- ---
<!doctype html> <!doctype html>
<html lang="fr"> <html lang="fr" class="h-screen">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
@@ -29,11 +29,15 @@ const {
<meta name="twitter:title" content={title} /> <meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} /> <meta name="twitter:description" content={description} />
</head> </head>
<body class="m-0 bg-white text-neutral-900 antialiased min-h-screen flex flex-col"> <body class="m-0 bg-white text-neutral-900 antialiased h-screen flex flex-col overflow-hidden">
<SiteHeader /> <div class="flex-shrink-0">
<div class="flex-1 flex flex-col min-h-0"> <SiteHeader />
</div>
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
<slot /> <slot />
</div> </div>
<Footer /> <div class="flex-shrink-0">
<Footer />
</div>
</body> </body>
</html> </html>

View File

@@ -10,7 +10,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro';
> >
<HamburgerMenu /> <HamburgerMenu />
<main class="min-h-screen bg-white"> <main class="h-full overflow-y-auto bg-white">
<article class="max-w-2xl mx-auto px-6 py-16 md:py-24"> <article class="max-w-2xl mx-auto px-6 py-16 md:py-24">
<header class="mb-10"> <header class="mb-10">

View File

@@ -15,18 +15,16 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro';
<MobileTabBar /> <MobileTabBar />
<PopupOnboarding /> <PopupOnboarding />
<!-- Desktop : grid 3 colonnes (header 64px deja consommes par SiteHeader, on prend le reste) --> <!-- Desktop : grid 3 colonnes V1.3-BG : prend toute la place du wrapper flex-1 du BaseLayout.
<div Header et footer sont fixes (flex-shrink-0), pas besoin de calc(100vh - X). -->
class="hidden md:grid md:grid-cols-[320px_1fr_320px] overflow-hidden" <div class="hidden md:grid md:grid-cols-[320px_1fr_320px] h-full overflow-hidden">
style="height: calc(100vh - 64px);" <aside class="border-r border-neutral-200 overflow-y-auto h-full"><ColJournal /></aside>
> <main class="overflow-hidden h-full"><ColCentre /></main>
<aside class="border-r border-neutral-200 overflow-y-auto"><ColJournal /></aside> <aside class="border-l border-neutral-200 overflow-y-auto h-full"><ColInsta /></aside>
<main class="overflow-hidden"><ColCentre /></main>
<aside class="border-l border-neutral-200 overflow-y-auto"><ColInsta /></aside>
</div> </div>
<!-- Mobile : SwipeContainer Vue island - header 48px + tabbar 44px = 92px reserves --> <!-- Mobile : SwipeContainer Vue island - tabbar 44px reserve dans la zone flex-1 -->
<div class="md:hidden overflow-hidden" style="height: calc(100dvh - 48px - 44px); margin-top: 44px;"> <div class="md:hidden h-full overflow-hidden" style="padding-top: 44px;">
<SwipeContainer client:load> <SwipeContainer client:load>
<ColJournal slot="left" /> <ColJournal slot="left" />
<ColCentre slot="center" /> <ColCentre slot="center" />

View File

@@ -10,7 +10,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro';
> >
<HamburgerMenu /> <HamburgerMenu />
<main class="min-h-screen bg-white"> <main class="h-full overflow-y-auto bg-white">
<article class="max-w-2xl mx-auto px-6 py-16 md:py-24"> <article class="max-w-2xl mx-auto px-6 py-16 md:py-24">
<!-- En-tete --> <!-- En-tete -->

View File

@@ -10,7 +10,7 @@ import HamburgerMenu from '../../components/astro/HamburgerMenu.astro';
> >
<HamburgerMenu /> <HamburgerMenu />
<main class="min-h-screen bg-white"> <main class="h-full overflow-y-auto bg-white">
<article class="max-w-xl mx-auto px-6 py-16 md:py-24"> <article class="max-w-xl mx-auto px-6 py-16 md:py-24">
<header class="mb-10"> <header class="mb-10">

View File

@@ -10,7 +10,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro';
> >
<HamburgerMenu /> <HamburgerMenu />
<main class="min-h-screen bg-white"> <main class="h-full overflow-y-auto bg-white">
<article class="max-w-2xl mx-auto px-6 py-16 md:py-24"> <article class="max-w-2xl mx-auto px-6 py-16 md:py-24">
<header class="mb-10"> <header class="mb-10">