GrateWizard bloc dédié, messagerie libre, page numérique 3 piliers
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- GrateWizard : lancement URL simple (plus de popup embed), bloc
  dédié violet sur la home entre axes et événement
- Messagerie : plus de champs obligatoires, plus de champ email
  séparé, hint email dans le message, remerciement onboarding
- Page /numerique : 3 piliers (Logiciel libre, WoT, Cloud libre)
  avec projets associés, remplace les extraits livre hors-sujet
- Admin : carte Messages ajoutée au dashboard
- Safelist icônes complétée

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-05 03:06:48 +01:00
parent 3a5c40a886
commit c564e7be5f
9 changed files with 279 additions and 119 deletions

View File

@@ -40,8 +40,28 @@
</div> </div>
</UiScrollReveal> </UiScrollReveal>
<!-- Bloc GrateWizard -->
<UiScrollReveal v-if="gw" :delay="250">
<button class="gw-block" @click="launchGW">
<div class="gw-icon">
<div class="i-lucide-sparkles h-7 w-7" />
</div>
<div class="gw-text">
<h2 class="font-display text-xl font-bold text-white sm:text-2xl">
{{ gw.title }}
</h2>
<p class="text-white/55 text-sm sm:text-base mt-0.5">
{{ gw.subtitle }}
</p>
</div>
<div class="gw-arrow">
<div class="i-lucide-arrow-up-right h-5 w-5" />
</div>
</button>
</UiScrollReveal>
<!-- Bloc Événement --> <!-- Bloc Événement -->
<UiScrollReveal v-if="evenement" :delay="300"> <UiScrollReveal v-if="evenement" :delay="350">
<NuxtLink :to="evenement.to" class="event-block"> <NuxtLink :to="evenement.to" class="event-block">
<div class="event-content"> <div class="event-content">
<div class="event-icon"> <div class="event-icon">
@@ -76,6 +96,7 @@ const { data: content } = await usePageContent('home')
const { launch } = useGrateWizard() const { launch } = useGrateWizard()
const axes = computed(() => (content.value as any)?.axes) const axes = computed(() => (content.value as any)?.axes)
const gw = computed(() => (content.value as any)?.gratewizard)
const evenement = computed(() => (content.value as any)?.evenement) const evenement = computed(() => (content.value as any)?.evenement)
function launchGW() { function launchGW() {
@@ -135,11 +156,84 @@ function launchGW() {
flex-shrink: 0; flex-shrink: 0;
} }
/* GrateWizard block */
.gw-block {
display: flex;
align-items: center;
gap: 1.25rem;
width: 100%;
padding: 1.75rem 2rem;
border-radius: 1rem;
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: 3.5rem;
height: 3.5rem;
border-radius: 0.875rem;
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: 2.5rem;
height: 2.5rem;
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) { @media (max-width: 640px) {
.event-block { .event-block {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
padding: 1.5rem; padding: 1.5rem;
} }
.gw-block {
padding: 1.25rem;
gap: 1rem;
}
.gw-arrow {
display: none;
}
} }
</style> </style>

View File

@@ -7,40 +7,36 @@
<h3 class="font-display text-lg font-bold text-white mb-4">Laisser un message</h3> <h3 class="font-display text-lg font-bold text-white mb-4">Laisser un message</h3>
<form v-if="!submitted" class="space-y-3" @submit.prevent="send"> <form v-if="!submitted" class="space-y-3" @submit.prevent="send">
<div class="grid gap-3 sm:grid-cols-2"> <input
<input v-model="form.author"
v-model="form.author" type="text"
type="text" placeholder="Votre nom"
placeholder="Votre nom *" class="msg-input"
required />
class="msg-input" <p class="text-white/30 text-xs -mt-1 px-1">Pour recevoir une réponse, laissez votre e-mail dans le message.</p>
/>
<input
v-model="form.email"
type="email"
placeholder="Email (optionnel)"
class="msg-input"
/>
</div>
<textarea <textarea
v-model="form.text" v-model="form.text"
placeholder="Votre message *" placeholder="Votre message"
required
rows="3" rows="3"
class="msg-input resize-none" class="msg-input resize-none"
/> />
<div class="flex justify-end"> <div class="flex justify-end">
<button type="submit" class="btn-primary text-sm" :disabled="sending"> <button type="submit" class="btn-primary text-sm" :disabled="sending || !canSend">
<div v-if="sending" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" /> <div v-if="sending" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" />
Envoyer Envoyer
</button> </button>
</div> </div>
</form> </form>
<div v-else class="text-center py-4"> <div v-else class="text-center py-6">
<div class="i-lucide-check-circle h-8 w-8 text-green-400 mx-auto mb-2" /> <div class="i-lucide-heart-handshake h-10 w-10 text-primary mx-auto mb-3" />
<p class="text-white/80">Merci pour votre message !</p> <p class="text-white/90 font-display text-lg font-semibold">Merci pour votre message !</p>
<p class="text-white/40 text-sm mt-1">Il sera visible après modération.</p> <p class="text-white/55 text-sm mt-2 max-w-md mx-auto leading-relaxed">
Il sera lu et traité dans un délai... humainement raisonnable.
</p>
<p class="text-primary/60 text-xs mt-4 italic">
Chaque message est un premier pas dans l'aventure.
</p>
</div> </div>
</div> </div>
</UiScrollReveal> </UiScrollReveal>
@@ -72,11 +68,14 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: messages } = await useFetch('/api/messages') const { data: messages } = await useFetch('/api/messages')
const form = reactive({ author: '', email: '', text: '' }) const form = reactive({ author: '', text: '' })
const sending = ref(false) const sending = ref(false)
const submitted = ref(false) const submitted = ref(false)
const canSend = computed(() => form.text.trim().length > 0)
async function send() { async function send() {
if (!canSend.value) return
sending.value = true sending.value = true
try { try {
await $fetch('/api/messages', { method: 'POST', body: form }) await $fetch('/api/messages', { method: 'POST', body: form })

View File

@@ -1,19 +1,10 @@
export function useGrateWizard() { export function useGrateWizard() {
const appConfig = useAppConfig() const appConfig = useAppConfig()
const { url, popup } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } } const { url } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } }
function launch(e?: Event) { function launch(e?: Event) {
const w = popup.width window.open(url, '_blank', 'noopener,noreferrer')
const h = popup.height e?.preventDefault()
const left = Math.round((window.screen.width - w) / 2)
const top = Math.round((window.screen.height - h) / 2)
const embedUrl = `${url}?embed=true&hideTabBar=true&tab=mn`
const win = window.open(
embedUrl,
'grateWizard',
`width=${w},height=${h},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,scrollbars=no,resizable=yes`,
)
if (win) e?.preventDefault()
} }
return { url, launch } return { url, launch }

View File

@@ -27,6 +27,12 @@
<p class="text-sm text-white/50">Métadonnées des pistes</p> <p class="text-sm text-white/50">Métadonnées des pistes</p>
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/messages" class="dash-card">
<div class="i-lucide-message-square h-8 w-8 text-accent mb-2" />
<h2 class="text-lg font-semibold text-white">Messages</h2>
<p class="text-sm text-white/50">Modération des messages visiteurs</p>
</NuxtLink>
<NuxtLink to="/admin/media" class="dash-card"> <NuxtLink to="/admin/media" class="dash-card">
<div class="i-lucide-image h-8 w-8 text-accent mb-2" /> <div class="i-lucide-image h-8 w-8 text-accent mb-2" />
<h2 class="text-lg font-semibold text-white">Médias</h2> <h2 class="text-lg font-semibold text-white">Médias</h2>

View File

@@ -1,75 +1,52 @@
<template> <template>
<div class="relative overflow-hidden section-padding"> <div class="relative overflow-hidden section-padding">
<!-- Shadok jardinier: character with watering can and plant --> <!-- Shadok jardinier -->
<svg class="shadok-jardinier" viewBox="0 0 240 300" fill="none" aria-hidden="true"> <svg class="shadok-jardinier" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="110" cy="160" rx="40" ry="48" fill="currentColor" opacity="0.85"/> <ellipse cx="110" cy="160" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="110" cy="96" r="25" fill="currentColor" opacity="0.8"/> <circle cx="110" cy="96" r="25" fill="currentColor" opacity="0.8"/>
<!-- Straw hat -->
<ellipse cx="110" cy="78" rx="35" ry="8" fill="currentColor" opacity="0.4"/> <ellipse cx="110" cy="78" rx="35" ry="8" fill="currentColor" opacity="0.4"/>
<path d="M85 78 Q110 60 135 78" fill="currentColor" opacity="0.35"/> <path d="M85 78 Q110 60 135 78" fill="currentColor" opacity="0.35"/>
<!-- Eyes (focused, looking down at plant) -->
<circle cx="102" cy="94" r="4" fill="currentColor" opacity="0.2"/> <circle cx="102" cy="94" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="120" cy="94" r="4" fill="currentColor" opacity="0.2"/> <circle cx="120" cy="94" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="103" cy="95" r="1.8" fill="currentColor" opacity="0.5"/> <circle cx="103" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="121" cy="95" r="1.8" fill="currentColor" opacity="0.5"/> <circle cx="121" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Smile -->
<path d="M103 106 Q110 111 118 106" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/> <path d="M103 106 Q110 111 118 106" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arm holding watering can -->
<line x1="70" y1="150" x2="40" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="70" y1="150" x2="40" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Watering can -->
<rect x="20" y="165" width="30" height="20" rx="3" fill="currentColor" opacity="0.4"/> <rect x="20" y="165" width="30" height="20" rx="3" fill="currentColor" opacity="0.4"/>
<line x1="20" y1="168" x2="10" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/> <line x1="20" y1="168" x2="10" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
<!-- Water drops -->
<circle cx="12" cy="165" r="1.5" fill="currentColor" opacity="0.25"/> <circle cx="12" cy="165" r="1.5" fill="currentColor" opacity="0.25"/>
<circle cx="8" cy="170" r="1.5" fill="currentColor" opacity="0.2"/> <circle cx="8" cy="170" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="15" cy="172" r="1.5" fill="currentColor" opacity="0.2"/> <circle cx="15" cy="172" r="1.5" fill="currentColor" opacity="0.2"/>
<!-- Other arm -->
<line x1="150" y1="150" x2="170" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="150" y1="150" x2="170" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Legs -->
<line x1="95" y1="205" x2="85" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="95" y1="205" x2="85" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="125" y1="205" x2="135" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="125" y1="205" x2="135" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Plant -->
<line x1="180" y1="220" x2="180" y2="180" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/> <line x1="180" y1="220" x2="180" y2="180" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<path d="M180 195 Q195 185 190 175" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/> <path d="M180 195 Q195 185 190 175" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<path d="M180 205 Q165 195 168 185" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/> <path d="M180 205 Q165 195 168 185" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<path d="M180 185 Q192 172 188 165" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.3"/> <path d="M180 185 Q192 172 188 165" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Pot -->
<path d="M170 220 L175 240 L185 240 L190 220 Z" fill="currentColor" opacity="0.35"/> <path d="M170 220 L175 240 L185 240 L190 220 Z" fill="currentColor" opacity="0.35"/>
</svg> </svg>
<!-- Shadok bâtisseur: character with trowel building a wall --> <!-- Shadok bâtisseur -->
<svg class="shadok-batisseur" viewBox="0 0 260 300" fill="none" aria-hidden="true"> <svg class="shadok-batisseur" viewBox="0 0 260 300" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="130" cy="150" rx="40" ry="48" fill="currentColor" opacity="0.85"/> <ellipse cx="130" cy="150" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="130" cy="86" r="25" fill="currentColor" opacity="0.8"/> <circle cx="130" cy="86" r="25" fill="currentColor" opacity="0.8"/>
<!-- Hard hat -->
<ellipse cx="130" cy="68" rx="28" ry="6" fill="currentColor" opacity="0.4"/> <ellipse cx="130" cy="68" rx="28" ry="6" fill="currentColor" opacity="0.4"/>
<rect x="108" y="60" width="44" height="10" rx="3" fill="currentColor" opacity="0.35"/> <rect x="108" y="60" width="44" height="10" rx="3" fill="currentColor" opacity="0.35"/>
<!-- Eyes (determined) -->
<circle cx="122" cy="84" r="4" fill="currentColor" opacity="0.2"/> <circle cx="122" cy="84" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="140" cy="84" r="4" fill="currentColor" opacity="0.2"/> <circle cx="140" cy="84" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="123" cy="83" r="1.8" fill="currentColor" opacity="0.5"/> <circle cx="123" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="141" cy="83" r="1.8" fill="currentColor" opacity="0.5"/> <circle cx="141" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Grin -->
<path d="M123 96 Q130 101 138 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/> <path d="M123 96 Q130 101 138 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arm with trowel -->
<line x1="170" y1="140" x2="210" y2="120" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="170" y1="140" x2="210" y2="120" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Trowel -->
<polygon points="210,115 230,110 225,120 210,122" fill="currentColor" opacity="0.45"/> <polygon points="210,115 230,110 225,120 210,122" fill="currentColor" opacity="0.45"/>
<line x1="210" y1="118" x2="200" y2="125" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/> <line x1="210" y1="118" x2="200" y2="125" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
<!-- Other arm -->
<line x1="90" y1="145" x2="65" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="90" y1="145" x2="65" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Legs -->
<line x1="115" y1="195" x2="105" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="115" y1="195" x2="105" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="145" y1="195" x2="155" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/> <line x1="145" y1="195" x2="155" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Wall (bricks) -->
<rect x="40" y="200" width="50" height="16" rx="1" fill="currentColor" opacity="0.3"/> <rect x="40" y="200" width="50" height="16" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="45" y="183" width="40" height="16" rx="1" fill="currentColor" opacity="0.28"/> <rect x="45" y="183" width="40" height="16" rx="1" fill="currentColor" opacity="0.28"/>
<rect x="50" y="166" width="30" height="16" rx="1" fill="currentColor" opacity="0.25"/> <rect x="50" y="166" width="30" height="16" rx="1" fill="currentColor" opacity="0.25"/>
<!-- Brick lines -->
<line x1="65" y1="200" x2="65" y2="216" stroke="currentColor" stroke-width="1" opacity="0.15"/> <line x1="65" y1="200" x2="65" y2="216" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="55" y1="183" x2="55" y2="199" stroke="currentColor" stroke-width="1" opacity="0.15"/> <line x1="55" y1="183" x2="55" y2="199" stroke="currentColor" stroke-width="1" opacity="0.15"/>
</svg> </svg>
@@ -80,29 +57,49 @@
<h1 class="page-title font-display font-bold tracking-tight text-white"> <h1 class="page-title font-display font-bold tracking-tight text-white">
{{ content?.title }} {{ content?.title }}
</h1> </h1>
<p class="mt-4 mx-auto max-w-2xl text-white/60"> <p class="mt-4 mx-auto max-w-2xl text-white/60 leading-relaxed">
{{ content?.description }} {{ content?.description }}
</p> </p>
</header> </header>
<div class="mx-auto max-w-3xl flex flex-col gap-6"> <div class="mx-auto max-w-3xl flex flex-col gap-6">
<div <div
v-for="(extract, i) in content?.extracts" v-for="pillar in content?.pillars"
:key="i" :key="pillar.id"
class="card-surface" class="pillar-card"
> >
<p class="mb-2 font-mono text-xs tracking-widest text-accent uppercase"> <div class="pillar-header">
{{ extract.chapter }} <div class="pillar-icon">
</p> <div :class="`i-lucide-${pillar.icon}`" class="h-5 w-5" />
<blockquote class="border-l-2 border-primary/30 pl-4 text-white/70 italic leading-relaxed whitespace-pre-line"> </div>
{{ extract.text }} <h2 class="font-display text-xl font-bold text-white">
</blockquote> {{ pillar.label }}
<div class="mt-4"> </h2>
<span v-if="pillar.gestation" class="gestation-badge">
<div class="i-lucide-flask-conical h-3 w-3" />
En gestation
</span>
</div>
<p class="text-white/65 leading-relaxed whitespace-pre-line mt-3">{{ pillar.text }}</p>
<!-- Project card -->
<div v-if="pillar.project" class="project-card mt-4">
<div class="project-icon">
<div class="i-lucide-rocket h-4 w-4" />
</div>
<div>
<span class="font-display font-semibold text-white text-sm">{{ pillar.project.name }}</span>
<span class="text-white/45 text-sm ml-2">{{ pillar.project.text }}</span>
</div>
</div>
<div v-if="pillar.to" class="mt-4">
<NuxtLink <NuxtLink
:to="`/modele-eco/${extract.chapterSlug}`" :to="pillar.to"
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors" class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
> >
Lire le chapitre En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" /> <div class="i-lucide-arrow-right h-3.5 w-3.5" />
</NuxtLink> </NuxtLink>
</div> </div>
@@ -129,6 +126,73 @@ useHead({
font-size: clamp(2rem, 5vw, 2.75rem); font-size: clamp(2rem, 5vw, 2.75rem);
} }
.pillar-card {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s;
}
.pillar-card:hover {
border-color: hsl(var(--color-primary) / 0.2);
}
.pillar-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.pillar-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
box-shadow: 0 0 12px hsl(var(--color-primary) / 0.12);
flex-shrink: 0;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
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);
}
.project-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: hsl(var(--color-bg) / 0.5);
border: 1px solid hsl(var(--color-primary) / 0.1);
}
.project-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.375rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
flex-shrink: 0;
}
.shadok-jardinier { .shadok-jardinier {
position: absolute; position: absolute;
left: 2%; left: 2%;

View File

@@ -25,6 +25,8 @@ export default defineNuxtConfig({
'i-lucide-droplets', 'i-lucide-calendar-heart', 'i-lucide-droplets', 'i-lucide-calendar-heart',
// Action icons // Action icons
'i-lucide-play', 'i-lucide-book-open', 'i-lucide-sparkles', 'i-lucide-play', 'i-lucide-book-open', 'i-lucide-sparkles',
'i-lucide-heart-handshake', 'i-lucide-arrow-up-right',
'i-lucide-rocket', 'i-lucide-flask-conical', 'i-lucide-arrow-right',
// Decision page // Decision page
'i-lucide-vote', 'i-lucide-scroll-text', 'i-lucide-git-branch', 'i-lucide-vote', 'i-lucide-scroll-text', 'i-lucide-git-branch',
], ],

View File

@@ -1,8 +1,8 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody<{ author: string; email?: string; text: string }>(event) const body = await readBody<{ author: string; email?: string; text: string }>(event)
if (!body.author?.trim() || !body.text?.trim()) { if (!body.text?.trim()) {
throw createError({ statusCode: 400, statusMessage: 'Nom et message requis' }) throw createError({ statusCode: 400, statusMessage: 'Message requis' })
} }
const data = await readYaml<{ messages: any[] }>('messages.yml') const data = await readYaml<{ messages: any[] }>('messages.yml')

View File

@@ -79,10 +79,6 @@ axes:
- id: open-pdf - id: open-pdf
label: Lecture du livre label: Lecture du livre
icon: book-open icon: book-open
- id: launch-gratewizard
label: grateWizard
icon: sparkles
secondary: true
- label: Productions collectives - label: Productions collectives
description: Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie. description: Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie.
to: /gestation/productions-collectives to: /gestation/productions-collectives
@@ -102,6 +98,12 @@ axes:
gestation: true gestation: true
icon: droplets icon: droplets
gratewizard:
title: grateWizard
subtitle: Le compagnon interactif du livre et du projet
description: Explorez le modèle économique, posez vos questions, suivez le fil du raisonnement.
icon: sparkles
evenement: evenement:
title: Le librodrome, title: Le librodrome,
subtitle: c'est également un événement. subtitle: c'est également un événement.

View File

@@ -1,46 +1,48 @@
kicker: Autonomie numérique kicker: Autonomie numérique
title: Le code source title: Le code source
description: Des passages du livre qui éclairent la démarche d'autonomie numérique — maîtriser le code source, c'est maîtriser l'outil. description: "Maîtriser le code source, c'est maîtriser l'outil. L'autonomie numérique est le socle : sans elle, pas de monnaie libre, pas de décision souveraine."
meta: meta:
title: Autonomie numérique title: Autonomie numérique
extracts:
- chapter: Introduction pillars:
chapterSlug: 01-introduction - id: logiciel-libre
label: Logiciel libre
icon: code-2
text: > text: >
Ben. Pour l'autonomie. Le logiciel libre n'est pas qu'une question technique.
...C'est tout — sauf un repli. C'est la condition d'existence d'outils qui nous appartiennent —
Balkanisation ? que nenni ! que l'on peut auditer, modifier, partager.
Réfuter l'autonomie... c'est fallacieux, Sans logiciel libre, toute promesse de souveraineté numérique est creuse.
c'est nous bannir en tant qu'adultes, bien vivants, project:
restez là sans mot dire, name: wishBounty
"restez des enfants !"... text: Application pour le financement fléché des développements libres.
mmh, suspect et sans avenir. gestation: true
- chapter: Introduction to: /gestation/logiciel-libre
chapterSlug: 01-introduction
- id: authentification-wot
label: "Authentification — WoT"
icon: share-2
text: > text: >
Ne plus subir les agendas. Créer les nôtr'. Une toile de confiance décentralisée, sans autorité centrale.
On manque de repères ?... Entre autres, ... Chaque identité est certifiée par ses pairs — pas par un serveur,
Faut les trouver,... pas par une entreprise. C'est le fondement de la monnaie libre Ğ1
En produisant, les inventer. et de toute gouvernance entre égaux.
- chapter: "Raison d'être d'une monnaie" project:
chapterSlug: 04-monnaie name: trustWallet
text: Gestionnaire de confiances.
gestation: true
to: /gestation/authentification-wot
- id: cloud-libre
label: Cloud libre
icon: cloud
text: > text: >
Même accès pour tous. Héberger ses propres services pour ne dépendre de personne.
Même pouvoir de création. Serveurs, noms de domaine, infrastructure.
Ce n'est plus "Que la dette soit". Un bouquet de services complet — Drive, Visio, Forum, Wiki, CMS —
C'est "Que l'équilibre soit". et demain, une IA frugale localisée.
- chapter: "Créer une économie ?" project:
chapterSlug: 06-economie name: Bouquet de services
text: > text: "Drive, Visio, Forum, Wiki, CMS. IA frugale localisée."
Le D.U, c'est une mesure. gestation: true
Pour ne plus obliger. Pour ne plus devoir. to: /gestation/cloud-libre
Je donne à mon économie, j'alimente le réservoir.
Je compte sur les autres, sur mon économie,
Pour y trouver ma pleine mesure.
- chapter: "Et maintenant ?… action ?"
chapterSlug: 10-maintenant
text: >
Mais la monnaie n'est pas la richesse.
C'est juste le mètre... pas le tissu.
C'est le baromètre... pas le climat.
Ne confondons pas la carte et le territoire.