Compare commits

...
5 Commits
Author SHA1 Message Date
YvvandClaude Opus 5.5 67cd869d83 actes : mise en ligne de la refonte (armé par Yvv)
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:44:28 +02:00
YvvandClaude Opus 5.5 201cfbbb50 design : accueil console seul, vrai §, curseurs animés, statuts, lecteur
- accueil : la console devient l'accueil unique (variantes B/C, sélecteur
  et ancien accueil retirés) ; curseurs qui montent de l'asservissement et
  dérivent seuls, un axe mis en avant à tour de rôle, arrêt au clic,
  immobiles en prefers-reduced-motion ; « AUTONOMIE » devant chaque axe,
  leviers mis en valeur, ligne des trois autonomies en couleurs
- logo : § de Playfair Display (SIL OFL) vectorisé, composant partagé
  UiSectionSign (en-tête, page événement) + favicon.svg
- UiStatusBadge (production / développement / gestation) sur l'accueil,
  les pages d'axe et les pages projet
- liens libreDecision / SejeteralO fixés sur la prod (plus de repli localhost)
- logiciel libre : bouton v1 dans la carte projet en tête de page
- couverture du livre : recadrage corrigé (BookSection)
- texte courant en Public Sans (zéros non barrés)
- lecteur : la pastille persistante ne masque plus la navigation du
  BookPlayer ; lecture directe du MP3 (les .ogg n'existent pas : 404)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:59:33 +02:00
YvvandClaude Opus 5.5 4d8fe4895d contenus : LaPlanK, sweetHome, statuts de production, sous-titre événement
- LPK → LaPlanK partout ; sweethomeCloud → sweetHome partout
- statuts : Monnaie libre en production depuis 2017 ; wishBounty,
  Décision collective, Tarifs de l'eau en développement ; v1 wishBounty
  en production pour l'écosystème Duniter ; « bridée » retiré
- accueil : « Le librodrome est une invitation », « … une boîte à outils » ;
  échelle de la console Asservissement → Émancipation
- sweetHome clé en main pour chaque collectif, association ou entreprise
  coopérative ; page Le Librodrome : « Un format d'événement »

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 05:59:33 +02:00
YvvandClaude Opus 5.5 e9fce4be8f design : accueil en trois variantes (console, parcours, établi) + couleur par axe
- typo : Bricolage Grotesque (titres) + Atkinson Hyperlegible (texte),
  Syne gardée pour le logo seul ; une seule source de polices (Bunny)
- palettes : teinte propre à chaque axe dans les 4 ambiances ;
  pages d'axe en axis-scope, cartes sans bordure (canon design)
- accueil : console des trois curseurs (tablist clavier), boîte à outils,
  bloc format éditorial + événement + reproduction ; ?v=a|b|c|0
- numérique : carte LPK ; bouquet rendu avec statut et légende
- messages : sans bordure, deux colonnes en grand écran

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:15:46 +02:00
YvvandClaude Opus 5.5 c3ada04e52 contenus : LPK sert l'axe numérique, bouquet réaligné sur les stacks HCO/LPK
- site.yml : bloc lpk (La Plank, asso 1901, financement des communs numériques)
- home.yml : bloc format + une ligne d'engagement par axe
- cloud-libre.yml : bouquet en objets {rôle, logiciel, statut, lien} —
  en service / au catalogue / à venir ; OpenCloud, Twenty, ERPNext,
  Vaultwarden, Woodpecker, Kubo… ; aucune ligne d'origine perdue ;
  équivalents « Gestion & organisation » ajoutés
- evenement.yml : Nextcloud → OpenCloud dans l'axe numérique

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:15:46 +02:00
49 changed files with 2432 additions and 1140 deletions
@@ -0,0 +1,35 @@
# Acte — mise en ligne de la refonte (accueil console, contenus LaPlanK/sweetHome)
Classe : déploie (le push de main déploie librodrome.org, sans étape de test)
Projet : librodrome Date : 2026-09-29
Armé le 2026-09-29 par Yvv : « Tu déploies directement stp. […] Déploie »
## Intention
Mettre en ligne la refonte : accueil « console des curseurs », vrai §, statuts
de production, bouquet sweetHome aligné sur les stacks réelles, LaPlanK,
correctifs lecteur (pastille, MP3 direct) et couverture du livre.
## Commandes exactes
```bash
git push origin <SHA de ce commit>:main
```
## Contenu sortant
5 commits (c3ada04, e9fce4b, 4d8fe48, 201cfbb + ce dossier) : app/, site/
(YAML de contenu), nuxt.config.ts, uno.config.ts, public/favicon.svg.
Scan `data/ .env .db .enc clés IDENTIFIANTS` sur `git diff origin/main..HEAD` : **vide**.
`git pull --rebase origin main` juste avant : à jour (aucune édition admin
en prod à intégrer).
## Effet attendu
librodrome.org reconstruit par Woodpecker. Irréversibilité : réversible.
## Rollback
```bash
git revert --no-edit c3ada04^..HEAD && git push origin HEAD:main
```
(ou redéployer 38467be)
## Vérifications post-acte
- [ ] CI Woodpecker verte / site servi avec le nouveau HTML
- [ ] captures prod : accueil desktop + mobile, numérique, cloud-libre, événement
- [ ] 0 erreur console, BookPlayer lit un MP3, /api/messages répond
+2 -2
View File
@@ -21,9 +21,9 @@ export default defineAppConfig({
},
},
libredecision: {
url: import.meta.dev ? 'http://localhost:3002' : 'https://decision.librodrome.org',
url: 'https://decision.librodrome.org',
},
sejeteral0: {
url: import.meta.dev ? 'http://localhost:3009' : 'https://collectivites.librodrome.org',
url: 'https://collectivites.librodrome.org',
},
})
+3 -3
View File
@@ -2,13 +2,13 @@
/* This file provides fallback and utility classes */
.font-display {
font-family: 'Syne', system-ui, sans-serif;
font-family: var(--font-display);
}
.font-sans {
font-family: 'Space Grotesk', system-ui, sans-serif;
font-family: var(--font-sans);
}
.font-mono {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-family: var(--font-mono);
}
+20 -2
View File
@@ -15,14 +15,27 @@
--player-height: 0rem;
--sidebar-width: 280px;
--font-display: 'Syne', sans-serif;
--font-sans: 'Space Grotesk', sans-serif;
--font-display: 'Bricolage Grotesque', system-ui, sans-serif;
--font-sans: 'Public Sans', system-ui, sans-serif;
--font-logo: 'Syne', sans-serif;
/* One hue per axis — overridden by each palette (stores/palette.ts) */
--axis-num: 198 70% 30%;
--axis-eco: 24 88% 42%;
--axis-cit: 150 45% 28%;
--font-mono: 'JetBrains Mono', monospace;
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out-expo: cubic-bezier(0.87, 0, 0.13, 1);
}
/* Fader level of the home console — registered so it can transition smoothly */
@property --level {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
*,
*::before,
*::after {
@@ -85,6 +98,11 @@ a {
background: hsl(var(--color-text) / 0.25);
}
/* ═══ Axis scopes — each axis page takes its own hue as primary ═══ */
.axis-scope--num { --color-primary: var(--axis-num); }
.axis-scope--eco { --color-primary: var(--axis-eco); }
.axis-scope--cit { --color-primary: var(--axis-cit); }
/* ═══ Light mode overrides ═══ */
.palette-light {
color-scheme: light;
+1 -1
View File
@@ -110,7 +110,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
gap: 0.625rem;
font-family: var(--font-display, 'Syne', sans-serif);
font-family: var(--font-display);
font-size: 0.9rem;
font-weight: 600;
color: white;
+11 -2
View File
@@ -378,6 +378,15 @@ function onTouchEnd(e: TouchEvent) {
}
// ── Lifecycle ──
// While the reader is open, the persistent mini player (above it) lifts up
// so it does not cover the reader navigation.
if (import.meta.client) {
watch(isOpen, (open) => {
document.documentElement.classList.toggle('book-player-open', open)
}, { immediate: true })
onUnmounted(() => document.documentElement.classList.remove('book-player-open'))
}
watch(isOpen, async (open) => {
if (open) {
showSommaire.value = false
@@ -606,7 +615,7 @@ onUnmounted(() => {
}
.reader-bar-title {
flex: 1;
font-family: var(--font-display, 'Syne', sans-serif);
font-family: var(--font-display);
font-size: clamp(0.85rem, 2.5vw, 1.1rem);
font-weight: 600;
color: white;
@@ -647,7 +656,7 @@ onUnmounted(() => {
gap: 0.25rem;
}
.sommaire-title {
font-family: var(--font-display, 'Syne', sans-serif);
font-family: var(--font-display);
font-size: 0.85rem;
font-weight: 700;
color: hsl(var(--color-text-muted));
-379
View File
@@ -1,379 +0,0 @@
<template>
<div class="axis-block">
<!-- Header -->
<div class="flex items-center gap-3 mb-6">
<div class="axis-icon" :class="`axis-icon--${color}`">
<div :class="iconClass(icon)" class="h-6 w-6" />
</div>
<h2 class="font-display text-2xl font-bold text-white">{{ title }}</h2>
</div>
<!-- Items grid -->
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="(item, i) in items"
:key="i"
class="axis-item card-surface"
:class="{ 'axis-item--gestation': item.gestation }"
>
<!-- Overlay link — full card clickable (z-index 0) -->
<component
:is="itemTag(item)"
v-bind="itemAttrs(item)"
class="axis-item-overlay"
:aria-label="item.label"
tabindex="-1"
/>
<!-- Card body (position relative, z-index 1 — above overlay) -->
<div class="axis-item-body">
<!-- Item icon -->
<div v-if="item.icon" class="axis-item-icon" :class="`axis-item-icon--${color}`">
<span v-if="item.icon === 'g1'" class="axis-item-icon-g1">Ğ1</span>
<div v-else :class="iconClass(item.icon)" class="h-5 w-5" />
</div>
<h3 class="font-display text-lg font-semibold text-white mb-1">
{{ item.label }}
<span v-if="item.gestation" class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</h3>
<p class="text-sm text-white/60 leading-relaxed">{{ item.description }}</p>
<!-- Presentation inline (projet en gestation) -->
<div v-if="item.presentation" class="axis-presentation-inline">
<div class="axis-pi-header">
<div class="i-lucide-sparkles h-3 w-3" />
<span class="axis-pi-title">{{ item.presentation.title }}</span>
</div>
<p class="axis-pi-text">{{ item.presentation.text }}</p>
</div>
</div>
<!-- Actions zone — z-index 1, above overlay, boutons ouvrent liens externes -->
<div v-if="item.actions?.length" class="axis-actions">
<!-- Primary row -->
<div class="axis-actions-row">
<component
:is="action.href ? 'a' : 'button'"
v-for="action in primaryActions(item.actions)"
:key="action.id"
v-bind="action.href ? { href: action.href, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="axis-action-btn"
:class="{ 'axis-action-btn--highlight': action.highlight }"
@click.stop="!action.href && handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</component>
</div>
<!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
<template v-for="action in secondaryActions(item.actions)" :key="action.label">
<NuxtLink
v-if="action.to"
:to="action.to"
class="axis-action-btn axis-action-btn--secondary"
@click.stop
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</NuxtLink>
<button
v-else
class="axis-action-btn axis-action-btn--secondary"
@click.stop="handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</button>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface AxisAction {
id: string
label: string
icon: string
highlight?: boolean
secondary?: boolean
to?: string
href?: string
}
interface AxisItem {
label: string
description: string
to?: string
href?: string
gestation?: boolean
icon?: string
actions?: AxisAction[]
presentation?: { title: string; text: string }
}
defineProps<{
title: string
icon: string
color?: 'primary' | 'accent'
items: AxisItem[]
}>()
const emit = defineEmits<{
'open-player': []
'open-pdf': []
'launch-gratewizard': []
}>()
function primaryActions(actions: AxisAction[]) {
return actions.filter(a => !a.secondary)
}
function secondaryActions(actions: AxisAction[]) {
return actions.filter(a => a.secondary)
}
function handleAction(id: string) {
if (id === 'open-player') emit('open-player')
else if (id === 'open-pdf') emit('open-pdf')
else if (id === 'launch-gratewizard') emit('launch-gratewizard')
}
function iconClass(name: string) {
return `i-lucide-${name}`
}
function itemTag(item: AxisItem) {
if (item.href) return 'a'
if (item.to) return resolveComponent('NuxtLink')
return 'div'
}
function itemAttrs(item: AxisItem) {
if (item.href) return { href: item.href, target: '_blank', rel: 'noopener noreferrer' }
if (item.to) return { to: item.to }
return {}
}
</script>
<style scoped>
.axis-block {
padding: 0;
}
.axis-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.75rem;
flex-shrink: 0;
}
.axis-icon--primary {
background: hsl(var(--color-primary) / 0.12);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.axis-icon--accent {
background: hsl(var(--color-accent) / 0.12);
border: 1px solid hsl(var(--color-accent) / 0.2);
color: hsl(var(--color-accent));
}
.axis-item {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s, box-shadow 0.2s;
position: relative;
/* overflow: visible pour laisser le tooltip sortir du cadre */
}
/* Lien overlay — couvre toute la carte, AU-DESSUS du contenu */
.axis-item-overlay {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
}
.axis-item:hover {
border-color: hsl(var(--color-primary) / 0.25);
box-shadow: 0 4px 24px hsl(var(--color-primary) / 0.06);
}
.axis-item--gestation {
opacity: 0.75;
}
.axis-item--gestation:hover {
opacity: 0.9;
}
.axis-item-body {
display: flex;
flex-direction: column;
padding: 1.25rem;
flex: 1;
color: inherit;
}
.axis-item-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
margin-bottom: 0.75rem;
}
.axis-item-icon--primary {
background: hsl(var(--color-primary) / 0.18);
color: hsl(var(--color-primary));
box-shadow: 0 0 14px hsl(var(--color-primary) / 0.15);
}
.axis-item-icon--accent {
background: hsl(var(--color-accent) / 0.18);
color: hsl(var(--color-accent));
box-shadow: 0 0 14px hsl(var(--color-accent) / 0.15);
}
.axis-item-icon-g1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.1rem;
line-height: 1;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: 0.5rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.7rem;
font-weight: 500;
font-family: var(--font-mono);
vertical-align: middle;
}
.axis-actions {
display: flex;
flex-direction: column;
gap: 0;
border-top: 1px solid hsl(var(--color-text) / 0.06);
background: hsl(var(--color-bg) / 0.4);
border-bottom-left-radius: 0.75rem;
border-bottom-right-radius: 0.75rem;
position: relative;
z-index: 2;
}
.axis-actions-row {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.75rem 1.25rem;
}
.axis-actions-secondary {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.5rem 1.25rem 0.75rem;
border-top: 1px solid hsl(var(--color-text) / 0.04);
}
.axis-action-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
color: hsl(var(--color-text) / 0.7);
background: hsl(var(--color-text) / 0.05);
border: 1px solid hsl(var(--color-text) / 0.1);
transition: all 0.2s;
cursor: pointer;
}
.axis-action-btn:hover {
color: hsl(var(--color-text));
background: hsl(var(--color-primary) / 0.12);
border-color: hsl(var(--color-primary) / 0.3);
}
.axis-action-btn--highlight {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.12);
border-color: hsl(var(--color-primary) / 0.25);
}
.axis-action-btn--highlight:hover {
background: hsl(var(--color-primary) / 0.2);
border-color: hsl(var(--color-primary) / 0.4);
}
.axis-action-btn--secondary {
color: hsl(var(--color-text) / 0.45);
background: transparent;
border-color: hsl(var(--color-text) / 0.06);
font-size: 0.75rem;
}
.axis-action-btn--secondary:hover {
color: hsl(var(--color-accent));
background: hsl(var(--color-accent) / 0.08);
border-color: hsl(var(--color-accent) / 0.2);
}
/* Presentation inline — projet en gestation, affiché dans la card */
.axis-presentation-inline {
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 8px;
background: hsl(var(--color-accent) / 0.07);
border: 1px solid hsl(var(--color-accent) / 0.18);
}
.axis-pi-header {
display: flex;
align-items: center;
gap: 0.3rem;
margin-bottom: 0.2rem;
color: hsl(var(--color-accent));
}
.axis-pi-title {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.axis-pi-text {
font-size: 0.72rem;
color: hsl(var(--color-text) / 0.55);
line-height: 1.45;
margin: 0;
}
</style>
-237
View File
@@ -1,237 +0,0 @@
<template>
<section class="section-padding">
<div class="container-content flex flex-col gap-16">
<UiScrollReveal>
<div id="numerique">
<HomeAxisBlock
v-if="axes?.numerique"
:title="axes.numerique.title"
:icon="axes.numerique.icon"
color="primary"
:items="axes.numerique.items"
/>
</div>
</UiScrollReveal>
<UiScrollReveal :delay="100">
<div id="economique">
<HomeAxisBlock
v-if="axes?.economie"
:title="axes.economie.title"
:icon="axes.economie.icon"
color="accent"
:items="axes.economie.items"
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
@launch-gratewizard="launchGW"
/>
<!-- Bloc GrateWizard — dans la section économique -->
<button v-if="gw" class="gw-block mt-4" @click="launchGW">
<div class="gw-icon">
<div class="i-lucide-sparkles h-5 w-5" />
</div>
<div class="gw-text">
<h3 class="font-display text-base font-bold text-white sm:text-lg">
{{ gw.title }}
</h3>
<p class="text-white/55 text-xs sm:text-sm mt-0.5">
{{ gw.subtitle }}
</p>
</div>
<div class="gw-arrow">
<div class="i-lucide-arrow-up-right h-4 w-4" />
</div>
</button>
</div>
</UiScrollReveal>
<UiScrollReveal :delay="200">
<div id="citoyenne">
<HomeAxisBlock
v-if="axes?.politique"
:title="axes.politique.title"
:icon="axes.politique.icon"
color="primary"
:items="axes.politique.items"
/>
</div>
</UiScrollReveal>
<!-- Bloc Événement -->
<UiScrollReveal v-if="evenement" :delay="350">
<NuxtLink :to="evenement.to" class="event-block">
<div class="event-content">
<div class="event-icon">
<div class="i-lucide-calendar-heart h-7 w-7" />
</div>
<div>
<h2 class="font-display text-2xl font-bold text-white sm:text-3xl">
{{ evenement.title }}
</h2>
<p class="font-display text-xl text-white/70 sm:text-2xl">
{{ evenement.subtitle }}
</p>
</div>
</div>
<span v-if="evenement.gestation" class="event-badge">
<div class="i-lucide-flask-conical h-3.5 w-3.5" />
En gestation
</span>
</NuxtLink>
</UiScrollReveal>
</div>
</section>
</template>
<script setup lang="ts">
defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { data: content } = await usePageContent('home')
const { launch } = useGrateWizard()
const axes = computed(() => (content.value as any)?.axes)
const gw = computed(() => (content.value as any)?.gratewizard)
const evenement = computed(() => (content.value as any)?.evenement)
function launchGW() {
launch()
}
</script>
<style scoped>
.event-block {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding: 2rem 2.5rem;
border-radius: 1rem;
border: 2px solid hsl(var(--color-accent) / 0.25);
background: linear-gradient(135deg, hsl(var(--color-accent) / 0.08), hsl(var(--color-primary) / 0.04));
transition: border-color 0.3s, box-shadow 0.3s;
text-decoration: none;
}
.event-block:hover {
border-color: hsl(var(--color-accent) / 0.45);
box-shadow: 0 0 40px hsl(var(--color-accent) / 0.08);
}
.event-content {
display: flex;
align-items: center;
gap: 1.25rem;
}
.event-icon {
display: flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-accent) / 0.15);
color: hsl(var(--color-accent));
flex-shrink: 0;
}
.event-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
font-size: 0.75rem;
font-weight: 500;
font-family: var(--font-mono);
white-space: nowrap;
flex-shrink: 0;
}
/* GrateWizard block */
.gw-block {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
padding: 1.25rem 1.5rem;
border-radius: 0.75rem;
border: none;
background: linear-gradient(135deg, hsl(280 50% 20% / 0.35), hsl(260 40% 15% / 0.25));
box-shadow: 0 0 40px hsl(280 60% 50% / 0.06), inset 0 1px 0 hsl(280 60% 70% / 0.08);
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
color: inherit;
}
.gw-block:hover {
background: linear-gradient(135deg, hsl(280 50% 24% / 0.45), hsl(260 40% 18% / 0.35));
box-shadow: 0 0 60px hsl(280 60% 50% / 0.12), inset 0 1px 0 hsl(280 60% 70% / 0.12);
transform: translateY(-2px);
}
.gw-block:active {
transform: translateY(0);
}
.gw-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.625rem;
background: hsl(280 60% 55% / 0.18);
color: hsl(280 60% 72%);
flex-shrink: 0;
box-shadow: 0 0 20px hsl(280 60% 50% / 0.15);
}
.gw-text {
flex: 1;
min-width: 0;
}
.gw-arrow {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: hsl(280 60% 55% / 0.1);
color: hsl(280 60% 65%);
flex-shrink: 0;
transition: all 0.2s;
}
.gw-block:hover .gw-arrow {
background: hsl(280 60% 55% / 0.2);
color: hsl(280 60% 80%);
transform: translate(2px, -2px);
}
@media (max-width: 640px) {
.event-block {
flex-direction: column;
align-items: flex-start;
padding: 1.5rem;
}
.gw-block {
padding: 1.25rem;
gap: 1rem;
}
.gw-arrow {
display: none;
}
}
</style>
+3 -3
View File
@@ -89,9 +89,9 @@ const titleLine2 = computed(() => titleParts.value[1])
.book-cover-3d {
width: 100%;
max-width: 360px;
aspect-ratio: 3 / 4;
background-size: 200% auto;
background-position: right center;
aspect-ratio: 1756 / 2480;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 0.75rem;
overflow: hidden;
+42
View File
@@ -0,0 +1,42 @@
<template>
<blockquote v-if="lines?.length" class="cites">
<p v-for="(line, i) in lines" :key="i" class="cite" :class="{ 'cite--lead': i === 0 }">{{ line }}</p>
</blockquote>
</template>
<script setup lang="ts">
defineProps<{
lines: string[]
}>()
</script>
<style scoped>
.cites {
margin: 0;
padding: 0;
max-width: 46rem;
}
.cite {
margin: 0;
font-family: var(--font-display);
font-weight: 500;
font-size: clamp(1.15rem, 2.4vw, 1.55rem);
line-height: 1.3;
letter-spacing: -0.015em;
color: hsl(var(--color-text) / 0.62);
}
.cite + .cite {
margin-top: 0.3rem;
}
.cite--lead {
font-weight: 800;
font-size: clamp(1.6rem, 3.6vw, 2.3rem);
line-height: 1.1;
letter-spacing: -0.03em;
color: hsl(var(--color-text));
margin-bottom: 0.6rem;
}
</style>
+555
View File
@@ -0,0 +1,555 @@
<template>
<div
ref="root"
class="console"
:class="{ 'console--compact': compact, 'console--still': reduced }"
@pointerenter="hovered = true"
@pointerleave="hovered = false"
@focusin="hovered = true"
@focusout="hovered = false"
>
<div class="console-board">
<!-- Scale, read bottom to top: from asservissement to émancipation -->
<div class="console-scale" aria-hidden="true">
<span class="console-scale-bottom">{{ bottomLabel }}</span>
<span class="console-scale-line" />
<span class="console-scale-top">{{ topLabel }}</span>
</div>
<div
class="console-faders"
role="tablist"
:aria-label="`Les trois axes — ${bottomLabel} ou ${topLabel}`"
@keydown="onKeydown"
>
<button
v-for="(axis, i) in axes"
:id="`fader-${uid}-${axis.key}`"
:key="axis.key"
ref="tabs"
type="button"
role="tab"
class="fader"
:class="{ 'fader--active': axis.key === active }"
:style="{ '--hue': `var(${axis.hue})`, '--level': levels[axis.key] ?? 0, '--i': i }"
:aria-selected="axis.key === active"
:aria-controls="`panel-${uid}`"
:tabindex="axis.key === active ? 0 : -1"
@click="choose(axis.key)"
>
<span class="fader-track" aria-hidden="true">
<span class="fader-fill" />
<span class="fader-cap" />
</span>
<span class="fader-label">
<span class="fader-prefix">{{ prefix }}</span>
<span class="fader-name">{{ axis.short }}</span>
</span>
<span class="fader-lever">{{ axis.lever }}</span>
</button>
</div>
</div>
<div
v-if="current"
:id="`panel-${uid}`"
role="tabpanel"
class="console-panel"
:style="{ '--hue': `var(${current.hue})` }"
:aria-labelledby="`fader-${uid}-${current.key}`"
>
<Transition name="panel" mode="out-in">
<div :key="current.key" class="panel-inner">
<p class="panel-upstream">
<span class="panel-upstream-label">{{ upstreamLabel }}</span>
{{ current.lever }}
</p>
<h2 class="panel-title">{{ current.title }}</h2>
<p v-if="current.promise" class="panel-promise">{{ current.promise }}</p>
<ul class="panel-tools">
<li v-for="tool in current.items" :key="tool.label">
<NuxtLink v-if="tool.to" :to="tool.to" class="panel-tool">
<span class="panel-tool-name">{{ tool.label }}</span>
<span v-if="tool.presentation" class="panel-tool-app">{{ tool.presentation.title }}</span>
<span v-if="tool.status || tool.gestation" class="panel-tool-state">{{ (tool.status || 'En gestation').toLowerCase() }}</span>
</NuxtLink>
</li>
</ul>
<p v-if="current.key === 'numerique' && stacks?.length" class="panel-stacks">
<span class="panel-stacks-label">{{ stacksLabel }}</span>
<span v-for="s in stacks.slice(0, maxStacks)" :key="s.name" class="panel-stack">{{ s.name }}</span>
<NuxtLink v-if="stacks.length > maxStacks && stacksTo" :to="stacksTo" class="panel-stacks-more">
+{{ stacks.length - maxStacks }}
</NuxtLink>
</p>
<NuxtLink :to="current.to" class="panel-more">
Voir l'axe {{ current.short }}
<span class="i-lucide-arrow-right h-4 w-4" aria-hidden="true" />
</NuxtLink>
</div>
</Transition>
</div>
</div>
</template>
<script setup lang="ts">
import type { AxisKey, HomeAxis, HomeStack } from '~/composables/useHomeData'
const props = withDefaults(defineProps<{
axes: HomeAxis[]
prefix?: string
topLabel?: string
bottomLabel?: string
upstreamLabel?: string
stacks?: HomeStack[]
stacksLabel?: string
stacksTo?: string
maxStacks?: number
compact?: boolean
}>(), {
prefix: 'Autonomie',
topLabel: 'Émancipation',
bottomLabel: 'Asservissement',
maxStacks: 8,
upstreamLabel: 'en amont',
stacksLabel: 'En service',
compact: false,
})
const active = defineModel<AxisKey>({ default: 'numerique' })
const uid = useId()
const root = ref<HTMLElement | null>(null)
const tabs = ref<HTMLButtonElement[]>([])
const current = computed(() => props.axes.find(a => a.key === active.value) ?? props.axes[0])
// ── Motion ──────────────────────────────────────────────────────────────
// The cursors move on their own, slowly: each one drifts, and every few
// beats another axis is pushed up towards émancipation. Visitors watch it;
// they can still pick an axis (click, arrows), which stops the rotation.
// SSR renders every cursor at the bottom, so the first move is the rise.
const BEAT_MS = 2600
const BEATS_PER_AXIS = 3
const levels = reactive<Partial<Record<AxisKey, number>>>({})
const reduced = ref(false)
const hovered = ref(false)
const visible = ref(true)
const chosen = ref(false)
let beat = 0
let timer: ReturnType<typeof setInterval> | undefined
function place() {
props.axes.forEach((axis, i) => {
const isActive = axis.key === active.value
if (reduced.value) {
levels[axis.key] = isActive ? 0.94 : 0.3
return
}
const wave = Math.sin(beat * 0.9 + i * 2.1)
levels[axis.key] = isActive ? 0.87 + 0.06 * wave : 0.3 + 0.14 * wave
})
}
function advance() {
const idx = props.axes.findIndex(a => a.key === active.value)
active.value = props.axes[(idx + 1) % props.axes.length]!.key
}
function onBeat() {
if (!visible.value) return
beat++
if (!chosen.value && !hovered.value && beat % BEATS_PER_AXIS === 0) advance()
place()
}
function choose(key: AxisKey) {
chosen.value = true
active.value = key
}
watch(active, place)
function onKeydown(e: KeyboardEvent) {
const keys = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End']
if (!keys.includes(e.key)) return
e.preventDefault()
const idx = props.axes.findIndex(a => a.key === active.value)
const last = props.axes.length - 1
let next = idx
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') next = idx === last ? 0 : idx + 1
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = idx === 0 ? last : idx - 1
if (e.key === 'Home') next = 0
if (e.key === 'End') next = last
choose(props.axes[next]!.key)
nextTick(() => tabs.value[next]?.focus())
}
onMounted(() => {
reduced.value = window.matchMedia('(prefers-reduced-motion: reduce)').matches
requestAnimationFrame(place)
if (reduced.value) return
timer = setInterval(onBeat, BEAT_MS)
const io = new IntersectionObserver(([entry]) => { visible.value = !!entry?.isIntersecting })
if (root.value) io.observe(root.value)
onUnmounted(() => {
clearInterval(timer)
io.disconnect()
})
})
</script>
<style scoped>
.console {
--track-h: 11rem;
--cap-h: 1.25rem;
display: grid;
gap: 1.25rem;
}
.console--compact {
--track-h: 8rem;
}
/* ── Board ── */
.console-board {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.9rem;
padding: 1.25rem 1.25rem 1rem;
border-radius: 16px;
background: hsl(var(--color-surface));
box-shadow:
inset 0 1px 0 hsl(var(--color-text) / 0.04),
0 18px 40px -24px hsl(var(--color-text) / 0.35);
}
/* Vertical scale, read from bottom to top */
.console-scale {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
height: var(--track-h);
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.02em;
white-space: nowrap;
}
.console-scale-bottom {
color: hsl(var(--color-text) / 0.45);
}
.console-scale-top {
font-weight: 800;
color: hsl(var(--color-text));
}
.console-scale-line {
flex: 1;
width: 1px;
background: linear-gradient(to bottom, hsl(var(--color-text) / 0.12), hsl(var(--color-text) / 0.45));
}
.console-faders {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
/* ── Fader ── */
.fader {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.55rem;
padding: 0 0.25rem 0.25rem;
border-radius: 12px;
cursor: pointer;
/* --level is a registered property (main.css): the cursors glide */
transition: --level 2.6s cubic-bezier(0.45, 0.05, 0.55, 0.95);
transition-delay: calc(var(--i) * 160ms);
}
.fader:focus-visible {
outline: 2px solid hsl(var(--hue));
outline-offset: 2px;
}
.fader-track {
position: relative;
width: 0.5rem;
height: var(--track-h);
border-radius: 999px;
background: hsl(var(--color-text) / 0.1);
}
.fader-fill {
position: absolute;
inset: auto 0 0 0;
height: calc(var(--level) * (var(--track-h) - var(--cap-h)) + var(--cap-h) / 2);
border-radius: inherit;
background: hsl(var(--hue) / 0.45);
transition: background 0.6s ease;
}
.fader--active .fader-fill {
background: hsl(var(--hue));
}
.fader-cap {
position: absolute;
left: 50%;
bottom: 0;
width: 2.6rem;
height: var(--cap-h);
margin-left: -1.3rem;
border-radius: 6px;
background: hsl(var(--color-bg));
box-shadow:
0 1px 0 hsl(var(--color-text) / 0.08),
0 4px 10px -2px hsl(var(--color-text) / 0.3);
transform: translateY(calc(-1 * var(--level) * (var(--track-h) - var(--cap-h))));
}
/* Cap groove */
.fader-cap::after {
content: '';
position: absolute;
left: 0.45rem;
right: 0.45rem;
top: 50%;
height: 2px;
margin-top: -1px;
border-radius: 2px;
background: hsl(var(--hue));
}
.fader-label {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
}
.fader-prefix {
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: hsl(var(--color-text) / 0.5);
}
.fader-name {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.05rem;
letter-spacing: -0.01em;
color: hsl(var(--color-text) / 0.65);
transition: color 0.6s ease;
}
.fader--active .fader-name {
color: hsl(var(--hue));
}
.fader-lever {
padding: 0.2rem 0.55rem;
border-radius: 20px;
font-family: var(--font-display);
font-weight: 600;
font-size: 0.8rem;
line-height: 1.2;
text-align: center;
color: hsl(var(--color-text) / 0.8);
background: hsl(var(--hue) / 0.1);
transition: background 0.6s ease, color 0.6s ease;
}
.fader--active .fader-lever {
color: hsl(var(--color-bg));
background: hsl(var(--hue));
}
/* ── Panel ── */
.console-panel {
position: relative;
padding: 1.25rem 1.4rem 1.2rem;
border-radius: 16px;
background: hsl(var(--hue) / 0.08);
}
.panel-upstream {
margin: 0 0 0.35rem;
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--hue));
}
.panel-upstream-label {
color: hsl(var(--color-text) / 0.45);
}
.panel-upstream-label::after {
content: ' ↑ ';
}
.panel-title {
margin: 0;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.35rem, 2.6vw, 1.7rem);
letter-spacing: -0.025em;
line-height: 1.1;
color: hsl(var(--color-text));
}
.panel-promise {
margin: 0.5rem 0 0;
font-size: 0.98rem;
line-height: 1.5;
color: hsl(var(--color-text) / 0.78);
}
.panel-tools {
list-style: none;
margin: 0.9rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.panel-tool {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
padding: 0.4rem 0.8rem;
border-radius: 20px;
background: hsl(var(--color-bg) / 0.7);
font-size: 0.85rem;
color: hsl(var(--color-text));
transition: transform 0.1s ease, background 0.2s ease;
}
.panel-tool:hover {
transform: translateY(-1px);
background: hsl(var(--color-bg));
}
.panel-tool:active {
transform: translateY(0);
}
.panel-tool-name {
font-weight: 700;
}
.panel-tool-app {
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--hue));
}
.panel-tool-state {
font-family: var(--font-mono);
font-size: 0.65rem;
color: hsl(var(--color-text) / 0.45);
}
.panel-stacks {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.25rem 0.6rem;
margin: 0.9rem 0 0;
font-size: 0.8rem;
color: hsl(var(--color-text) / 0.7);
}
.panel-stacks-label {
font-family: var(--font-mono);
font-size: 0.68rem;
color: hsl(var(--color-text) / 0.45);
}
.panel-stacks-more {
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--hue));
}
.panel-stack::before {
content: '';
display: inline-block;
width: 0.4rem;
height: 0.4rem;
margin-right: 0.3rem;
border-radius: 50%;
background: hsl(var(--hue));
vertical-align: 0.05em;
}
.panel-more {
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin-top: 1rem;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.92rem;
color: hsl(var(--hue));
}
.panel-more:hover span {
transform: translateX(2px);
}
.panel-more span {
transition: transform 0.12s ease;
}
.panel-enter-active,
.panel-leave-active {
transition: opacity 0.18s ease, transform 0.18s ease;
}
.panel-enter-from {
opacity: 0;
transform: translateY(6px);
}
.panel-leave-to {
opacity: 0;
transform: translateY(-4px);
}
@media (max-width: 480px) {
.console { --track-h: 8.5rem; }
.console-board { padding: 1rem 0.6rem 0.75rem; gap: 0.35rem; }
.console-scale { font-size: 0.62rem; }
.fader-cap { width: 2.2rem; margin-left: -1.1rem; }
.fader-name { font-size: 0.88rem; }
.fader-prefix { font-size: 0.52rem; letter-spacing: 0.08em; }
.fader-lever { font-size: 0.68rem; padding: 0.2rem 0.4rem; }
}
@media (prefers-reduced-motion: reduce) {
.fader,
.fader-fill,
.fader-name,
.fader-lever,
.panel-enter-active,
.panel-leave-active {
transition: none;
}
}
</style>
+362
View File
@@ -0,0 +1,362 @@
<template>
<div class="format">
<header v-if="format && showHeader" class="format-head">
<h2 class="format-title">{{ format.title }}</h2>
<p class="format-text">{{ format.text }}</p>
</header>
<div class="format-grid">
<!-- Éditorial : le livre et les chansons -->
<article v-if="book" class="format-card format-card--book">
<div
class="format-cover"
role="img"
:aria-label="book.coverAlt"
:style="{ backgroundImage: `url(${book.coverImage})` }"
/>
<div class="format-body">
<p class="format-kicker">{{ book.kicker }}</p>
<h3 class="format-card-title">{{ book.title }}</h3>
<p class="format-desc">{{ book.description }}</p>
<div class="format-actions">
<button type="button" class="fbtn fbtn--main" @click="$emit('open-player')">
<span class="i-lucide-play h-4 w-4" aria-hidden="true" />
{{ book.cta?.player }}
</button>
<button type="button" class="fbtn" @click="$emit('open-pdf')">
<span class="i-lucide-book-open h-4 w-4" aria-hidden="true" />
{{ book.cta?.pdf }}
</button>
<NuxtLink to="/economique/modele-eco" class="fbtn fbtn--quiet">
<span class="i-lucide-list h-4 w-4" aria-hidden="true" />
Présentation des chapitres
</NuxtLink>
<NuxtLink to="/economique/commande" class="fbtn fbtn--quiet">
<span class="i-lucide-shopping-bag h-4 w-4" aria-hidden="true" />
Commandez le livre
</NuxtLink>
</div>
</div>
</article>
<!-- Événementiel -->
<NuxtLink v-if="evenement" :to="evenement.to" class="format-card format-card--event">
<p class="format-kicker">
{{ evenement.teaser }}
<span v-if="evenement.gestation" class="format-state">en gestation</span>
</p>
<h3 class="format-card-title format-card-title--event">{{ evenement.subtitle }}</h3>
<p v-if="evenement.tagline" class="format-tagline">{{ evenement.tagline }}</p>
<ul v-if="evenement.config.length" class="format-config">
<li v-for="c in evenement.config" :key="c.label">
<span :class="`i-lucide-${c.icon}`" class="h-4 w-4" aria-hidden="true" />
<span>{{ c.label }}</span>
</li>
</ul>
<span class="format-more">
Voir l'événement
<span class="i-lucide-arrow-right h-4 w-4" aria-hidden="true" />
</span>
</NuxtLink>
</div>
<!-- Reproduction + à qui l'on s'adresse -->
<div v-if="evenement?.reproduction || hero?.addressees?.length" class="format-repro">
<p v-if="evenement?.reproduction" class="format-repro-line">
<span class="format-basins" aria-hidden="true"><i v-for="n in 5" :key="n" /></span>
{{ evenement.reproduction }}
</p>
<div v-if="hero?.addressees?.length" class="format-audience">
<p class="format-audience-label">{{ hero.audience }}</p>
<ul>
<li v-for="a in hero.addressees" :key="a.label">
<strong>{{ a.label }}</strong> {{ a.value }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
format?: { title: string, text: string }
book?: any
evenement?: any
hero?: any
showHeader?: boolean
}>(), {
showHeader: true,
})
defineEmits<{
'open-player': []
'open-pdf': []
}>()
</script>
<style scoped>
.format {
display: grid;
gap: 1.5rem;
}
.format-head {
max-width: 44rem;
}
.format-title {
margin: 0;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.75rem, 4.2vw, 2.75rem);
letter-spacing: -0.03em;
line-height: 1.05;
color: hsl(var(--color-text));
}
.format-text {
margin: 0.75rem 0 0;
font-size: 1.1rem;
line-height: 1.5;
color: hsl(var(--color-text) / 0.72);
}
.format-grid {
display: grid;
gap: 1.25rem;
}
@media (min-width: 1024px) {
.format-grid { grid-template-columns: 1.35fr 1fr; }
}
.format-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.5rem;
border-radius: 16px;
background: hsl(var(--color-surface));
}
.format-card--book {
flex-direction: row;
gap: 1.5rem;
}
.format-cover {
flex-shrink: 0;
width: clamp(6.5rem, 16vw, 10rem);
aspect-ratio: 1756 / 2480;
border-radius: 6px;
background-size: cover;
background-position: center;
box-shadow: 0 16px 30px -18px hsl(var(--color-text) / 0.6);
}
.format-body {
min-width: 0;
}
.format-kicker {
margin: 0;
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--axis-eco));
}
.format-card--event .format-kicker {
color: hsl(var(--axis-cit));
}
.format-state {
margin-left: 0.35rem;
color: hsl(var(--color-text) / 0.5);
}
.format-state::before {
content: '◌ ';
}
.format-card-title {
margin: 0.3rem 0 0;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.3rem, 2.4vw, 1.6rem);
letter-spacing: -0.02em;
line-height: 1.12;
color: hsl(var(--color-text));
}
.format-card-title--event {
font-weight: 700;
}
.format-desc {
margin: 0.5rem 0 0;
font-size: 0.95rem;
line-height: 1.5;
color: hsl(var(--color-text) / 0.72);
}
.format-actions {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 1rem;
}
.fbtn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 2.5rem;
padding: 0.45rem 1rem;
border-radius: 20px;
font-size: 0.86rem;
font-weight: 700;
color: hsl(var(--color-text) / 0.85);
background: hsl(var(--color-text) / 0.07);
transition: transform 0.1s ease, background 0.2s ease;
}
.fbtn:hover { transform: translateY(-1px); background: hsl(var(--axis-eco) / 0.14); }
.fbtn:active { transform: translateY(0); }
.fbtn--main {
color: hsl(var(--color-bg));
background: hsl(var(--axis-eco));
}
.fbtn--main:hover { background: hsl(var(--axis-eco) / 0.88); }
.fbtn--quiet {
font-weight: 400;
background: transparent;
}
.format-card--event {
color: hsl(var(--color-text));
transition: transform 0.12s ease;
}
.format-card--event:hover {
transform: translateY(-3px);
}
.format-tagline {
margin: 0.2rem 0 0;
font-family: var(--font-display);
font-size: 1.05rem;
color: hsl(var(--color-text) / 0.7);
}
.format-config {
list-style: none;
margin: 0.75rem 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
font-size: 0.85rem;
color: hsl(var(--color-text) / 0.78);
}
.format-config li {
display: flex;
align-items: center;
gap: 0.45rem;
}
.format-config li > span:first-child {
color: hsl(var(--axis-cit));
flex-shrink: 0;
}
.format-more {
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin-top: auto;
padding-top: 1rem;
font-family: var(--font-display);
font-weight: 700;
color: hsl(var(--axis-cit));
}
/* Reproduction */
.format-repro {
display: grid;
gap: 1.25rem;
padding: 1.25rem 0 0;
}
@media (min-width: 768px) {
.format-repro {
grid-template-columns: 1fr 1fr;
align-items: start;
}
}
.format-repro-line {
margin: 0;
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(1.15rem, 2.2vw, 1.4rem);
line-height: 1.25;
letter-spacing: -0.015em;
color: hsl(var(--color-text));
}
/* One basin, then its copies */
.format-basins {
display: flex;
gap: 0.35rem;
margin-bottom: 0.6rem;
}
.format-basins i {
display: block;
width: 0.8rem;
height: 0.8rem;
border-radius: 50%;
box-shadow: inset 0 0 0 1.5px hsl(var(--axis-cit) / 0.6);
}
.format-basins i:first-child {
background: hsl(var(--axis-cit));
box-shadow: none;
}
.format-audience-label {
margin: 0 0 0.5rem;
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--color-text) / 0.5);
}
.format-audience ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.95rem;
color: hsl(var(--color-text) / 0.7);
}
.format-audience strong {
color: hsl(var(--color-text));
}
@media (max-width: 640px) {
.format-card--book { flex-direction: column; }
.format-config { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
.fbtn, .format-card--event { transition: none; }
}
</style>
+212
View File
@@ -0,0 +1,212 @@
<template>
<div class="va">
<!-- Hero : the headline, and the three cursors to push -->
<section class="va-hero">
<HomeShadoks />
<div class="container-content va-hero-grid">
<div class="va-intro">
<h1 class="va-h1">
<span class="va-h1-a">{{ heading[0] }}</span>
<span class="va-h1-b">{{ heading[1] }}</span>
</h1>
<p class="va-axes">
<template v-for="(seg, i) in axesLine" :key="i">
<span v-if="seg.hue" class="va-axes-word" :style="{ '--hue': `var(${seg.hue})` }">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</p>
<p class="va-means">{{ heading[4] }}</p>
<p v-if="hero.approach" class="va-approach">{{ hero.approach }}</p>
</div>
<div class="va-console">
<p class="va-console-title">{{ heading[2] }}</p>
<HomeConsole
v-model="active"
:axes="axes"
:prefix="consoleLabels?.prefix"
:top-label="consoleLabels?.top"
:bottom-label="consoleLabels?.bottom"
:stacks="bouquet?.inService"
:stacks-to="lpk?.to"
:stacks-label="lpk ? `En service chez ${lpk.name} :` : 'En service :'"
/>
</div>
</div>
</section>
<!-- Pourquoi -->
<section class="va-band">
<div class="container-content">
<HomeCitations :lines="hero.citations ?? []" />
</div>
</section>
<!-- Le format -->
<section class="va-section">
<div class="container-content">
<HomeFormat
:format="format"
:book="book"
:evenement="evenement"
:hero="hero"
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
/>
</div>
</section>
<!-- La boîte à outils -->
<section class="va-section va-section--tools">
<div class="container-content">
<h2 class="va-h2">{{ toolboxTitle }}</h2>
<HomeToolbox
:axes="axes"
:lpk="lpk"
:bouquet="bouquet"
:gratewizard="gratewizard"
anchors
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import type { AxisKey } from '~/composables/useHomeData'
defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { hero, axes, lpk, bouquet, evenement, book, format, gratewizard, consoleLabels, toolboxTitle } = await useHomeData()
const heading = computed<string[]>(() => hero.value.heading ?? [])
// « Autonomie numérique, économique, citoyenne. » — each axis word in its own hue
const AXIS_WORDS: Record<string, string> = {
numérique: '--axis-num',
économique: '--axis-eco',
citoyenne: '--axis-cit',
}
const axesLine = computed(() =>
(heading.value[3] ?? '')
.split(/(numérique|économique|citoyenne)/)
.filter(Boolean)
.map(text => ({ text, hue: AXIS_WORDS[text] })))
const active = ref<AxisKey>('numerique')
</script>
<style scoped>
.va-hero {
position: relative;
overflow: hidden;
padding: clamp(2.5rem, 6vw, 5rem) 1rem clamp(2.5rem, 5vw, 4rem);
}
.va-hero-grid {
position: relative;
z-index: 1;
display: grid;
gap: 2.5rem;
align-items: center;
}
@media (min-width: 1024px) {
.va-hero-grid {
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
gap: 4rem;
}
}
.va-h1 {
margin: 0;
display: flex;
flex-direction: column;
font-family: var(--font-display);
letter-spacing: -0.035em;
line-height: 0.98;
}
.va-h1-a {
font-weight: 800;
font-size: clamp(2.4rem, 6.4vw, 4.6rem);
color: hsl(var(--color-text));
}
.va-h1-b {
margin-top: 0.3rem;
font-weight: 400;
font-size: clamp(1.6rem, 3.8vw, 2.7rem);
letter-spacing: -0.025em;
color: hsl(var(--color-text) / 0.55);
}
.va-axes {
margin: 1.75rem 0 0;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.3rem, 2.6vw, 1.85rem);
letter-spacing: -0.025em;
line-height: 1.15;
color: hsl(var(--color-text));
}
.va-axes-word {
color: hsl(var(--hue));
}
.va-means {
margin: 0.35rem 0 0;
font-family: var(--font-display);
font-style: italic;
font-size: 1.05rem;
color: hsl(var(--color-text) / 0.6);
}
.va-approach {
margin: 1.75rem 0 0;
max-width: 26rem;
font-size: 1rem;
line-height: 1.5;
color: hsl(var(--color-text) / 0.8);
}
.va-approach::after {
content: ' ↓';
color: hsl(var(--color-primary));
}
@media (min-width: 1024px) {
.va-approach::after { content: ' →'; }
}
.va-console-title {
margin: 0 0 0.75rem;
font-family: var(--font-display);
font-weight: 700;
font-size: 1.1rem;
color: hsl(var(--color-accent));
}
.va-band {
padding: clamp(2.5rem, 6vw, 4.5rem) 1rem;
background: hsl(var(--color-surface) / 0.55);
}
.va-section {
padding: clamp(3rem, 7vw, 5.5rem) 1rem 0;
}
.va-h2 {
margin: 0 0 1.5rem;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.75rem, 4.2vw, 2.75rem);
letter-spacing: -0.03em;
line-height: 1.05;
color: hsl(var(--color-text));
}
</style>
+29 -15
View File
@@ -1,6 +1,6 @@
<template>
<section class="section-padding">
<div class="container-content mx-auto max-w-3xl">
<section class="messages-section">
<div class="container-content messages-grid">
<!-- Formulaire -->
<UiScrollReveal>
@@ -49,8 +49,8 @@
<!-- 2 derniers messages publiés -->
<UiScrollReveal v-if="messages?.length" :delay="100">
<div class="mt-8 space-y-4">
<h3 class="font-display text-lg font-bold section-title text-center">Derniers messages</h3>
<div class="space-y-4">
<h3 class="font-display text-lg font-bold section-title">Derniers messages</h3>
<div v-for="msg in messages.slice(0, 2)" :key="msg.id" class="message-card">
<!-- En-tête auteur -->
<div class="flex items-center gap-2 mb-2">
@@ -72,7 +72,7 @@
</div>
</div>
</div>
<div class="text-center">
<div>
<NuxtLink to="/messages" class="btn-ghost text-sm">
Voir tous les messages
<div class="i-lucide-arrow-right ml-1 h-3.5 w-3.5" />
@@ -150,24 +150,39 @@ function formatDate(iso: string) {
.reply-author { color: hsl(var(--color-primary) / 0.8); }
.reply-text { color: hsl(var(--color-text) / 0.62); }
/* ── Mise en page ── */
.messages-section {
padding: clamp(3rem, 7vw, 5.5rem) 1rem 2rem;
}
.messages-grid {
display: grid;
gap: 2rem;
align-items: start;
}
@media (min-width: 1024px) {
.messages-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); }
}
/* ── Carte formulaire ── */
.message-form-card {
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.1);
border-radius: 0.75rem;
border-radius: 16px;
padding: 1.5rem;
}
.msg-input {
width: 100%;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-text) / 0.12);
min-height: 2.5rem;
padding: 0.55rem 0.85rem;
border-radius: 12px;
border: none;
background: hsl(var(--color-bg));
color: hsl(var(--color-text));
font-size: 0.875rem;
font-size: 0.95rem;
font-family: inherit;
transition: border-color 0.2s;
transition: box-shadow 0.2s;
}
.msg-input--select {
@@ -181,14 +196,13 @@ function formatDate(iso: string) {
.msg-input:focus {
outline: none;
border-color: hsl(var(--color-primary) / 0.5);
box-shadow: 0 0 0 2px hsl(var(--color-primary) / 0.5);
}
/* ── Carte message ── */
.message-card {
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.1);
border-radius: 0.75rem;
border-radius: 16px;
padding: 1rem 1.25rem;
}
@@ -1,9 +1,5 @@
<template>
<section class="relative overflow-hidden section-padding hero-section">
<!-- Background gradient -->
<div class="absolute inset-0 bg-gradient-to-b from-primary/10 via-transparent to-surface-bg" />
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(12_76%_48%/0.12),transparent_70%)]" />
<div class="shadoks" aria-hidden="true">
<!-- Shadok bird decoration -->
<svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true">
<ellipse cx="90" cy="100" rx="45" ry="40" fill="currentColor" opacity="0.85"/>
@@ -41,80 +37,47 @@
<rect x="35" y="185" width="40" height="20" rx="2" fill="currentColor" opacity="0.15"/>
<ellipse cx="55" cy="195" rx="12" ry="6" fill="currentColor" opacity="0.12"/>
</svg>
<!-- Content -->
<div class="container-content relative z-10 px-4">
<div class="mx-auto max-w-2xl">
<HomeTypewriterText
v-if="hero"
:hero="hero"
/>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('home')
const hero = computed(() => {
const raw = (content.value as any)?.hero
if (!raw) return null
return {
heading: Array.isArray(raw.heading) ? raw.heading : [],
citations: Array.isArray(raw.citations) ? raw.citations : [],
approach: raw.approach || '',
axes: Array.isArray(raw.axes) ? raw.axes : [],
audience: raw.audience || '',
addressees: Array.isArray(raw.addressees) ? raw.addressees : [],
}
})
</script>
<style scoped>
.hero-section {
min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
.shadoks {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.shadok-bird {
position: absolute;
right: 4%;
top: 12%;
width: clamp(110px, 15vw, 220px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-primary));
right: 3%;
top: 6%;
width: clamp(90px, 10vw, 150px);
opacity: 0.22;
color: hsl(var(--axis-num));
animation: shadok-float 8s ease-in-out infinite;
z-index: 1;
}
@keyframes shadok-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
.shadok-boulanger {
position: absolute;
left: 3%;
bottom: 8%;
width: clamp(100px, 13vw, 190px);
opacity: 0.25;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-boulanger 9s ease-in-out infinite;
z-index: 1;
left: 1.5%;
bottom: 4%;
width: clamp(80px, 9vw, 140px);
opacity: 0.2;
color: hsl(var(--axis-eco));
animation: shadok-float 9s ease-in-out infinite reverse;
}
@keyframes shadok-float-boulanger {
@keyframes shadok-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok-bird { display: none; }
.shadok-boulanger { display: none; }
.shadoks { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.shadok-bird, .shadok-boulanger { animation: none; }
}
</style>
+198
View File
@@ -0,0 +1,198 @@
<template>
<div class="tool" :class="{ 'tool--gestation': tool.gestation }">
<div class="tool-main">
<span class="tool-icon" aria-hidden="true">
<span v-if="tool.icon === 'g1'" class="tool-g1">Ğ1</span>
<span v-else :class="`i-lucide-${tool.icon}`" class="h-4 w-4" />
</span>
<div class="tool-text">
<component :is="tool.to ? NuxtLink : 'span'" :to="tool.to" class="tool-name">
{{ tool.label }}
</component>
<UiStatusBadge class="tool-status" :status="tool.status" :gestation="tool.gestation" :since="tool.since" />
<p class="tool-desc">{{ tool.description }}</p>
<p v-if="tool.presentation" class="tool-app">
<strong>{{ tool.presentation.title }}</strong> — {{ tool.presentation.text }}
</p>
</div>
</div>
<div v-if="tool.actions?.length" class="tool-actions">
<template v-for="action in tool.actions" :key="action.label">
<a
v-if="action.href"
:href="action.href"
target="_blank"
rel="noopener noreferrer"
class="tool-btn"
:class="{ 'tool-btn--main': action.highlight }"
>
<span :class="`i-lucide-${action.icon}`" class="h-3.5 w-3.5" aria-hidden="true" />
{{ action.label }}
</a>
<NuxtLink
v-else-if="action.to"
:to="action.to"
class="tool-btn"
:class="{ 'tool-btn--quiet': action.secondary }"
>
<span :class="`i-lucide-${action.icon}`" class="h-3.5 w-3.5" aria-hidden="true" />
{{ action.label }}
</NuxtLink>
<button
v-else
type="button"
class="tool-btn"
:class="{ 'tool-btn--main': action.highlight }"
@click="$emit('action', action.id)"
>
<span :class="`i-lucide-${action.icon}`" class="h-3.5 w-3.5" aria-hidden="true" />
{{ action.label }}
</button>
</template>
<span v-for="action in tool.actions.filter(a => a.note)" :key="`note-${action.label}`" class="tool-note">
{{ action.note }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { HomeTool } from '~/composables/useHomeData'
defineProps<{
tool: HomeTool
}>()
defineEmits<{
action: [id: string]
}>()
const NuxtLink = resolveComponent('NuxtLink')
</script>
<style scoped>
.tool {
padding: 0.85rem 0.9rem;
border-radius: 12px;
background: hsl(var(--color-bg) / 0.55);
}
.tool-main {
display: flex;
gap: 0.7rem;
}
.tool-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 2rem;
height: 2rem;
border-radius: 10px;
background: hsl(var(--hue) / 0.12);
color: hsl(var(--hue));
}
.tool-g1 {
font-family: var(--font-display);
font-weight: 800;
font-size: 0.8rem;
}
.tool-text {
min-width: 0;
}
.tool-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1rem;
color: hsl(var(--color-text));
}
a.tool-name:hover {
color: hsl(var(--hue));
}
.tool-status {
margin-left: 0.4rem;
}
.tool-note {
align-self: center;
font-family: var(--font-mono);
font-size: 0.66rem;
color: hsl(var(--axis-cit));
}
.tool-desc {
margin: 0.2rem 0 0;
font-size: 0.85rem;
line-height: 1.45;
color: hsl(var(--color-text) / 0.68);
}
.tool-app {
margin: 0.35rem 0 0;
font-size: 0.78rem;
line-height: 1.4;
color: hsl(var(--color-text) / 0.6);
}
.tool-app strong {
font-family: var(--font-mono);
font-weight: 500;
color: hsl(var(--hue));
}
.tool-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.7rem;
padding-left: 2.7rem;
}
.tool-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 2.25rem;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 700;
color: hsl(var(--color-text) / 0.8);
background: hsl(var(--color-text) / 0.06);
transition: transform 0.1s ease, background 0.2s ease;
}
.tool-btn:hover {
transform: translateY(-1px);
background: hsl(var(--hue) / 0.14);
}
.tool-btn:active {
transform: translateY(0);
}
.tool-btn--main {
color: hsl(var(--color-bg));
background: hsl(var(--hue));
}
.tool-btn--main:hover {
background: hsl(var(--hue) / 0.88);
}
.tool-btn--quiet {
font-weight: 400;
background: transparent;
}
@media (prefers-reduced-motion: reduce) {
.tool-btn { transition: none; }
}
</style>
+241
View File
@@ -0,0 +1,241 @@
<template>
<div class="toolbox-wrap">
<div class="toolbox" :class="{ 'toolbox--single': axes.length === 1 }">
<section
v-for="axis in axes"
:id="anchors ? axis.to.slice(1) : undefined"
:key="axis.key"
class="toolbox-axis"
:style="{ '--hue': `var(${axis.hue})` }"
:aria-labelledby="`tb-${uid}-${axis.key}`"
>
<header class="toolbox-head">
<p class="toolbox-lever">{{ axis.lever }}</p>
<h3 :id="`tb-${uid}-${axis.key}`" class="toolbox-title">
<NuxtLink :to="axis.to">{{ axis.title }}</NuxtLink>
</h3>
<p v-if="showPromise && axis.promise" class="toolbox-promise">{{ axis.promise }}</p>
</header>
<ul class="toolbox-tools">
<li v-for="tool in axis.items" :key="tool.label">
<HomeToolItem :tool="tool" @action="onAction" />
</li>
</ul>
<!-- Numérique : the stacks LaPlanK serves -->
<NuxtLink
v-if="axis.key === 'numerique' && lpk && bouquet"
:to="lpk.to"
class="toolbox-lpk"
>
<span class="toolbox-lpk-head">
<strong>{{ lpk.name }}</strong> <span>{{ lpk.text }}</span>
</span>
<span class="toolbox-lpk-stacks">
<span v-for="s in bouquet.inService" :key="s.name" class="toolbox-lpk-stack">{{ s.name }}</span>
</span>
<span class="toolbox-lpk-more">
{{ bouquet.title }} · {{ bouquet.stacks.length }}
<span class="i-lucide-arrow-right h-3.5 w-3.5" aria-hidden="true" />
</span>
</NuxtLink>
<!-- Économique : grateWizard -->
<button
v-if="axis.key === 'economie' && gratewizard"
type="button"
class="toolbox-gw"
@click="launch()"
>
<span class="i-lucide-sparkles h-4 w-4" aria-hidden="true" />
<span class="toolbox-gw-text">
<strong>{{ gratewizard.title }}</strong>
<span>{{ gratewizard.subtitle }}</span>
</span>
<span class="i-lucide-arrow-up-right h-4 w-4" aria-hidden="true" />
</button>
</section>
</div>
</div>
</template>
<script setup lang="ts">
import type { HomeAxis } from '~/composables/useHomeData'
withDefaults(defineProps<{
axes: HomeAxis[]
lpk?: { name: string, text: string, to: string }
bouquet?: { title: string, inService: { name?: string }[], stacks: unknown[] } | null
gratewizard?: { title: string, subtitle: string }
showPromise?: boolean
anchors?: boolean
}>(), {
showPromise: true,
anchors: false,
})
const emit = defineEmits<{
'open-player': []
'open-pdf': []
}>()
const uid = useId()
const { launch } = useGrateWizard()
function onAction(id: string) {
if (id === 'open-player') emit('open-player')
else if (id === 'open-pdf') emit('open-pdf')
}
</script>
<style scoped>
.toolbox-wrap {
container-type: inline-size;
}
.toolbox {
display: grid;
gap: 1.25rem;
}
@container (min-width: 60rem) {
.toolbox:not(.toolbox--single) {
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: start;
}
}
.toolbox-axis {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1.25rem;
border-radius: 16px;
background: hsl(var(--color-surface));
box-shadow: inset 0 3px 0 hsl(var(--hue));
scroll-margin-top: calc(var(--header-height) + 1rem);
}
.toolbox-lever {
margin: 0;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--hue));
}
.toolbox-title {
margin: 0.15rem 0 0;
font-family: var(--font-display);
font-weight: 800;
font-size: 1.3rem;
letter-spacing: -0.02em;
color: hsl(var(--color-text));
}
.toolbox-title a:hover {
color: hsl(var(--hue));
}
.toolbox-promise {
margin: 0.35rem 0 0;
font-size: 0.9rem;
line-height: 1.45;
color: hsl(var(--color-text) / 0.7);
}
.toolbox-tools {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* LaPlanK */
.toolbox-lpk {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.9rem;
border-radius: 12px;
background: hsl(var(--hue) / 0.1);
color: hsl(var(--color-text));
transition: transform 0.12s ease;
}
.toolbox-lpk:hover {
transform: translateY(-3px);
}
.toolbox-lpk-head {
font-size: 0.82rem;
line-height: 1.4;
color: hsl(var(--color-text) / 0.7);
}
.toolbox-lpk-head strong {
font-family: var(--font-display);
font-size: 1rem;
color: hsl(var(--hue));
}
.toolbox-lpk-stacks {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.toolbox-lpk-stack {
padding: 0.15rem 0.55rem;
border-radius: 20px;
font-size: 0.75rem;
background: hsl(var(--color-bg) / 0.75);
}
.toolbox-lpk-more {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--hue));
}
/* grateWizard */
.toolbox-gw {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.85rem 0.9rem;
border-radius: 12px;
text-align: left;
color: hsl(var(--hue));
background: hsl(var(--hue) / 0.1);
transition: transform 0.12s ease;
}
.toolbox-gw:hover {
transform: translateY(-3px);
}
.toolbox-gw-text {
flex: 1;
display: flex;
flex-direction: column;
font-size: 0.8rem;
color: hsl(var(--color-text) / 0.7);
}
.toolbox-gw-text strong {
font-family: var(--font-display);
font-size: 1rem;
color: hsl(var(--color-text));
}
@media (prefers-reduced-motion: reduce) {
.toolbox-lpk, .toolbox-gw { transition: none; }
}
</style>
-279
View File
@@ -1,279 +0,0 @@
<template>
<div class="hero-content">
<!-- 5-line heading with alternating typography -->
<div class="hero-heading">
<h1 v-if="hero.heading?.length" class="hero-lines">
<span
v-for="(line, i) in hero.heading"
:key="i"
class="hero-line"
:class="`hero-line--${i + 1}`"
>{{ line }}</span>
</h1>
</div>
<!-- Discrete aside block -->
<details class="hero-aside">
<summary class="hero-aside-toggle">En savoir plus</summary>
<div class="hero-aside-body">
<!-- Citations -->
<blockquote v-if="hero.citations?.length" class="hero-citations">
<p v-for="(cite, i) in hero.citations" :key="i" class="hero-cite">
{{ cite }}
</p>
</blockquote>
<!-- Approach + Axes -->
<div v-if="hero.approach" class="hero-approach">
<p class="hero-approach-text">{{ hero.approach }}</p>
<dl v-if="hero.axes?.length" class="hero-axes">
<div v-for="(axis, i) in hero.axes" :key="i" class="hero-axis">
<dt>{{ axis.label }}</dt>
<dd>{{ axis.value }}</dd>
</div>
</dl>
</div>
<!-- Audience + Addressees -->
<div v-if="hero.audience" class="hero-approach hero-audience">
<p class="hero-approach-text">{{ hero.audience }}</p>
<dl v-if="hero.addressees?.length" class="hero-axes">
<div v-for="(item, i) in hero.addressees" :key="i" class="hero-axis">
<dt>{{ item.label }}</dt>
<dd>{{ item.value }}</dd>
</div>
</dl>
</div>
</div>
</details>
</div>
</template>
<script setup lang="ts">
interface HeroAxis {
label: string
value: string
}
interface HeroData {
heading: string[]
citations: string[]
approach: string
axes: HeroAxis[]
audience: string
addressees: HeroAxis[]
}
defineProps<{
hero: HeroData
}>()
</script>
<style scoped>
.hero-content {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
/* ── Heading — 5 lines ── */
.hero-heading {
margin-bottom: 1.5rem;
}
.hero-lines {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1em;
}
.hero-line {
display: block;
font-family: var(--font-display);
line-height: 1.3;
}
/* l1: Construire une autonomie collective. — bold, large */
.hero-line--1 {
font-weight: 700;
font-size: clamp(1.45rem, 3.8vw, 2.5rem);
color: hsl(var(--color-text) / 0.95);
letter-spacing: -0.02em;
}
/* l2: à l'échelle des bassins de vie — light, same size, softer */
.hero-line--2 {
font-weight: 300;
font-size: clamp(1.3rem, 3.2vw, 2.1rem);
color: hsl(var(--color-text) / 0.55);
letter-spacing: -0.01em;
}
/* l3: Pousser les curseurs — accent color, medium */
.hero-line--3 {
font-weight: 600;
font-size: clamp(1.1rem, 2.6vw, 1.6rem);
color: hsl(var(--color-accent));
margin-top: 0.5em;
letter-spacing: 0.02em;
}
/* l4: Autonomie numérique, économique, citoyenne. — small-caps feel */
.hero-line--4 {
font-family: var(--font-sans);
font-weight: 500;
font-size: clamp(0.9rem, 2vw, 1.15rem);
color: hsl(var(--color-text) / 0.65);
letter-spacing: 0.06em;
text-transform: uppercase;
margin-top: 0.15em;
}
/* l5: — s'en donner les moyens — — italic, same tone as l4 */
.hero-line--5 {
font-style: italic;
font-weight: 400;
font-size: clamp(0.95rem, 2.2vw, 1.2rem);
color: hsl(var(--color-text) / 0.65);
margin-top: 0.3em;
}
/* ── Discrete aside block ── */
.hero-aside {
width: 100%;
max-width: 36em;
}
.hero-aside-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin: 0 auto;
padding: 0.35rem 0.85rem;
border-radius: 9999px;
font-family: var(--font-sans);
font-size: 0.78rem;
font-weight: 500;
color: hsl(var(--color-text) / 0.35);
border: 1px solid hsl(var(--color-text) / 0.08);
cursor: pointer;
transition: all 0.2s;
list-style: none;
}
.hero-aside-toggle::-webkit-details-marker {
display: none;
}
.hero-aside-toggle::before {
content: '▸';
font-size: 0.65em;
transition: transform 0.2s;
}
.hero-aside[open] .hero-aside-toggle::before {
transform: rotate(90deg);
}
.hero-aside-toggle:hover {
color: hsl(var(--color-text) / 0.55);
border-color: hsl(var(--color-text) / 0.15);
}
.hero-aside-body {
margin-top: 1.25rem;
animation: aside-reveal 0.3s ease;
}
@keyframes aside-reveal {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Citations ── */
.hero-citations {
margin: 0 0 1.25rem;
padding: 0.6rem 0 0.6rem 1rem;
border-left: 2px solid hsl(var(--color-accent) / 0.3);
text-align: left;
}
.hero-cite {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: 0.92rem;
line-height: 1.55;
color: hsl(var(--color-text) / 0.45);
margin: 0;
}
.hero-cite + .hero-cite {
margin-top: 0.25rem;
}
/* ── Approach + Axes ── */
.hero-approach {
text-align: center;
}
.hero-audience {
margin-top: 1rem;
padding-top: 0.75rem;
border-top: 1px solid hsl(var(--color-text) / 0.07);
}
.hero-approach-text {
font-family: var(--font-sans);
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.03em;
text-transform: uppercase;
color: hsl(var(--color-text) / 0.35);
margin: 0 0 0.6rem;
}
.hero-axes {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
margin: 0;
}
.hero-axis {
display: flex;
align-items: baseline;
gap: 0.4rem;
justify-content: center;
}
.hero-axis dt {
font-family: var(--font-display);
font-weight: 600;
font-size: 0.9rem;
color: hsl(var(--color-primary));
}
.hero-axis dt::after {
content: ' →';
color: hsl(var(--color-text) / 0.2);
font-weight: 400;
}
.hero-axis dd {
font-family: var(--font-sans);
font-size: 0.88rem;
color: hsl(var(--color-text) / 0.5);
margin: 0;
}
</style>
+29 -21
View File
@@ -3,22 +3,7 @@
<div class="container-content flex h-[var(--header-height)] items-center justify-between px-4">
<!-- Logo -->
<NuxtLink to="/" class="logo-link flex items-center gap-2.5">
<svg class="logo-icon" viewBox="0 0 46 78" fill="none" aria-hidden="true">
<defs>
<linearGradient id="sect-grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="hsl(var(--color-primary))"/>
<stop offset="100%" stop-color="hsl(var(--color-accent))"/>
</linearGradient>
</defs>
<path
d="M 33 10 C 26 7 18 7 14 11 C 8 15 7 24 10 31 C 13 38 21 40 27 44 C 33 48 38 55 35 62 C 32 69 24 72 17 70 C 10 68 8 72 10 75 C 12 78 20 79 28 76"
stroke="url(#sect-grad)"
stroke-width="5"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</svg>
<UiSectionSign class="logo-icon" />
<span class="logo-text">{{ site?.identity.name }}</span>
</NuxtLink>
@@ -27,10 +12,11 @@
<!-- Autonomie : prefix + axis buttons -->
<span class="nav-prefix">Autonomie&thinsp;:</span>
<NuxtLink
v-for="item in axes"
v-for="(item, i) in axes"
:key="item.to"
:to="item.to"
class="btn-ghost text-sm"
class="btn-ghost text-sm nav-axis"
:style="{ '--hue': `var(${AXIS_HUES[i] ?? '--color-primary'})` }"
active-class="!text-[hsl(var(--color-text))] bg-[hsl(var(--color-text)/0.06)]"
>
{{ item.label }}
@@ -73,6 +59,9 @@ const isMobileMenuOpen = ref(false)
const axes = computed(() => (site.value as any)?.navigation?.axes ?? [])
const extra = computed(() => (site.value as any)?.navigation?.extra ?? [])
const allNav = computed(() => [...axes.value, ...extra.value])
// Same order as navigation.axes in site.yml: numérique, économique, citoyenne
const AXIS_HUES = ['--axis-num', '--axis-eco', '--axis-cit']
</script>
<style scoped>
@@ -84,13 +73,23 @@ const allNav = computed(() => [...axes.value, ...extra.value])
}
.logo-icon {
width: 1.6rem;
height: 2rem;
width: auto;
height: 2.1rem;
flex-shrink: 0;
transform: rotate(-8deg);
transition: transform 0.2s ease;
}
.logo-link:hover .logo-icon {
transform: rotate(0deg) scale(1.08);
}
@media (prefers-reduced-motion: reduce) {
.logo-icon { transition: none; }
}
.logo-text {
font-family: var(--font-display);
font-family: var(--font-logo);
font-weight: 400;
font-size: 1.25rem;
letter-spacing: 0.04em;
@@ -110,6 +109,15 @@ const allNav = computed(() => [...axes.value, ...extra.value])
margin-right: 0.125rem;
}
.nav-axis::before {
content: '';
width: 0.45rem;
height: 0.45rem;
margin-right: 0.45rem;
border-radius: 50%;
background: hsl(var(--hue));
}
.nav-sep {
display: block;
width: 1px;
@@ -179,6 +179,7 @@ onClickOutside(widgetRef, () => {
.mini-player {
position: fixed;
bottom: 1rem;
transition: bottom 0.3s ease;
right: max(1rem, calc((100vw - 80rem) / 2));
z-index: 70;
display: flex;
@@ -186,6 +187,11 @@ onClickOutside(widgetRef, () => {
align-items: flex-end;
}
/* Book player open: stay above its navigation bar */
:global(html.book-player-open .mini-player) {
bottom: 5.5rem;
}
/* ═══════════════════════════════════════
PILL
═══════════════════════════════════════ */
+16
View File
@@ -0,0 +1,16 @@
<template>
<!-- § — glyphe de Playfair Display 700 (SIL OFL), vectorisé ; dégradé primaire → accent -->
<svg class="section-sign" viewBox="0 0 511 970" aria-hidden="true">
<defs>
<linearGradient :id="gradId" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="hsl(var(--color-primary))" />
<stop offset="100%" stop-color="hsl(var(--color-accent))" />
</linearGradient>
</defs>
<path :fill="`url(#${gradId})`" d="M277 0Q320 0 349 7.5Q378 15 395 22Q426 37 443.5 59.5Q461 82 461 115Q461 143 441.5 164Q422 185 387 185Q354 185 332.5 165.5Q311 146 311 116Q311 92 324.5 71Q338 50 362 42Q357 34 344.5 29Q332 24 316 21.5Q300 19 284 19Q246 19 223 32.5Q200 46 190 66.5Q180 87 180 110Q180 150 200 177.5Q220 205 256 230Q292 255 340 287Q394 324 432.5 354.5Q471 385 491 418Q511 451 511 495Q511 548 475.5 593Q440 638 352 677L343 667Q373 646 388 623Q403 600 403 567Q403 536 386 511.5Q369 487 333.5 459Q298 431 241 394Q178 353 135.5 318.5Q93 284 71.5 248Q50 212 50 167Q50 116 78.5 78.5Q107 41 158.5 20.5Q210 0 277 0ZM158 292 168 303Q137 323 122.5 346Q108 369 108 403Q108 433 124.5 458Q141 483 177 511Q213 539 270 576Q332 617 374.5 651.5Q417 686 439 722Q461 758 461 803Q461 853 432 891Q403 929 352 949.5Q301 970 233 970Q190 970 161 962.5Q132 955 115 948Q85 933 67 910.5Q49 888 49 855Q49 827 69 806Q89 785 123 785Q157 785 178 804.5Q199 824 199 854Q199 878 185.5 898.5Q172 919 148 928Q152 935 165 940.5Q178 946 194.5 948.5Q211 951 226 951Q265 951 287.5 937.5Q310 924 320.5 903Q331 882 331 860Q331 820 311 792.5Q291 765 255 740Q219 715 171 683Q116 646 78 615.5Q40 585 20 551.5Q0 518 0 475Q0 421 34.5 376Q69 331 158 292Z" />
</svg>
</template>
<script setup lang="ts">
const gradId = `sect-grad-${useId()}`
</script>
+69
View File
@@ -0,0 +1,69 @@
<template>
<span v-if="label" class="status" :class="`status--${tone}`">
<span class="status-dot" aria-hidden="true" />
<span>{{ label }}</span>
<span v-if="since" class="status-since">· {{ since }}</span>
</span>
</template>
<script setup lang="ts">
// Project maturity, as written in YAML: `status: En production` (+ `since`),
// `status: En développement`, or the legacy `gestation: true`.
const props = defineProps<{
status?: string
gestation?: boolean
since?: string
}>()
const label = computed(() => props.status || (props.gestation ? 'En gestation' : ''))
const tone = computed(() => {
const s = label.value.toLowerCase()
if (s.includes('production')) return 'prod'
if (s.includes('développement') || s.includes('developpement')) return 'dev'
return 'gest'
})
</script>
<style scoped>
.status {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.15rem 0.6rem;
border-radius: 20px;
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 500;
line-height: 1.5;
white-space: nowrap;
vertical-align: middle;
color: hsl(var(--color-text) / 0.75);
background: hsl(var(--color-text) / 0.06);
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
flex-shrink: 0;
}
/* En production : plein */
.status--prod .status-dot { background: hsl(var(--axis-cit)); }
.status--prod { color: hsl(var(--axis-cit)); background: hsl(var(--axis-cit) / 0.1); }
/* En développement : demi-plein */
.status--dev .status-dot {
background: linear-gradient(to top, hsl(var(--axis-eco)) 50%, transparent 50%);
box-shadow: inset 0 0 0 1.5px hsl(var(--axis-eco));
}
.status--dev { color: hsl(var(--axis-eco)); background: hsl(var(--axis-eco) / 0.1); }
/* En gestation : cercle */
.status--gest .status-dot { box-shadow: inset 0 0 0 1.5px hsl(var(--color-text) / 0.45); }
.status-since {
opacity: 0.75;
}
</style>
+5 -19
View File
@@ -71,11 +71,9 @@ export function useAudioPlayer() {
store.setSong(song)
firedMilestones.clear()
// Try OGG first, fall back to MP3
const oggPath = song.file.replace(/\.mp3$/, '.ogg')
const canOgg = el.canPlayType('audio/ogg; codecs=vorbis')
el.src = canOgg ? oggPath : song.file
// Only MP3 files are published (no .ogg on disk nor in production):
// asking for OGG first cost a 404 and a delay on every track.
el.src = song.file
el.volume = store.volume
try {
@@ -84,20 +82,8 @@ export function useAudioPlayer() {
startTimeUpdate()
trackAudioPlay(song.id, song.title)
}
catch {
// If OGG failed, try MP3
if (el.src !== song.file) {
el.src = song.file
try {
await el.play()
store.play()
startTimeUpdate()
trackAudioPlay(song.id, song.title)
}
catch (err) {
console.error('Playback failed:', err)
}
}
catch (err) {
console.error('Playback failed:', err)
}
}
+151
View File
@@ -0,0 +1,151 @@
// Normalised view of the home content, shared by the three home variants.
// Everything comes from YAML (site/pages/home.yml, site/site.yml,
// site/pages/evenement.yml, site/pages/numerique/cloud-libre.yml) — no copy here.
export type AxisKey = 'numerique' | 'economie' | 'politique'
export type StackStatus = 'service' | 'catalogue' | 'avenir'
export interface HomeAction {
id: string
label: string
icon: string
highlight?: boolean
secondary?: boolean
to?: string
href?: string
/** Short status line shown under the button (e.g. the v1 in production) */
note?: string
}
export interface HomeTool {
label: string
description: string
to?: string
href?: string
gestation?: boolean
/** « En production », « En développement »… — wins over `gestation` */
status?: string
since?: string
icon?: string
presentation?: { title: string, text: string }
actions?: HomeAction[]
}
export interface HomeStack {
role: string
name?: string
status: StackStatus
url?: string
}
export interface HomeAxis {
key: AxisKey
/** Short label as used in the navigation ("numérique", "économique", "citoyenne") */
short: string
title: string
icon: string
to: string
/** What is "le plus en amont" on this axis (hero.axes) */
lever: string
promise: string
/** CSS custom property holding the axis hue */
hue: string
items: HomeTool[]
}
const AXIS_META: Record<AxisKey, { heroLabel: string, to: string, hue: string, short: string }> = {
numerique: { heroLabel: 'numérique', to: '/numerique', hue: '--axis-num', short: 'numérique' },
economie: { heroLabel: 'économie', to: '/economique', hue: '--axis-eco', short: 'économique' },
politique: { heroLabel: 'citoyenne', to: '/citoyenne', hue: '--axis-cit', short: 'citoyenne' },
}
export const AXIS_ORDER: AxisKey[] = ['numerique', 'economie', 'politique']
export async function useHomeData() {
const [{ data: home }, { data: site }, { data: event }, { data: cloud }] = await Promise.all([
usePageContent('home'),
useSiteContent(),
usePageContent('evenement'),
usePageContent('numerique/cloud-libre'),
])
const hero = computed(() => (home.value as any)?.hero ?? {})
const axes = computed<HomeAxis[]>(() => {
const raw = (home.value as any)?.axes ?? {}
const levers: { label: string, value: string }[] = hero.value.axes ?? []
return AXIS_ORDER.filter(k => raw[k]).map((key) => {
const meta = AXIS_META[key]
return {
key,
short: meta.short,
title: raw[key].title,
icon: raw[key].icon,
to: meta.to,
lever: levers.find(l => l.label === meta.heroLabel)?.value ?? '',
promise: raw[key].promise ?? '',
hue: meta.hue,
items: raw[key].items ?? [],
}
})
})
const lpk = computed(() => (site.value as any)?.lpk as
{ name: string, text: string, role: string, to: string } | undefined)
/** Bouquet sweetHome, flattened — the stacks served by LaPlanK */
const bouquet = computed(() => {
const section = ((cloud.value as any)?.sections ?? []).find((s: any) => s.type === 'territoire')
const b = section?.bouquet
if (!b) return null
const stacks: (HomeStack & { category: string })[] = []
for (const cat of b.categories ?? []) {
for (const item of cat.items ?? []) {
if (typeof item === 'string') continue
stacks.push({ ...item, category: cat.label })
}
}
return {
title: b.title as string,
statuses: (b.statuses ?? {}) as Record<StackStatus, string>,
categories: (b.categories ?? []) as { label: string, icon: string, items: HomeStack[] }[],
stacks,
inService: stacks.filter(s => s.status === 'service' && s.name),
}
})
const evenement = computed(() => {
const e = event.value as any
const teaser = (home.value as any)?.evenement
if (!e && !teaser) return null
const politique = (e?.axes ?? []).find((a: any) => a.label === 'Politique')
return {
to: teaser?.to ?? '/evenement',
title: e?.title ?? teaser?.title,
teaser: teaser ? `${teaser.title} ${teaser.subtitle}` : '',
subtitle: e?.subtitle ?? '',
tagline: e?.tagline ?? '',
leitmotiv: e?.leitmotiv ?? '',
description: e?.description ?? '',
gestation: e?.gestation ?? teaser?.gestation,
config: (e?.config ?? []) as { icon: string, label: string, detail: string }[],
espaces: (e?.espaces ?? []) as { icon: string, label: string, desc: string }[],
/** "Autonomie d'un bassin de vie — accessible et reproductible" */
reproduction: (politique?.items?.[0] as string) ?? '',
}
})
return {
home,
hero,
axes,
lpk,
bouquet,
evenement,
book: computed(() => (home.value as any)?.book),
format: computed(() => (home.value as any)?.format as { title: string, text: string } | undefined),
toolboxTitle: computed(() => (home.value as any)?.toolbox?.title as string | undefined),
consoleLabels: computed(() => (home.value as any)?.console as { prefix: string, top: string, bottom: string } | undefined),
gratewizard: computed(() => (home.value as any)?.gratewizard),
}
}
+6
View File
@@ -14,6 +14,12 @@ interface SiteContent {
credits: string
links: NavItem[]
}
lpk?: {
name: string
text: string
role: string
to: string
}
gratewizard: {
url: string
popup: {
+2 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="section-padding">
<div class="axis-scope axis-scope--cit section-padding">
<div class="container-content">
<!-- Header -->
@@ -86,10 +86,7 @@
<p style="color: hsl(var(--color-text-muted))" class="leading-relaxed">
{{ content.project.text }}
</p>
<span v-if="content?.gestation" class="gestation-badge mt-3">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="mt-3" :status="content?.status" :gestation="content?.gestation" />
</div>
<!-- Extended content -->
+10 -7
View File
@@ -1,5 +1,5 @@
<template>
<div class="relative overflow-hidden section-padding">
<div class="axis-scope axis-scope--cit relative overflow-hidden section-padding">
<!-- 1. Shadok Capitaine (top-left, profile — at ship's wheel, telescope) -->
<svg class="shadok shadok-capitaine" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
@@ -352,6 +352,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Décision collective
</h2>
<UiStatusBadge class="ml-auto" v-bind="statusOf('decision')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Se donner les moyens de la décision collective.
@@ -376,10 +377,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Tarifs de l'eau
</h2>
<span class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="ml-auto" v-bind="statusOf('tarifs-eau')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Application pour obtenir justice sociale et incitation dynamique à la réduction.
@@ -404,6 +402,10 @@ definePageMeta({
})
const { data: content } = await usePageContent('citoyenne')
const statusOf = (id: string) => {
const item = ((content.value as any)?.items ?? []).find((i: any) => i.id === id) ?? {}
return { status: item.status, since: item.since, gestation: item.gestation }
}
const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
@@ -421,8 +423,8 @@ useSeoPage({
}
.item-card {
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
border-radius: 16px;
border: none;
background: hsl(var(--color-surface));
transition: border-color 0.2s;
overflow: hidden;
@@ -449,6 +451,7 @@ useSeoPage({
.item-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
+12 -9
View File
@@ -1,5 +1,5 @@
<template>
<div class="relative overflow-hidden section-padding">
<div class="axis-scope axis-scope--eco relative overflow-hidden section-padding">
<!-- Shadok boulangère (top-left, walking profile, carrying bread tray) -->
<svg class="shadok shadok-boulangere" viewBox="0 0 160 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -396,6 +396,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Monnaie libre
</h2>
<UiStatusBadge class="ml-auto" v-bind="statusOf('monnaie-libre')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
La Ğ1 (June) : une monnaie co-créée par ses membres, sans dette ni intérêt. Le dividende universel comme base.
@@ -424,10 +425,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Productions collectives
</h2>
<span class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="ml-auto" v-bind="statusOf('productions-collectives')" />
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie. Passer la seconde.
@@ -451,6 +449,10 @@ definePageMeta({
})
const { data: content } = await usePageContent('economique')
const statusOf = (id: string) => {
const item = ((content.value as any)?.items ?? []).find((i: any) => i.id === id) ?? {}
return { status: item.status, since: item.since, gestation: item.gestation }
}
useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie économique',
@@ -469,8 +471,8 @@ const showPdfReader = ref(false)
.item-card {
display: block;
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
border-radius: 16px;
border: none;
background: hsl(var(--color-surface));
transition: border-color 0.2s, transform 0.12s ease;
text-decoration: none;
@@ -484,6 +486,7 @@ const showPdfReader = ref(false)
.item-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
@@ -524,8 +527,8 @@ const showPdfReader = ref(false)
.book-block {
padding: 1.25rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
border-radius: 16px;
border: none;
background: hsl(var(--color-surface));
}
+2 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="section-padding">
<div class="axis-scope axis-scope--eco section-padding">
<div class="container-content">
<!-- Header -->
@@ -17,6 +17,7 @@
<p class="text-lg leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<UiStatusBadge class="mt-4" :status="content?.status" :since="content?.since" />
</div>
<!-- Navigation prev / index / next -->
@@ -1,5 +1,5 @@
<template>
<div class="section-padding">
<div class="axis-scope axis-scope--eco section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
@@ -18,10 +18,9 @@
{{ content?.description }}
</p>
<span v-if="content?.gestation" class="gestation-badge mx-auto mb-8">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<div class="mb-8 text-center">
<UiStatusBadge :status="content?.status" :gestation="content?.gestation" />
</div>
<!-- Content -->
<div v-if="content?.content" class="prose-block mb-10">
+14 -10
View File
@@ -332,20 +332,16 @@
<div class="container-content relative z-10 text-center">
<!-- Logo SVG vectoriel — inline pour currentColor -->
<div class="evt-logo-wrap mx-auto mb-6" aria-hidden="true">
<svg viewBox="0 0 64 80" fill="none" class="evt-logo">
<path d="M 28 6 C 44 2,52 12,47 14 C 40 18,16 18,12 28 C 8 38,16 50,28 54 C 38 56,52 60,52 68 C 52 76,42 78,34 74"
stroke="currentColor" stroke-width="16" stroke-linecap="round" stroke-linejoin="round" opacity="0.12"/>
<path d="M 28 6 C 44 2,52 12,47 14 C 40 18,16 18,12 28 C 8 38,16 50,28 54 C 38 56,52 60,52 68 C 52 76,42 78,34 74"
stroke="currentColor" stroke-width="9" stroke-linecap="round" stroke-linejoin="round" opacity="0.38"/>
<path d="M 28 6 C 44 2,52 12,47 14 C 40 18,16 18,12 28 C 8 38,16 50,28 54 C 38 56,52 60,52 68 C 52 76,42 78,34 74"
stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.92"/>
</svg>
<UiSectionSign class="evt-logo" />
</div>
<p class="mb-3 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-extrabold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p v-if="evtContent?.format" class="evt-format font-display">
{{ evtContent.format }}
</p>
<p v-if="evtContent?.subtitle" class="mt-2 text-base italic" style="color: hsl(var(--color-text-muted))">
{{ evtContent.subtitle }}
</p>
@@ -447,6 +443,14 @@ useSeoPage({
</script>
<style scoped>
.evt-format {
margin-top: 0.35rem;
font-weight: 600;
font-size: clamp(1.25rem, 3vw, 1.75rem);
letter-spacing: -0.02em;
color: hsl(var(--axis-cit));
}
.page-title {
font-size: clamp(2.5rem, 6vw, 3.5rem);
}
@@ -657,12 +661,12 @@ useSeoPage({
align-items: center;
justify-content: center;
color: hsl(var(--color-primary));
filter: drop-shadow(0 0 14px hsl(var(--color-primary) / 0.25));
filter: drop-shadow(0 6px 14px hsl(var(--color-primary) / 0.25));
}
.evt-logo {
width: 100%;
height: 100%;
width: auto;
}
/* ── Badge gestation hero ── */
+1 -2
View File
@@ -1,7 +1,6 @@
<template>
<div>
<HomeHeroSection />
<HomeAxisGrid @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" />
<HomeLanding @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" />
<HomeMessages />
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
+119 -14
View File
@@ -1,5 +1,5 @@
<template>
<div class="page-outer section-padding">
<div class="axis-scope axis-scope--num page-outer section-padding">
<!-- Shadok pinguin — tenant une clé USB, petit Tux à ses pieds -->
<svg class="shadok-dl shadok-dl-pinguin" viewBox="0 0 170 210" fill="none" aria-hidden="true">
@@ -247,10 +247,19 @@
<p style="color: hsl(var(--color-text-muted))" class="leading-relaxed">
{{ content.project.text }}
</p>
<span v-if="content?.gestation" class="gestation-badge mt-3">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge
class="mt-3"
:status="content.project.status"
:gestation="content.project.gestation || content?.gestation"
:since="content.project.since"
/>
<div v-if="content.project.action?.href" class="project-action mt-4">
<UiBaseButton :href="content.project.action.href" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
{{ content.project.action.label }}
</UiBaseButton>
<span v-if="content.project.action.note" class="project-action-note">{{ content.project.action.note }}</span>
</div>
</div>
</div>
@@ -477,12 +486,18 @@
</div>
</div>
<!-- Bouquet sweethomeCloud -->
<!-- Bouquet sweetHome -->
<div v-if="section.bouquet" class="territoire-bouquet">
<h3 class="territoire-bouquet-title">
<div class="i-lucide-package h-4 w-4" />
{{ section.bouquet.title }}
</h3>
<p v-if="section.bouquet.provider" class="territoire-bouquet-provider">{{ section.bouquet.provider }}</p>
<ul v-if="section.bouquet.statuses" class="bouquet-legend" aria-label="Légende des statuts">
<li v-for="(label, key) in section.bouquet.statuses" :key="key">
<span class="bouquet-dot" :class="`bouquet-dot--${key}`" aria-hidden="true" />{{ label }}
</li>
</ul>
<div class="territoire-bouquet-grid">
<div
v-for="(cat, ci) in section.bouquet.categories"
@@ -494,8 +509,24 @@
<span>{{ cat.label }}</span>
</div>
<ul class="territoire-bouquet-items">
<li v-for="(item, ii) in cat.items" :key="ii">{{ item }}</li>
<template v-for="(item, ii) in cat.items" :key="ii">
<li v-if="typeof item === 'string'">{{ item }}</li>
<li v-else class="bouquet-item">
<span
class="bouquet-dot"
:class="`bouquet-dot--${item.status}`"
:title="section.bouquet.statuses?.[item.status]"
/>
<span class="bouquet-role">{{ item.role }}</span>
<template v-if="item.name">
<a v-if="item.url" :href="item.url" target="_blank" rel="noopener" class="bouquet-name">{{ item.name }}</a>
<span v-else class="bouquet-name">{{ item.name }}</span>
</template>
<span class="sr-only">— {{ section.bouquet.statuses?.[item.status] }}</span>
</li>
</template>
</ul>
<p v-if="cat.note" class="bouquet-note">{{ cat.note }}</p>
</div>
</div>
</div>
@@ -599,13 +630,6 @@
</div><!-- /sections-area -->
<div v-if="slug === 'logiciel-libre'" class="text-center mt-6">
<UiBaseButton href="https://axiom-team.fr/dons" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
Voir la v1 bridée (premier flux)
</UiBaseButton>
</div>
<div v-if="slug === 'tarifs-eau'" class="text-center mt-6">
<UiBaseButton :href="sejeteral0Url" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
@@ -1628,6 +1652,87 @@ useSeoPage({
color: hsl(var(--color-primary) / 0.5);
}
.project-action {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
}
.project-action-note {
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--axis-cit));
}
/* Bouquet — status of each stack */
.territoire-bouquet-provider {
font-size: 0.8rem;
color: hsl(var(--color-text-muted));
margin: -0.25rem 0 0.75rem;
}
.bouquet-legend {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.35rem 1rem;
padding: 0;
margin: 0 0 1rem;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--color-text-muted));
}
.bouquet-legend li {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.territoire-bouquet-items li.bouquet-item {
padding-left: 0;
display: grid;
grid-template-columns: 0.5rem minmax(0, 1fr);
column-gap: 0.45rem;
align-items: baseline;
}
.bouquet-item > .bouquet-dot { grid-row: 1; align-self: center; }
.bouquet-item > .bouquet-role { grid-column: 2; }
.bouquet-item > .bouquet-name { grid-column: 2; }
.territoire-bouquet-items li.bouquet-item::before { content: none; }
.bouquet-dot {
display: inline-block;
flex-shrink: 0;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
align-self: center;
}
.bouquet-dot--service { background: hsl(var(--axis-num)); }
.bouquet-dot--catalogue { box-shadow: inset 0 0 0 1.5px hsl(var(--axis-num)); }
.bouquet-dot--avenir { box-shadow: inset 0 0 0 1px hsl(var(--color-text) / 0.3); }
.bouquet-role { color: hsl(var(--color-text-muted)); }
.bouquet-name {
color: hsl(var(--color-text));
font-weight: 600;
}
a.bouquet-name:hover { color: hsl(var(--axis-num)); }
.bouquet-note {
margin: 0.4rem 0 0;
font-size: 0.72rem;
font-style: italic;
color: hsl(var(--color-text-muted));
}
/* Estimation toggled */
.territoire-estimate {
border-top: 1px solid hsl(var(--color-text) / 0.06);
+71 -7
View File
@@ -1,5 +1,5 @@
<template>
<div class="relative overflow-hidden section-padding">
<div class="axis-scope axis-scope--num relative overflow-hidden section-padding">
<!-- Shadok codeuse — sitting at desk, typing on laptop, glasses -->
<svg class="shadok-codeuse" viewBox="0 0 170 200" fill="none" aria-hidden="true">
<!-- Desk -->
@@ -315,6 +315,16 @@
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-6">
<!-- LPK sert l'axe numérique -->
<NuxtLink v-if="lpk" :to="lpk.to" class="lpk-card">
<span class="lpk-name">{{ lpk.name }}</span>
<span class="lpk-body">
<span class="lpk-full">{{ lpk.text }}</span>
<span class="lpk-role">{{ lpk.role }}</span>
</span>
<span class="i-lucide-arrow-right h-5 w-5 lpk-arrow" aria-hidden="true" />
</NuxtLink>
<div
v-for="pillar in content?.pillars"
:key="pillar.id"
@@ -332,10 +342,7 @@
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
{{ pillar.label }}
</h2>
<span v-if="pillar.gestation" class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
<UiStatusBadge class="ml-auto" :status="pillar.status" :gestation="pillar.gestation" :since="pillar.since" />
</div>
<p class="leading-relaxed whitespace-pre-line mt-3" style="color: hsl(var(--color-text-muted))">{{ pillar.text }}</p>
@@ -366,6 +373,7 @@
<div :class="`i-lucide-${action.icon}`" class="h-3.5 w-3.5" />
{{ action.label }}
</a>
<span v-for="action in (pillar as any).actions.filter((a: any) => a.note)" :key="`n-${action.label}`" class="pillar-action-note">{{ action.note }}</span>
</div>
</div>
</div>
@@ -379,6 +387,8 @@ definePageMeta({
})
const { data: content } = await usePageContent('numerique')
const { data: site } = await useSiteContent()
const lpk = computed(() => site.value?.lpk)
useSeoPage({
title: content.value?.meta?.title ?? 'Autonomie numérique',
@@ -387,13 +397,66 @@ useSeoPage({
</script>
<style scoped>
.lpk-card {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 1.25rem 1.5rem;
border-radius: 16px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-text));
transition: transform 0.12s ease;
}
.lpk-card:hover { transform: translateY(-3px); }
.lpk-card:active { transform: translateY(0); }
.lpk-name {
font-family: var(--font-display);
font-weight: 800;
font-size: 2rem;
letter-spacing: -0.03em;
color: hsl(var(--color-primary));
}
.lpk-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.lpk-full {
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--color-text-muted));
}
.lpk-role {
font-size: 1.02rem;
line-height: 1.45;
}
.pillar-action-note {
align-self: center;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--axis-cit));
}
.lpk-arrow { color: hsl(var(--color-primary)); flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) {
.lpk-card { transition: none; }
}
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.pillar-card {
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
border-radius: 16px;
border: none;
background: hsl(var(--color-surface));
transition: border-color 0.2s;
position: relative;
@@ -462,6 +525,7 @@ useSeoPage({
.pillar-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
+19
View File
@@ -8,6 +8,10 @@ interface PaletteColors {
surfaceLight: string
text: string
textMuted: string
// One hue per axis: numérique (cool) · économique (warm) · citoyenne (green)
axisNum: string
axisEco: string
axisCit: string
isLight: boolean
label: string
icon: string
@@ -25,6 +29,9 @@ const palettes: Record<PaletteName, PaletteColors> = {
surfaceLight: '20 8% 17%', // bois fumé
text: '0 0% 100%',
textMuted: '0 0% 65%',
axisNum: '195 55% 60%',
axisEco: '32 85% 55%',
axisCit: '95 35% 55%',
isLight: false,
label: 'Automne',
icon: 'i-lucide-leaf',
@@ -39,6 +46,9 @@ const palettes: Record<PaletteName, PaletteColors> = {
surfaceLight: '220 12% 18%', // brume nocturne
text: '0 0% 100%',
textMuted: '210 10% 65%',
axisNum: '205 70% 66%',
axisEco: '36 80% 62%',
axisCit: '160 40% 58%',
isLight: false,
label: 'Hiver',
icon: 'i-lucide-snowflake',
@@ -55,6 +65,9 @@ const palettes: Record<PaletteName, PaletteColors> = {
surfaceLight: '148 22% 77%', // feuillage vif
text: '155 50% 6%', // encre noire-verte
textMuted: '150 22% 28%', // sous-bois dense
axisNum: '212 65% 34%',
axisEco: '338 80% 42%',
axisCit: '152 80% 22%',
isLight: true,
label: 'Printemps',
icon: 'i-lucide-flower-2',
@@ -69,6 +82,9 @@ const palettes: Record<PaletteName, PaletteColors> = {
surfaceLight: '30 32% 78%', // argile chaude
text: '20 45% 8%', // brun profond
textMuted: '22 22% 30%', // ombre terracotta
axisNum: '198 70% 30%',
axisEco: '24 88% 42%',
axisCit: '150 45% 28%',
isLight: true,
label: 'Été',
icon: 'i-lucide-sun',
@@ -90,6 +106,9 @@ const paletteMoods = (Object.entries(palettes) as [PaletteName, PaletteColors][]
'--color-surface-light': c.surfaceLight,
'--color-text': c.text,
'--color-text-muted': c.textMuted,
'--axis-num': c.axisNum,
'--axis-eco': c.axisEco,
'--axis-cit': c.axisCit,
},
}),
)
+3 -2
View File
@@ -83,7 +83,7 @@ export default defineNuxtConfig({
'i-lucide-message-circle', 'i-lucide-bot', 'i-lucide-globe-2',
'i-lucide-map-pin', 'i-lucide-euro', 'i-lucide-package',
'i-lucide-settings', 'i-lucide-layout-grid', 'i-lucide-server',
'i-lucide-chevron-down', 'i-lucide-info',
'i-lucide-chevron-down', 'i-lucide-info', 'i-lucide-briefcase', 'i-lucide-key', 'i-lucide-shield',
],
},
@@ -91,10 +91,11 @@ export default defineNuxtConfig({
head: {
htmlAttrs: { lang: 'fr' },
link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
{ rel: 'preconnect', href: 'https://fonts.bunny.net' },
{
rel: 'stylesheet',
href: 'https://fonts.bunny.net/css?family=syne:400,500,600,700,800|space-grotesk:300,400,500,600,700|jetbrains-mono:400,500,700',
href: 'https://fonts.bunny.net/css?family=bricolage-grotesque:400,500,600,700,800|public-sans:400,400i,600,700,700i|jetbrains-mono:400,500,700|syne:400',
},
],
meta: [
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-230 0 970 970">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#d4520f"/>
<stop offset="100%" stop-color="#e02b41"/>
</linearGradient>
</defs>
<path fill="url(#g)" d="M277 0Q320 0 349 7.5Q378 15 395 22Q426 37 443.5 59.5Q461 82 461 115Q461 143 441.5 164Q422 185 387 185Q354 185 332.5 165.5Q311 146 311 116Q311 92 324.5 71Q338 50 362 42Q357 34 344.5 29Q332 24 316 21.5Q300 19 284 19Q246 19 223 32.5Q200 46 190 66.5Q180 87 180 110Q180 150 200 177.5Q220 205 256 230Q292 255 340 287Q394 324 432.5 354.5Q471 385 491 418Q511 451 511 495Q511 548 475.5 593Q440 638 352 677L343 667Q373 646 388 623Q403 600 403 567Q403 536 386 511.5Q369 487 333.5 459Q298 431 241 394Q178 353 135.5 318.5Q93 284 71.5 248Q50 212 50 167Q50 116 78.5 78.5Q107 41 158.5 20.5Q210 0 277 0ZM158 292 168 303Q137 323 122.5 346Q108 369 108 403Q108 433 124.5 458Q141 483 177 511Q213 539 270 576Q332 617 374.5 651.5Q417 686 439 722Q461 758 461 803Q461 853 432 891Q403 929 352 949.5Q301 970 233 970Q190 970 161 962.5Q132 955 115 948Q85 933 67 910.5Q49 888 49 855Q49 827 69 806Q89 785 123 785Q157 785 178 804.5Q199 824 199 854Q199 878 185.5 898.5Q172 919 148 928Q152 935 165 940.5Q178 946 194.5 948.5Q211 951 226 951Q265 951 287.5 937.5Q310 924 320.5 903Q331 882 331 860Q331 820 311 792.5Q291 765 255 740Q219 715 171 683Q116 646 78 615.5Q40 585 20 551.5Q0 518 0 475Q0 421 34.5 376Q69 331 158 292Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+2 -1
View File
@@ -12,6 +12,7 @@ items:
- id: decision
label: Décision collective
icon: gavel
status: En développement
description: Se donner les moyens de la décision collective.
to: /citoyenne/decision
- id: tarifs-eau
@@ -19,4 +20,4 @@ items:
icon: droplets
description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la décision à la population des communes.
to: /citoyenne/tarifs-eau
gestation: true
status: En développement
+1
View File
@@ -4,6 +4,7 @@ meta:
kicker: Autonomie citoyenne
title: Plateforme Décision
icon: gavel
status: En développement
description: Se donner les moyens de la décision collective.
features:
+1 -1
View File
@@ -4,7 +4,7 @@ meta:
kicker: Autonomie citoyenne
title: Tarifs de l'eau
icon: droplets
gestation: true
status: En développement
description: >
Application pour obtenir justice sociale et incitation dynamique
+2
View File
@@ -14,6 +14,8 @@ items:
- id: monnaie-libre
label: Monnaie libre
icon: g1
status: En production
since: Depuis 2017
description: "La Ğ1 (June) : chaque membre crée la même part de monnaie, chaque jour. Sans dette ni intérêt — le dividende universel."
to: /economique/monnaie-libre
- id: modele-eco
+2
View File
@@ -4,6 +4,8 @@ meta:
kicker: Autonomie économique
title: Monnaie libre
icon: g1
status: En production
since: Depuis 2017
description: >
Toute économie repose sur un code monétaire.
+2 -1
View File
@@ -3,6 +3,7 @@ meta:
kicker: Performance d'émancipation civile
title: Le Librodrome
format: Un format d'événement
subtitle: "Une paece — performance artistique d'émancipation civile et économique"
leitmotiv: Construire une autonomie collective.
tagline: Rendre possible. Passer la seconde.
@@ -17,7 +18,7 @@ axes:
label: Numérique
items:
- Affranchissement GAFAM — logiciels libres, Linux, migration
- Cloud local décentralisé — Nextcloud, IPFS, Matrix, PeerTube
- Cloud local décentralisé — OpenCloud, IPFS, Matrix, PeerTube
- IA collective locale — Mistral, UPlanet
- icon: coins
+22 -4
View File
@@ -36,25 +36,37 @@ book:
cta:
player: Présentation musicale
pdf: Lecture du livre
toolbox:
title: Le librodrome est une boîte à outils
console:
prefix: Autonomie
top: Émancipation
bottom: Asservissement
format:
title: Le librodrome est une invitation
text: À la fois éditorial pour un bassin de vie et événementiel — une boîte à outils pour construire une autonomie
collective, reproductible.
axes:
numerique:
title: Autonomie numérique
icon: monitor
promise: LaPlanK met à disposition le cloud libre et les outils numériques nécessaires à la coopération.
items:
- label: Logiciel libre
description: "Passer à Linux, adopter des équivalents libres, un assistant shell local sans cloud — guide de migration et atelier au prochain Librodrome."
to: /numerique/logiciel-libre
gestation: true
status: En développement
icon: code-2
presentation:
title: wishBounty
text: Application pour le financement fléché des développements.
actions:
- id: open-dons
label: Voir la v1 bridée (premier flux)
label: Voir la v1 (premier flux)
icon: external-link
highlight: true
href: https://axiom-team.fr/dons
note: En production pour l'écosystème Duniter
- label: Authentification — WoT
description: "Une clé pour les accès, une signature pour les actes. Le seul système sans autorité centrale ni biométrie — DID W3C, Duniter, EUDI Wallet 2026."
to: /numerique/authentification-wot
@@ -69,16 +81,19 @@ axes:
gestation: true
icon: cloud
presentation:
title: sweethomeCloud
title: sweetHome
text: "Bouquet de services libres auto-hébergés — clé en main."
economie:
title: Autonomie économique
icon: coins
promise: L'économie du don, un modèle applicable pour se donner les moyens de produire autrement.
items:
- label: Monnaie libre
description: "La Ğ1 (June) : chaque membre crée la même part de monnaie, chaque jour. Sans dette ni intérêt — le dividende universel."
to: /economique/monnaie-libre
icon: g1
status: En production
since: Depuis 2017
- label: Économie du don
description: Un livre et des chansons pour une proposition de modèle économique fondé sur le don.
to: /economique/modele-eco
@@ -119,11 +134,14 @@ axes:
politique:
title: Autonomie citoyenne
icon: landmark
promise: La décision et la réflexion pour redonner aux populations locales le pouvoir d'agir — extensible à plus
grande échelle par reproduction.
items:
- label: Décision collective
description: Se donner les moyens de la décision collective.
to: /citoyenne/decision
icon: gavel
status: En développement
actions:
- id: open-libredecision
label: Ouvrir libreDecision
@@ -134,7 +152,7 @@ axes:
description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la
décision à la population des communes.
to: /citoyenne/tarifs-eau
gestation: true
status: En développement
icon: droplets
actions:
- id: open-sejeteral0
+5 -4
View File
@@ -16,13 +16,14 @@ pillars:
project:
name: wishBounty
text: Application pour le financement fléché des développements libres.
gestation: true
status: En développement
to: /numerique/logiciel-libre
actions:
- label: Voir la v1 bridée (premier flux)
- label: Voir la v1 (premier flux)
icon: external-link
highlight: true
href: https://axiom-team.fr/dons
note: En production pour l'écosystème Duniter
- id: authentification-wot
label: "Authentification — WoT"
@@ -47,7 +48,7 @@ pillars:
Un bouquet de services complet — Drive, Visio, Forum, Wiki, CMS —
et demain, une IA frugale localisée.
project:
name: sweethomeCloud
text: "Bouquet de services libres auto-hébergés — clé en main pour chaque territoire."
name: sweetHome
text: "Bouquet de services libres auto-hébergés — clé en main pour chaque collectif, association ou entreprise coopérative."
gestation: true
to: /numerique/cloud-libre
+92 -31
View File
@@ -12,8 +12,8 @@ description: >
Ces dernières émergent à peine.
project:
name: sweethomeCloud
text: Bouquet de services libres auto-hébergés — clé en main pour chaque bassin de vie.
name: sweetHome
text: Bouquet de services libres auto-hébergés — clé en main pour chaque bassin de vie. Servi par LaPlanK.
gestation: true
sections:
@@ -102,6 +102,10 @@ sections:
categories:
- label: "Fichiers & synchronisation"
items:
- from: "Google Drive / OneDrive"
to: OpenCloud
url: https://opencloud.eu
note: "Fichiers et espaces partagés — le choix de LaPlanK (cloud.laplank.org)"
- from: "Google Drive / Dropbox"
to: Nextcloud
url: https://nextcloud.com/fr
@@ -148,6 +152,32 @@ sections:
to: "Matrix + Element"
url: https://element.io
note: Messagerie décentralisée et fédérée
- label: "Gestion & organisation"
items:
- from: "Salesforce / HubSpot"
to: Twenty
url: https://twenty.com
note: "CRM libre, auto-hébergeable"
- from: "Sage / SAP"
to: ERPNext
url: https://erpnext.com
note: "Comptabilité, facturation, stocks — ERP libre"
- from: Logiciel de compta associative
to: Paheko
url: https://paheko.cloud
note: "Gestion et comptabilité d'association"
- from: "LastPass / 1Password"
to: Vaultwarden
url: https://github.com/dani-garcia/vaultwarden
note: "Coffre de mots de passe partagé, compatible Bitwarden"
- from: GitHub
to: Forgejo
url: https://forgejo.org
note: "Forge git communautaire, auto-hébergeable"
- from: Google Analytics
to: Umami
url: https://umami.is
note: "Mesure d'audience sans cookie ni pistage"
- label: Auto-hébergement facile
items:
- from: Pas de serveur
@@ -182,7 +212,7 @@ sections:
- "Référence officielle : guide CNIL blockchain et RGPD + lignes directrices CEPD 02/2025"
- type: projet
title: sweethomeCloud
title: sweetHome
kicker: Prochain projet
icon: home
badge: En gestation
@@ -190,12 +220,12 @@ sections:
Un bouquet de services libres, auto-hébergés et prêts à l'emploi —
pour les collectivités, associations et collectifs qui veulent
maîtriser leur infrastructure numérique sans expertise technique.
sweethomeCloud s'appuie sur les briques documentées dans cette page :
Nextcloud, Matrix, Jitsi, YunoHost, Ollama — packagées, maintenues,
documentées ensemble. Une instance par territoire.
sweetHome s'appuie sur les briques documentées dans cette page :
OpenCloud, Matrix, Jitsi, YunoHost, Ollama — packagées, maintenues,
documentées ensemble. Une instance par territoire. Servi par LaPlanK.
features:
- icon: hard-drive
label: "Drive, agenda, contacts — Nextcloud"
label: "Fichiers et espaces partagés — OpenCloud"
- icon: video
label: "Visioconférence — Jitsi / Galène"
- icon: message-circle
@@ -213,7 +243,7 @@ sections:
text: >
Un bassin de vie de 50 000 personnes peut déployer et maintenir son propre réseau
numérique souverain. Le matériel est accessible, les logiciels libres sont matures.
sweethomeCloud sera ce bouquet — packagé, documenté, maintenu ensemble.
sweetHome sera ce bouquet — packagé, documenté, maintenu ensemble.
Deux modèles économiques, au choix.
economies:
- label: "Euro non vénale"
@@ -223,48 +253,79 @@ sections:
desc: "Services financés exclusivement en Ğ1, créée par ses membres. Aligné avec l'économie du don."
icon: coins
bouquet:
title: "Le bouquet sweethomeCloud"
title: "Le bouquet sweetHome"
provider: "Servi par LaPlanK — les stacks choisies et exploitées par LaPlanK et HCO"
statuses:
service: En service
catalogue: Au catalogue
avenir: À venir
categories:
- label: Collaboration quotidienne
icon: users
items:
- "Drive, agenda, contacts, photos — Nextcloud"
- "Visioconférence — Jitsi / Galène"
- "Messagerie chiffrée souveraine"
- "Email hébergé — Postfix + Dovecot"
- { role: "Fichiers, espaces partagés", name: OpenCloud, status: service, url: "https://opencloud.eu" }
- { role: "Agenda, contacts, photos", name: Nextcloud, status: catalogue, url: "https://nextcloud.com/fr" }
- { role: "Coffre de mots de passe partagé", name: Vaultwarden, status: service, url: "https://github.com/dani-garcia/vaultwarden" }
- { role: Visioconférence, name: "Jitsi / Galène", status: avenir }
- { role: Messagerie chiffrée souveraine, name: Matrix, status: avenir }
- { role: Email hébergé, name: "docker-mailserver (Postfix + Dovecot)", status: catalogue, url: "https://docker-mailserver.github.io/docker-mailserver/latest/" }
- label: "Gestion & coopération"
icon: briefcase
items:
- { role: "Contacts, partenaires, suivi", name: Twenty, status: service, url: "https://twenty.com" }
- { role: "Comptabilité, facturation", name: ERPNext, status: service, url: "https://erpnext.com" }
- { role: Comptabilité associative, name: Paheko, status: avenir, url: "https://paheko.cloud" }
- { role: Pilotage budgétaire partagé, name: budgetPilot, status: service }
- { role: Financement fléché des développements, name: wishBounty, status: avenir }
- label: "Web & publication"
icon: globe-2
items:
- "CMS — WordUp"
- "Forum"
- "Wiki"
- "Sites web, pages publiques"
- { role: "Sites web, pages publiques", name: librodrome.org, status: service }
- { role: Mesure d'audience sans pistage, name: Umami, status: service, url: "https://umami.is" }
- { role: CMS, name: WordUp, status: avenir }
- { role: Forum, status: avenir }
- { role: Wiki, status: avenir }
- label: "Infrastructure technique"
icon: settings
items:
- "Git — Forgejo"
- "Automatisation — n8n"
- "Mailer transactionnel — Postal"
- "DNS souverain"
- { role: Git, name: "Gitea, puis Forgejo", status: service, url: "https://forgejo.org" }
- { role: Intégration continue, name: Woodpecker CI, status: service, url: "https://woodpecker-ci.org" }
- { role: Déploiement des stacks, name: "myos — Docker Swarm", status: service }
- { role: Secrets chiffrés, name: "sops + age", status: service, url: "https://github.com/getsops/sops" }
- { role: DNS souverain, name: PowerDNS, status: catalogue, url: "https://www.powerdns.com" }
- { role: Supervision, name: "Prometheus + Grafana", status: catalogue, url: "https://grafana.com/oss/" }
- { role: Sauvegardes, name: restic, status: catalogue, url: "https://restic.net" }
- { role: Automatisation, name: n8n, status: avenir }
- { role: Mailer transactionnel, name: Postal, status: avenir }
- label: "Identité & accès"
icon: key
items:
- { role: Connexion unique (OpenID Connect), name: Dex, status: avenir, url: "https://dexidp.io" }
- { role: Annuaire, name: "LDAP — 389 Directory Server", status: catalogue, url: "https://www.port389.org" }
- { role: Gestionnaire de confiances, name: trustWallet, status: avenir }
- label: "Souveraineté protocole"
icon: shield
items:
- "Nœud Duniter — Ğ1"
- "Stockage IPFS"
- "Miroirs logiciels libres"
- { role: Nœud Duniter — Ğ1, name: Duniter v2s, status: catalogue, url: "https://duniter.org" }
- { role: Stockage IPFS, name: Kubo, status: service, url: "https://ipfs.tech" }
- { role: Miroirs logiciels libres, status: avenir }
- label: "Intelligence artificielle locale"
icon: bot
note: "Aucune donnée ne sort du territoire"
items:
- "LLM généraliste en file — Ollama + Llama 8B"
- "LLM lourd sur demande — Llama 70B"
- "IA code — Codestral / DeepSeek"
- "Aucune donnée ne sort du territoire"
- { role: "Transcription et traduction", name: "Whisper + LibreTranslate", status: catalogue, url: "https://libretranslate.com" }
- { role: Assistant conversationnel, name: BionicGPT, status: catalogue, url: "https://bionic-gpt.com" }
- { role: LLM généraliste en file, name: "Ollama + Llama 8B", status: avenir }
- { role: LLM lourd sur demande, name: Llama 70B, status: avenir }
- { role: IA code, name: "Codestral / DeepSeek", status: avenir }
- label: "Applications du territoire"
icon: layout-grid
items:
- "librodrome — économie & autonomie"
- "libreDecision — décision collective"
- "Applications métier à venir"
- { role: "Économie & autonomie", name: librodrome, status: service }
- { role: Décision collective, name: libreDecision, status: service, url: "https://decision.librodrome.org" }
- { role: Coefficients relatifs, name: grateWizard, status: service, url: "https://gratewizard.axiom-team.fr" }
- { role: Tarifs de l'eau, name: SejeteralO, status: avenir }
- { role: Applications métier, status: avenir }
estimate:
toggle: "Estimation matérielle pour 50 000 personnes"
note: >
+5 -1
View File
@@ -13,7 +13,11 @@ description: >
project:
name: wishBounty
text: Application pour le financement fléché des développements libres.
gestation: true
status: En développement
action:
label: Voir la v1 (premier flux)
href: https://axiom-team.fr/dons
note: En production pour l'écosystème Duniter
sections:
- type: arguments
+5
View File
@@ -24,3 +24,8 @@ gratewizard:
popup:
width: 420
height: 720
lpk:
name: LaPlanK
text: Association 1901 — financement des communs numériques.
role: LaPlanK met à disposition le cloud libre et les outils numériques nécessaires à la coopération.
to: /numerique/cloud-libre
+6 -10
View File
@@ -2,7 +2,6 @@ import {
defineConfig,
presetIcons,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
@@ -17,21 +16,18 @@ export default defineConfig({
ph: () => import('@iconify-json/ph/icons.json').then(i => i.default),
},
}),
presetWebFonts({
provider: 'bunny',
fonts: {
display: 'Outfit:400,500,600,700,800',
sans: 'Inter:300,400,500,600,700',
mono: 'JetBrains Mono:400,500,700',
calligraphy: 'Playfair Display:400,700',
},
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
theme: {
// Font files are loaded once, from Bunny Fonts, in nuxt.config.ts (app.head.link).
fontFamily: {
display: 'var(--font-display)',
sans: 'var(--font-sans)',
mono: 'var(--font-mono)',
},
colors: {
primary: {
DEFAULT: 'hsl(var(--color-primary))',