Compare commits

..

2 Commits

Author SHA1 Message Date
Yvv efed0b9033 Page événement : contenu structuré axes/espaces/config depuis PDF Genèse
ci/woodpecker/push/woodpecker Pipeline was successful
- evenement.yml : kicker, titre, subtitle, leitmotiv, tagline, gestation,
  description, 3 axes (numérique/économique/politique), 6 espaces, 4 config
- evenement.vue : hero complet (shadoks, logo SVG inline, badges), sections
  axes/espaces/config, styles scoped responsive
- bookplayer.config.yml → slugs 06-produire/07-echanger déjà commités
- Ajout Librodrome-Logo.png + librodrome-logo.svg (vectorisation en cours)
- Ajout PDF genèse en public/pdf/
- .gitignore, CLAUDE.md, BookSection, économique : ajustements session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 04:08:00 +01:00
Yvv 99a8b84164 Vouvoiement, libreDecision : bouton + URL
- Vouvoiement partout dans les pages numerique (cloud-libre, logiciel-libre, wot)
- Bouton "Ouvrir libreDecision" dans home AxisBlock (Décision collective)
  - AxisAction supporte href pour liens externes
  - URL prod : decision.librodrome.org
- Labels "Ouvrir libreDecision" (sans Ğ) dans citoyenne/index et [slug]
- app.config.ts : URL prod libredecision mise à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 02:37:42 +01:00
19 changed files with 1005 additions and 138 deletions
+3
View File
@@ -25,6 +25,9 @@ public/pdfjs/pdf.worker.min.mjs
# Sources originales (PDF, JPG — pas servies par l'appli) # Sources originales (PDF, JPG — pas servies par l'appli)
sources/ sources/
# Runtime data (Docker volume — never committed)
data/
# Local env files # Local env files
.env .env
.env.* .env.*
+43 -40
View File
@@ -2,81 +2,84 @@
Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don. Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don.
## Protocole de début de session
1. `git pull --rebase origin main` (récupère les commits admin git sync YAML prod)
2. Vérifier que `data/messages.yml` existe — si absent, signaler avant toute opération
3. Si l'objectif de la session n'est pas précisé, le demander
## Stack ## Stack
- **Nuxt 4** (Vue 3, TypeScript, Nitro) - Nuxt 4 (Vue 3, TypeScript, Nitro) + Nuxt Content, Pinia, UnoCSS, VueUse, Nuxt Image
- **Modules** : Nuxt Content, Pinia, UnoCSS, VueUse, Nuxt Image - Icônes : Lucide + Phosphor (via @iconify-json) ; Package manager : pnpm
- **Icônes** : Lucide + Phosphor (via @iconify-json) - Déploiement : Docker + Traefik, CI via Woodpecker
- **Package manager** : pnpm
- **Déploiement** : Docker + Traefik, CI via Woodpecker
## Structure ## Structure
``` ```
app/ app/
pages/ pages/
numerique/ # Autonomie numérique (index + [slug] détail) numerique/ # index + [slug]
economique/ # Autonomie économique (index, monnaie-libre, commande, productions-collectives) economique/ # index, monnaie-libre, commande, productions-collectives
modele-eco/ # Livre : sommaire + chapitres [slug] modele-eco/ # sommaire + chapitres [slug]
citoyenne/ # Autonomie citoyenne (index + [slug] détail) citoyenne/ # index + [slug]
en-musique/ # Player audio en-musique/ # player audio
evenement.vue # Événement admin/ # back-office (pages/, book/, songs, messages, media)
admin/ # Back-office (pages/, book/, songs, messages, media)
components/ components/
book/Actions.vue # Boutons partagés livre (player, PDF, chapitres, commande) book/Actions.vue # boutons partagés (player, PDF, chapitres, commande)
home/ # BookSection, AxisBlock, AxisGrid, HeroSection, Messages home/ # BookSection, AxisBlock, AxisGrid, HeroSection, HomeMessages
admin/, player/, song/, ui/ admin/, player/, song/, ui/
composables/ # useAudioPlayer, useBookData, useGrateWizard, usePageContent... composables/ # useAudioPlayer, useBookData, useGrateWizard, usePageContent
stores/palette.ts # 4 palettes saisonnières (été par défaut, persisté localStorage) stores/palette.ts # 4 palettes saisonnières (été par défaut, persisté localStorage)
assets/css/ # main.css (UnoCSS + overrides light mode) assets/css/main.css # UnoCSS + overrides light mode
site/ site/
pages/ # Contenu YAML par section (numerique/, economique/, citoyenne/) pages/ # Contenu YAML administrable par section (sous-dossiers)
site.yml # Config globale (nav, footer, GrateWizard) site.yml # Config globale (nav, footer, GrateWizard)
bookplayer.config.yml # Config player/chapitres bookplayer.config.yml # Config player/chapitres
data/
messages.yml # Runtime — volume Docker ../data:/src/data — JAMAIS dans git
server/ server/
api/content/pages/[...path].get.ts # GET pages YAML (chemins imbriqués) api/content/ # GET pages YAML (chemins imbriqués)
api/admin/content/pages/[...path].put.ts # PUT pages YAML api/admin/content/ # PUT pages YAML + liste
api/admin/content/pages.get.ts # Liste toutes les pages api/messages/ # GET (publiés) + POST (nouveau message)
api/admin/messages/ # GET tous + PUT (type, reply, published) + DELETE
utils/content.ts # readDataYaml/writeDataYaml (data/) + readYaml/writeYaml (site/)
middleware/redirects.ts # 301 : /gestation, /modele-eco, /decision, /lire middleware/redirects.ts # 301 : /gestation, /modele-eco, /decision, /lire
docker/ docker/
Dockerfile, docker-compose.yml, docker-compose.dev.yml Dockerfile, docker-compose.yml, docker-compose.dev.yml
``` ```
## Ports dev (CRITIQUE) ## Données runtime (CRITIQUE)
| Projet | Port | Config | - `data/messages.yml` : volume Docker monté `../data:/src/data` (relatif à `docker/`)
|--------|------|--------| - Persisté entre les rebuilds — **jamais écrasé par les commits ni par le déploiement**
| **librodrome** | **3000** | `nuxt.config.ts``devServer.port: 3000` | - Structure message : `{ id, author, email, text, type, published, createdAt, reply: { text, publishedAt } | null }`
| **GrateWizard** | **3001** | `package.json` `next dev --port 3001` | - Types : `reaction` (ancien, affiché "Réaction", plus proposé dans les formulaires) | `question` | `suggestion` | `retour`
| **SejeteralO frontend** | **3009** | `frontend/nuxt.config.ts``devServer.port: 3009` | - En dev local : `<racine>/data/messages.yml`
| **SejeteralO backend** | **8000** | Makefile → `uvicorn --port 8000` | - **Avant toute migration de chemin ou écriture sur data/ : demander confirmation**
**Ne jamais changer ces ports.**
## Intégration GrateWizard ## Intégration GrateWizard
- URL dev : `app/app.config.ts``localhost:3001` - URL dev : `app/app.config.ts``localhost:3001`
- URL prod : `https://gratewizard.axiom-team.fr` - URL prod : `https://gratewizard.axiom-team.fr`
- Bloc GrateWizard dans la section économique de la home
## Contenu administrable ## Contenu administrable
- YAML dans `site/pages/` organisé par section (sous-dossiers) - YAML dans `site/pages/` par section ; API supporte les chemins imbriqués
- API supporte les chemins imbriqués (`numerique/logiciel-libre`) - Admin : `/admin/pages` liste, `/admin/pages/{path}` édite en YAML
- Admin : `/admin/pages` liste toutes les pages, `/admin/pages/{path}` édite en YAML - Git sync auto en prod (`ADMIN_GIT_SYNC=true`) → d'où le `git pull --rebase` obligatoire en début de session
- Git sync auto en prod (ADMIN_GIT_SYNC=true)
## Commandes ## Commandes
```bash ```bash
pnpm dev # Dev server sur :3000 pnpm dev # Dev server :3000
pnpm build # Build production pnpm build # Build production
PORT=3099 node .output/server/index.mjs # Test build prod (toujours avant commit)
``` ```
## Conventions ## Conventions
- Langue du site : français - CSS via UnoCSS + variables CSS palettes ; composants Vue SFC `<script setup lang="ts">`
- Commits en français, style concis - Shadoks SVG inline thématiques sur chaque page (hidden mobile, opacity 0.180.28)
- CSS via UnoCSS (utility-first) + variables CSS palettes - Hexagramme 益 (#42 Yi, Augmentation) dans `layouts/default.vue`
- Composants Vue SFC avec `<script setup lang="ts">` - Signature § (logo calligraphique SVG gradient) dans `TheHeader.vue` — ne pas modifier sans demander
- Shadoks SVG inline thématiques sur chaque page (hidden mobile)
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineAppConfig({
}, },
}, },
libredecision: { libredecision: {
url: import.meta.dev ? 'http://localhost:3002' : 'https://decision.laplank.org', url: import.meta.dev ? 'http://localhost:3002' : 'https://decision.librodrome.org',
}, },
sejeteral0: { sejeteral0: {
url: import.meta.dev ? 'http://localhost:3009' : 'https://collectivites.librodrome.org', url: import.meta.dev ? 'http://localhost:3009' : 'https://collectivites.librodrome.org',
+6 -3
View File
@@ -52,16 +52,18 @@
<div v-if="item.actions?.length" class="axis-actions"> <div v-if="item.actions?.length" class="axis-actions">
<!-- Primary row --> <!-- Primary row -->
<div class="axis-actions-row"> <div class="axis-actions-row">
<button <component
:is="action.href ? 'a' : 'button'"
v-for="action in primaryActions(item.actions)" v-for="action in primaryActions(item.actions)"
:key="action.id" :key="action.id"
v-bind="action.href ? { href: action.href, target: '_blank', rel: 'noopener noreferrer' } : {}"
class="axis-action-btn" class="axis-action-btn"
:class="{ 'axis-action-btn--highlight': action.highlight }" :class="{ 'axis-action-btn--highlight': action.highlight }"
@click.stop="handleAction(action.id)" @click.stop="!action.href && handleAction(action.id)"
> >
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" /> <div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }} {{ action.label }}
</button> </component>
</div> </div>
<!-- Secondary row --> <!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary"> <div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
@@ -91,6 +93,7 @@ interface AxisAction {
highlight?: boolean highlight?: boolean
secondary?: boolean secondary?: boolean
to?: string to?: string
href?: string
} }
interface AxisItem { interface AxisItem {
+39 -8
View File
@@ -1,11 +1,11 @@
<template> <template>
<section class="section-padding"> <section :class="compact ? 'section-book-compact' : 'section-padding'">
<div class="container-content"> <div class="container-content">
<div class="grid items-center gap-12 md:grid-cols-2"> <div :class="['grid items-center', compact ? 'gap-5 md:grid-cols-[auto_1fr]' : 'gap-12 md:grid-cols-2']">
<!-- Book cover --> <!-- Book cover -->
<UiScrollReveal> <UiScrollReveal>
<div class="book-cover-wrapper relative"> <div class="book-cover-wrapper relative">
<div class="book-cover-3d"> <div :class="['book-cover-3d', compact && 'book-cover-3d--compact']">
<img <img
:src="content?.book.coverImage" :src="content?.book.coverImage"
:alt="content?.book.coverAlt" :alt="content?.book.coverAlt"
@@ -18,14 +18,21 @@
<!-- Content + CTAs --> <!-- Content + CTAs -->
<div> <div>
<UiScrollReveal> <UiScrollReveal>
<p class="mb-2 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.book.kicker }}</p> <p :class="['font-mono tracking-widest text-accent uppercase', compact ? 'text-xs mb-1' : 'text-sm mb-2']">
<h2 class="heading-section font-display font-bold tracking-tight text-white"> {{ content?.book.kicker }}
{{ content?.book.title }} </p>
<h2 :class="['book-heading font-display font-bold tracking-tight text-white', compact && 'book-heading--compact']">
<template v-if="compact">
<span class="block">{{ titleLine1 }}</span>
<span class="block" style="color: hsl(var(--color-text) / 0.55)">{{ titleLine2 }}</span>
</template>
<template v-else>{{ content?.book.title }}</template>
</h2> </h2>
</UiScrollReveal> </UiScrollReveal>
<UiScrollReveal :delay="100"> <UiScrollReveal :delay="100">
<p class="mt-4 text-lg leading-relaxed text-white/60"> <p :class="['leading-relaxed', compact ? 'mt-2 text-sm' : 'mt-4 text-lg text-white/60']"
:style="compact ? 'color: hsl(var(--color-text-muted)); font-size: 0.85rem' : ''">
{{ content?.book.description }} {{ content?.book.description }}
</p> </p>
</UiScrollReveal> </UiScrollReveal>
@@ -46,8 +53,10 @@
<script setup lang="ts"> <script setup lang="ts">
withDefaults(defineProps<{ withDefaults(defineProps<{
showChapters?: boolean showChapters?: boolean
compact?: boolean
}>(), { }>(), {
showChapters: true, showChapters: true,
compact: false,
}) })
defineEmits<{ defineEmits<{
@@ -56,9 +65,22 @@ defineEmits<{
}>() }>()
const { data: content } = await usePageContent('home') const { data: content } = await usePageContent('home')
const titleParts = computed(() => {
const title = content.value?.book.title ?? ''
const idx = title.indexOf('—')
if (idx === -1) return [title, '']
return [title.slice(0, idx).trim(), '— ' + title.slice(idx + 1).trim()]
})
const titleLine1 = computed(() => titleParts.value[0])
const titleLine2 = computed(() => titleParts.value[1])
</script> </script>
<style scoped> <style scoped>
.section-book-compact {
padding: 0;
}
.book-cover-wrapper { .book-cover-wrapper {
perspective: 800px; perspective: 800px;
display: flex; display: flex;
@@ -78,6 +100,10 @@ const { data: content } = await usePageContent('home')
max-width: 360px; max-width: 360px;
} }
.book-cover-3d--compact {
max-width: 120px;
}
.book-cover-3d:hover { .book-cover-3d:hover {
transform: rotateY(-8deg) rotateX(3deg) scale(1.02); transform: rotateY(-8deg) rotateX(3deg) scale(1.02);
box-shadow: box-shadow:
@@ -92,7 +118,12 @@ const { data: content } = await usePageContent('home')
transform: translateX(-50%); transform: translateX(-50%);
} }
.heading-section { .book-heading {
font-size: clamp(1.625rem, 4vw, 2.125rem); font-size: clamp(1.625rem, 4vw, 2.125rem);
} }
.book-heading--compact {
font-size: clamp(0.95rem, 2.5vw, 1.15rem);
line-height: 1.35;
}
</style> </style>
+234 -20
View File
@@ -1,25 +1,67 @@
<template> <template>
<div class="section-padding"> <div class="section-padding">
<div class="container-content"> <div class="container-content">
<div class="mx-auto max-w-2xl">
<!-- Header -->
<div class="mx-auto max-w-2xl text-center mb-8">
<div class="section-icon mx-auto mb-6"> <div class="section-icon mx-auto mb-6">
<div :class="`i-lucide-${content?.icon ?? 'landmark'}`" class="h-12 w-12" /> <div :class="`i-lucide-${content?.icon ?? 'landmark'}`" class="h-12 w-12" />
</div> </div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center"> <p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }} {{ content?.kicker }}
</p> </p>
<h1 class="font-display text-3xl font-bold mb-4" style="color: hsl(var(--color-text))">
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title ?? slug }} {{ content?.title ?? slug }}
</h1> </h1>
<p class="text-lg leading-relaxed" style="color: hsl(var(--color-text-muted))">
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }} {{ content?.description }}
</p> </p>
</div>
<!-- Features grid (for decision page) --> <!-- Navigation prev / index / next -->
<div v-if="content?.features" class="grid gap-4 sm:grid-cols-2 mb-12"> <nav class="ctx-nav mx-auto max-w-2xl mb-10">
<NuxtLink v-if="prevItem?.to" :to="prevItem.to" class="ctx-nav-btn ctx-nav-prev">
<div class="i-lucide-arrow-left h-4 w-4 shrink-0" />
<span>{{ prevItem.label }}</span>
</NuxtLink>
<div v-else class="ctx-nav-spacer" />
<NuxtLink to="/citoyenne" class="ctx-nav-btn ctx-nav-index">
<div class="i-lucide-layout-grid h-4 w-4" />
<span>Citoyenne</span>
</NuxtLink>
<NuxtLink v-if="nextItem?.to" :to="nextItem.to" class="ctx-nav-btn ctx-nav-next">
<span>{{ nextItem.label }}</span>
<div class="i-lucide-arrow-right h-4 w-4 shrink-0" />
</NuxtLink>
<div v-else class="ctx-nav-spacer" />
</nav>
<!-- Zone sections relative pour ancrer le sidebar -->
<div class="sections-area">
<!-- Sidebar sommaire -->
<aside v-if="sommaire.length > 1" class="page-sidebar">
<nav class="sommaire-sidebar">
<p class="sommaire-sidebar-title">Sur cette page</p>
<ol class="sommaire-sidebar-list">
<li v-for="(entry, ei) in sommaire" :key="ei">
<a :href="`#${entry.id}`" class="sommaire-sidebar-link">
<span class="sommaire-n">{{ ei + 1 }}</span>
<span>{{ entry.title }}</span>
</a>
</li>
</ol>
</nav>
</aside>
<!-- Features grid -->
<div
v-if="content?.features"
id="section-features"
class="grid gap-4 sm:grid-cols-2 mb-12 max-w-2xl mx-auto section-anchor"
>
<div v-for="feature in content.features" :key="feature.title" class="feature-card"> <div v-for="feature in content.features" :key="feature.title" class="feature-card">
<div class="feature-icon"> <div class="feature-icon">
<div :class="`i-lucide-${feature.icon} h-5 w-5`" /> <div :class="`i-lucide-${feature.icon} h-5 w-5`" />
@@ -30,7 +72,11 @@
</div> </div>
<!-- Project card --> <!-- Project card -->
<div v-if="content?.project" class="project-card mb-8"> <div
v-if="content?.project"
id="section-project"
class="project-card mb-8 max-w-2xl mx-auto section-anchor"
>
<div class="project-icon"> <div class="project-icon">
<div class="i-lucide-rocket h-5 w-5" /> <div class="i-lucide-rocket h-5 w-5" />
</div> </div>
@@ -47,27 +93,30 @@
</div> </div>
<!-- Extended content --> <!-- Extended content -->
<div v-if="content?.content" class="prose-block mb-10"> <div
v-if="content?.content"
id="section-content"
class="prose-block mb-10 max-w-2xl mx-auto section-anchor"
>
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))"> <p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }} {{ content.content }}
</p> </p>
</div> </div>
<div class="text-center flex flex-col items-center gap-3 sm:flex-row sm:justify-center"> <!-- Actions -->
<div class="text-center flex flex-col items-center gap-3 sm:flex-row sm:justify-center mb-4">
<UiBaseButton v-if="slug === 'decision'" :href="decisionUrl" target="_blank"> <UiBaseButton v-if="slug === 'decision'" :href="decisionUrl" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" /> <div class="i-lucide-external-link mr-2 h-4 w-4" />
Ouvrir Glibredecision Ouvrir libreDecision
</UiBaseButton> </UiBaseButton>
<UiBaseButton v-if="slug === 'tarifs-eau'" :href="sejeteral0Url" target="_blank"> <UiBaseButton v-if="slug === 'tarifs-eau'" :href="sejeteral0Url" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" /> <div class="i-lucide-external-link mr-2 h-4 w-4" />
Lancer SejeteralO Lancer SejeteralO
</UiBaseButton> </UiBaseButton>
<UiBaseButton variant="ghost" to="/citoyenne">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie citoyenne
</UiBaseButton>
</div>
</div> </div>
</div><!-- /sections-area -->
</div> </div>
</div> </div>
</template> </template>
@@ -76,15 +125,35 @@
const route = useRoute() const route = useRoute()
const slug = route.params.slug as string const slug = route.params.slug as string
const { data: content } = await usePageContent(`citoyenne/${slug}`) const [{ data: content }, { data: homeData }] = await Promise.all([
usePageContent(`citoyenne/${slug}`),
usePageContent('home'),
])
const appConfig = useAppConfig() const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#' const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#' const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({ // Sommaire
title: content.value?.meta?.title ?? slug, const sommaire = computed(() => {
const entries: { title: string; id: string }[] = []
if ((content.value?.features as unknown[])?.length) entries.push({ title: 'Fonctionnalités', id: 'section-features' })
if (content.value?.project) entries.push({ title: (content.value.project as { name: string }).name, id: 'section-project' })
if (content.value?.content) entries.push({ title: 'En savoir plus', id: 'section-content' })
return entries
}) })
// Prev / next dans la section citoyenne
interface AxisItem { label: string; to?: string; icon?: string }
const politiqueItems = computed<AxisItem[]>(
() => (homeData.value as Record<string, unknown> | null)?.axes?.politique?.items as AxisItem[] ?? [],
)
const currentPath = `/citoyenne/${slug}`
const currentIdx = computed(() => politiqueItems.value.findIndex(i => i.to === currentPath))
const prevItem = computed(() => currentIdx.value > 0 ? politiqueItems.value[currentIdx.value - 1] : null)
const nextItem = computed(() => currentIdx.value < politiqueItems.value.length - 1 ? politiqueItems.value[currentIdx.value + 1] : null)
useHead({ title: content.value?.meta?.title ?? slug })
</script> </script>
<style scoped> <style scoped>
@@ -156,4 +225,149 @@ useHead({
border-radius: 0.75rem; border-radius: 0.75rem;
background: hsl(var(--color-surface)); background: hsl(var(--color-surface));
} }
.section-anchor {
scroll-margin-top: 5.5rem;
}
/* ── Navigation prev/next ── */
.ctx-nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.5rem;
align-items: center;
}
.ctx-nav-spacer { min-width: 0; }
.ctx-nav-btn {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 0.875rem;
border-radius: 20px;
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
color: hsl(var(--color-text-muted));
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.08);
transition: all 0.15s;
min-width: 0;
}
.ctx-nav-btn:hover {
color: hsl(var(--color-primary));
border-color: hsl(var(--color-primary) / 0.25);
background: hsl(var(--color-primary) / 0.05);
}
.ctx-nav-prev { justify-content: flex-start; }
.ctx-nav-next { justify-content: flex-end; }
.ctx-nav-prev span,
.ctx-nav-next span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ctx-nav-index {
justify-content: center;
background: hsl(var(--color-primary) / 0.08);
border-color: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
font-size: 0.75rem;
padding: 0.45rem 0.75rem;
white-space: nowrap;
flex-shrink: 0;
}
.ctx-nav-index:hover { background: hsl(var(--color-primary) / 0.15); }
@media (max-width: 480px) {
.ctx-nav { grid-template-columns: auto 1fr auto; }
.ctx-nav-prev span, .ctx-nav-next span { display: none; }
.ctx-nav-prev, .ctx-nav-next { padding: 0.5rem; }
}
/* ── Zone sections ── */
.sections-area {
position: relative;
}
/* ── Sidebar sommaire ── */
.page-sidebar {
display: none;
position: absolute;
top: 0;
right: 0;
width: 10.5rem;
height: 100%;
}
@media (min-width: 1300px) {
.page-sidebar { display: block; }
}
.sommaire-sidebar {
position: sticky;
top: 5.5rem;
padding: 0.875rem;
border-radius: 14px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.07);
}
.sommaire-sidebar-title {
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--color-text-muted));
margin: 0 0 0.6rem;
}
.sommaire-sidebar-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.sommaire-sidebar-link {
display: flex;
align-items: baseline;
gap: 0.45rem;
padding: 0.28rem 0.4rem;
border-radius: 8px;
text-decoration: none;
font-size: 0.75rem;
color: hsl(var(--color-text-muted));
line-height: 1.4;
transition: color 0.12s, background 0.12s;
}
.sommaire-sidebar-link:hover {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.06);
}
.sommaire-n {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
font-family: var(--font-mono);
font-size: 0.6rem;
font-weight: 700;
flex-shrink: 0;
}
</style> </style>
+1 -1
View File
@@ -363,7 +363,7 @@
<div class="item-actions"> <div class="item-actions">
<a :href="decisionUrl" target="_blank" rel="noopener" class="action-btn action-btn--primary"> <a :href="decisionUrl" target="_blank" rel="noopener" class="action-btn action-btn--primary">
<div class="i-lucide-external-link h-3.5 w-3.5" /> <div class="i-lucide-external-link h-3.5 w-3.5" />
Ouvrir Glibredecision Ouvrir libreDecision
</a> </a>
</div> </div>
</div> </div>
+40 -2
View File
@@ -387,7 +387,8 @@
<div class="mx-auto max-w-3xl flex flex-col gap-8"> <div class="mx-auto max-w-3xl flex flex-col gap-8">
<!-- Monnaie libre --> <!-- Monnaie libre -->
<NuxtLink to="/economique/monnaie-libre" class="item-card group"> <NuxtLink to="/economique/monnaie-libre" class="item-card g1-card group">
<span class="g1-watermark" aria-hidden="true">Ğ1</span>
<div class="item-header"> <div class="item-header">
<div class="item-icon"> <div class="item-icon">
<span class="g1-icon">Ğ1</span> <span class="g1-icon">Ğ1</span>
@@ -408,6 +409,7 @@
<!-- Modèle économique — bloc livre --> <!-- Modèle économique — bloc livre -->
<div class="book-block"> <div class="book-block">
<HomeBookSection <HomeBookSection
compact
@open-player="showBookPlayer = true" @open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true" @open-pdf="showPdfReader = true"
/> />
@@ -520,12 +522,48 @@ const showPdfReader = ref(false)
} }
.book-block { .book-block {
padding: 1.5rem; padding: 1.25rem;
border-radius: 0.75rem; border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08); border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface)); background: hsl(var(--color-surface));
} }
/* Monnaie libre card */
.g1-card {
position: relative;
overflow: hidden;
background: linear-gradient(
135deg,
hsl(var(--color-surface)) 60%,
hsl(var(--color-primary) / 0.06) 100%
);
border-color: hsl(var(--color-primary) / 0.14);
}
.g1-card:hover {
border-color: hsl(var(--color-primary) / 0.3);
}
.g1-watermark {
position: absolute;
right: -0.25rem;
top: 50%;
transform: translateY(-50%);
font-family: var(--font-display);
font-weight: 900;
font-size: 5.5rem;
line-height: 1;
color: hsl(var(--color-primary) / 0.07);
pointer-events: none;
user-select: none;
letter-spacing: -0.02em;
transition: color 0.2s;
}
.g1-card:hover .g1-watermark {
color: hsl(var(--color-primary) / 0.11);
}
/* Shadok illustrations */ /* Shadok illustrations */
.shadok { .shadok {
position: absolute; position: absolute;
+224 -19
View File
@@ -1,33 +1,79 @@
<template> <template>
<div class="section-padding"> <div class="section-padding">
<div class="container-content"> <div class="container-content">
<div class="mx-auto max-w-2xl">
<!-- Header -->
<div class="mx-auto max-w-2xl text-center mb-8">
<div class="section-icon mx-auto mb-6"> <div class="section-icon mx-auto mb-6">
<span v-if="content?.icon === 'g1'" class="g1-icon">Ğ1</span> <span v-if="content?.icon === 'g1'" class="g1-icon">Ğ1</span>
<div v-else :class="`i-lucide-${content?.icon ?? 'coins'}`" class="h-12 w-12" /> <div v-else :class="`i-lucide-${content?.icon ?? 'coins'}`" class="h-12 w-12" />
</div> </div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }} {{ content?.kicker }}
</p> </p>
<h1 class="font-display text-3xl font-bold mb-4" style="color: hsl(var(--color-text))">
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }} {{ content?.title }}
</h1> </h1>
<p class="text-lg leading-relaxed" style="color: hsl(var(--color-text-muted))">
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }} {{ content?.description }}
</p> </p>
</div>
<!-- Navigation prev / index / next -->
<nav class="ctx-nav mx-auto max-w-2xl mb-10">
<NuxtLink v-if="prevItem?.to" :to="prevItem.to" class="ctx-nav-btn ctx-nav-prev">
<div class="i-lucide-arrow-left h-4 w-4 shrink-0" />
<span>{{ prevItem.label }}</span>
</NuxtLink>
<div v-else class="ctx-nav-spacer" />
<NuxtLink to="/economique" class="ctx-nav-btn ctx-nav-index">
<div class="i-lucide-layout-grid h-4 w-4" />
<span>Économique</span>
</NuxtLink>
<NuxtLink v-if="nextItem?.to" :to="nextItem.to" class="ctx-nav-btn ctx-nav-next">
<span>{{ nextItem.label }}</span>
<div class="i-lucide-arrow-right h-4 w-4 shrink-0" />
</NuxtLink>
<div v-else class="ctx-nav-spacer" />
</nav>
<!-- Zone sections relative pour ancrer le sidebar -->
<div class="sections-area">
<!-- Sidebar sommaire -->
<aside v-if="sommaire.length > 1" class="page-sidebar">
<nav class="sommaire-sidebar">
<p class="sommaire-sidebar-title">Sur cette page</p>
<ol class="sommaire-sidebar-list">
<li v-for="(entry, ei) in sommaire" :key="ei">
<a :href="`#${entry.id}`" class="sommaire-sidebar-link">
<span class="sommaire-n">{{ ei + 1 }}</span>
<span>{{ entry.title }}</span>
</a>
</li>
</ol>
</nav>
</aside>
<!-- Content --> <!-- Content -->
<div v-if="content?.content" class="prose-block mb-8"> <div
v-if="content?.content"
id="section-content"
class="prose-block mb-8 max-w-2xl mx-auto section-anchor"
>
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))"> <p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }} {{ content.content }}
</p> </p>
</div> </div>
<!-- External links --> <!-- External links -->
<div v-if="content?.links" class="flex flex-col gap-3 mb-10"> <div
v-if="content?.links"
id="section-links"
class="flex flex-col gap-3 mb-10 max-w-2xl mx-auto section-anchor"
>
<a <a
v-for="link in content.links" v-for="link in content.links"
:key="link.href" :key="link.href"
@@ -44,23 +90,37 @@
</a> </a>
</div> </div>
<div class="text-center"> </div><!-- /sections-area -->
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('economique/monnaie-libre') const [{ data: content }, { data: homeData }] = await Promise.all([
usePageContent('economique/monnaie-libre'),
usePageContent('home'),
])
useHead({ // Sommaire
title: content.value?.meta?.title ?? 'Monnaie libre', const sommaire = computed(() => {
const entries: { title: string; id: string }[] = []
if (content.value?.content) entries.push({ title: 'La théorie', id: 'section-content' })
if ((content.value?.links as unknown[])?.length) entries.push({ title: 'Ressources', id: 'section-links' })
return entries
}) })
// Prev / next dans la section économique
interface AxisItem { label: string; to?: string; icon?: string }
const economieItems = computed<AxisItem[]>(
() => (homeData.value as Record<string, unknown> | null)?.axes?.economie?.items as AxisItem[] ?? [],
)
const currentPath = '/economique/monnaie-libre'
const currentIdx = computed(() => economieItems.value.findIndex(i => i.to === currentPath))
const prevItem = computed(() => currentIdx.value > 0 ? economieItems.value[currentIdx.value - 1] : null)
const nextItem = computed(() => currentIdx.value < economieItems.value.length - 1 ? economieItems.value[currentIdx.value + 1] : null)
useHead({ title: content.value?.meta?.title ?? 'Monnaie libre' })
</script> </script>
<style scoped> <style scoped>
@@ -116,4 +176,149 @@ useHead({
color: hsl(var(--color-primary)); color: hsl(var(--color-primary));
flex-shrink: 0; flex-shrink: 0;
} }
.section-anchor {
scroll-margin-top: 5.5rem;
}
/* ── Navigation prev/next ── */
.ctx-nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.5rem;
align-items: center;
}
.ctx-nav-spacer { min-width: 0; }
.ctx-nav-btn {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.5rem 0.875rem;
border-radius: 20px;
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
color: hsl(var(--color-text-muted));
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.08);
transition: all 0.15s;
min-width: 0;
}
.ctx-nav-btn:hover {
color: hsl(var(--color-primary));
border-color: hsl(var(--color-primary) / 0.25);
background: hsl(var(--color-primary) / 0.05);
}
.ctx-nav-prev { justify-content: flex-start; }
.ctx-nav-next { justify-content: flex-end; }
.ctx-nav-prev span,
.ctx-nav-next span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ctx-nav-index {
justify-content: center;
background: hsl(var(--color-primary) / 0.08);
border-color: hsl(var(--color-primary) / 0.15);
color: hsl(var(--color-primary));
font-size: 0.75rem;
padding: 0.45rem 0.75rem;
white-space: nowrap;
flex-shrink: 0;
}
.ctx-nav-index:hover { background: hsl(var(--color-primary) / 0.15); }
@media (max-width: 480px) {
.ctx-nav { grid-template-columns: auto 1fr auto; }
.ctx-nav-prev span, .ctx-nav-next span { display: none; }
.ctx-nav-prev, .ctx-nav-next { padding: 0.5rem; }
}
/* ── Zone sections ── */
.sections-area {
position: relative;
}
/* ── Sidebar sommaire ── */
.page-sidebar {
display: none;
position: absolute;
top: 0;
right: 0;
width: 10.5rem;
height: 100%;
}
@media (min-width: 1300px) {
.page-sidebar { display: block; }
}
.sommaire-sidebar {
position: sticky;
top: 5.5rem;
padding: 0.875rem;
border-radius: 14px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.07);
}
.sommaire-sidebar-title {
font-family: var(--font-mono);
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: hsl(var(--color-text-muted));
margin: 0 0 0.6rem;
}
.sommaire-sidebar-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.sommaire-sidebar-link {
display: flex;
align-items: baseline;
gap: 0.45rem;
padding: 0.28rem 0.4rem;
border-radius: 8px;
text-decoration: none;
font-size: 0.75rem;
color: hsl(var(--color-text-muted));
line-height: 1.4;
transition: color 0.12s, background 0.12s;
}
.sommaire-sidebar-link:hover {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.06);
}
.sommaire-n {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
font-family: var(--font-mono);
font-size: 0.6rem;
font-weight: 700;
flex-shrink: 0;
}
</style> </style>
+277 -2
View File
@@ -330,15 +330,106 @@
</svg> </svg>
<div class="container-content relative z-10 text-center"> <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>
</div>
<p class="mb-3 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p> <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))"> <h1 class="page-title font-display font-extrabold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }} {{ content?.title }}
</h1> </h1>
<p class="mt-4 text-lg" style="color: hsl(var(--color-text-muted))"> <p v-if="evtContent?.subtitle" class="mt-2 text-base italic" style="color: hsl(var(--color-text-muted))">
{{ evtContent.subtitle }}
</p>
<p class="mt-4 text-lg leading-relaxed max-w-xl mx-auto" style="color: hsl(var(--color-text-muted))">
{{ content?.description }} {{ content?.description }}
</p> </p>
<!-- Leitmotiv -->
<div v-if="evtContent?.leitmotiv" class="mt-8 flex flex-col items-center gap-3">
<p class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
« {{ evtContent.leitmotiv }} »
</p>
<p v-if="evtContent?.tagline" class="font-mono text-sm tracking-widest uppercase" style="color: hsl(var(--color-primary))">
{{ evtContent.tagline }}
</p>
<span v-if="evtContent?.gestation" class="evt-gestation-badge">
<div class="i-lucide-flask-conical h-3.5 w-3.5" />
En gestation
</span>
</div> </div>
</div> </div>
</div>
<!-- Contenu événement -->
<section v-if="evtContent?.axes || evtContent?.espaces || evtContent?.config" class="evt-content section-padding">
<div class="container-content flex flex-col gap-14">
<!-- 3 axes -->
<div v-if="evtContent?.axes">
<h2 class="evt-section-title">Trois axes d'émancipation</h2>
<p class="evt-section-sub">« je subis ou je m'affranchis »</p>
<div class="axes-grid">
<div v-for="(axe, i) in evtContent.axes" :key="i" class="axe-card">
<div class="axe-icon">
<div :class="`i-lucide-${axe.icon} h-5 w-5`" />
</div>
<h3 class="font-display text-lg font-bold mb-3" style="color: hsl(var(--color-text))">
{{ axe.label }}
</h3>
<ul class="axe-list">
<li v-for="(item, j) in axe.items" :key="j">
<div class="i-lucide-arrow-right h-3.5 w-3.5 shrink-0 mt-0.5" style="color: hsl(var(--color-primary))" />
<span>{{ item }}</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Espaces permanents -->
<div v-if="evtContent?.espaces">
<h2 class="evt-section-title">Espaces & programme</h2>
<p class="evt-section-sub">Chorégraphie séquencée de plénières, ateliers et espaces permanents</p>
<div class="espaces-grid">
<div v-for="(esp, i) in evtContent.espaces" :key="i" class="espace-card">
<div class="espace-icon">
<div :class="`i-lucide-${esp.icon} h-4 w-4`" />
</div>
<div>
<p class="font-semibold text-sm" style="color: hsl(var(--color-text))">{{ esp.label }}</p>
<p class="text-xs mt-0.5 leading-relaxed" style="color: hsl(var(--color-text-muted))">{{ esp.desc }}</p>
</div>
</div>
</div>
</div>
<!-- Config -->
<div v-if="evtContent?.config">
<h2 class="evt-section-title">Format & lieu</h2>
<div class="config-grid">
<div v-for="(cfg, i) in evtContent.config" :key="i" class="config-card">
<div class="config-icon">
<div :class="`i-lucide-${cfg.icon} h-4 w-4`" />
</div>
<div>
<p class="font-semibold text-sm" style="color: hsl(var(--color-text))">{{ cfg.label }}</p>
<p class="text-xs mt-0.5" style="color: hsl(var(--color-text-muted))">{{ cfg.detail }}</p>
</div>
</div>
</div>
</div>
</div>
</section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -347,9 +438,10 @@ definePageMeta({
}) })
const { data: content } = await usePageContent('evenement') const { data: content } = await usePageContent('evenement')
const evtContent = computed(() => content.value as Record<string, any> | null)
useHead({ useHead({
title: content.value?.meta?.title ?? 'Évènement', title: evtContent.value?.meta?.title ?? 'Évènement',
}) })
</script> </script>
@@ -555,4 +647,187 @@ useHead({
display: none; display: none;
} }
} }
/* ── Logo SVG ── */
.evt-logo-wrap {
width: 4.5rem;
height: 4.5rem;
display: flex;
align-items: center;
justify-content: center;
color: hsl(var(--color-primary));
filter: drop-shadow(0 0 14px hsl(var(--color-primary) / 0.25));
}
.evt-logo {
width: 100%;
height: 100%;
}
/* ── Badge gestation hero ── */
.evt-gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background: hsl(var(--color-accent) / 0.12);
border: 1px solid hsl(var(--color-accent) / 0.25);
color: hsl(var(--color-accent));
font-size: 0.75rem;
font-weight: 600;
font-family: var(--font-mono);
letter-spacing: 0.04em;
}
/* ── Section contenus événement ── */
.evt-content {
background: hsl(var(--color-bg));
}
.evt-section-title {
font-family: var(--font-display);
font-size: clamp(1.25rem, 3vw, 1.75rem);
font-weight: 800;
color: hsl(var(--color-text));
margin-bottom: 0.25rem;
}
.evt-section-sub {
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: hsl(var(--color-primary));
margin-bottom: 1.5rem;
}
/* ── Axes ── */
.axes-grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.axes-grid { grid-template-columns: repeat(3, 1fr); }
}
.axe-card {
padding: 1.5rem;
border-radius: 16px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.07);
transition: border-color 0.2s, box-shadow 0.2s;
}
.axe-card:hover {
border-color: hsl(var(--color-primary) / 0.25);
box-shadow: 0 4px 24px hsl(var(--color-primary) / 0.06);
}
.axe-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.625rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
margin-bottom: 1rem;
}
.axe-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.axe-list li {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.82rem;
line-height: 1.5;
color: hsl(var(--color-text-muted));
}
/* ── Espaces ── */
.espaces-grid {
display: grid;
gap: 0.75rem;
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.espaces-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.espaces-grid { grid-template-columns: repeat(3, 1fr); }
}
.espace-card {
display: flex;
align-items: flex-start;
gap: 0.875rem;
padding: 1rem 1.25rem;
border-radius: 12px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.06);
transition: border-color 0.15s;
}
.espace-card:hover {
border-color: hsl(var(--color-accent) / 0.25);
}
.espace-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: hsl(var(--color-accent) / 0.1);
color: hsl(var(--color-accent));
flex-shrink: 0;
}
/* ── Config ── */
.config-grid {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 640px) {
.config-grid { grid-template-columns: repeat(4, 1fr); }
}
.config-card {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
border-radius: 12px;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.06);
}
.config-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.875rem;
height: 1.875rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
flex-shrink: 0;
}
</style> </style>
+1 -1
View File
@@ -14,7 +14,7 @@ services:
ports: ports:
- 3000 - 3000
volumes: volumes:
- ./data:/src/data - ../data:/src/data
- ./public:/src/public - ./public:/src/public
restart: always restart: always
labels: labels:
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+25
View File
@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 80" fill="none" role="img" aria-label="Librodrome">
<!--
Logotype calligraphique Librodrome — § (section sign) brush-stroke.
Vectorisé depuis le PNG source (analyse pixel, 200×200 px).
Tracé S calligraphique : crochet haut-droit → arc gauche → arc droit → fin centre.
3 couches opacité : effet pinceau naturaliste.
currentColor → s'adapte automatiquement aux 4 palettes.
-->
<!-- Halo — diffusion pinceau -->
<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"
/>
<!-- Corps — épaisseur principale -->
<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"
/>
<!-- Trait vif — ligne calligraphique -->
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.
+68 -4
View File
@@ -1,5 +1,69 @@
kicker: Bientôt
title: En gestation
description: Cette rubrique est en cours de préparation.
meta: meta:
title: Évènement title: Le Librodrome — L'événement
kicker: Performance d'émancipation civile
title: Le Librodrome
subtitle: "Une paece — performance artistique d'émancipation civile et économique"
leitmotiv: Construire une autonomie collective.
tagline: Rendre possible. Passer la seconde.
gestation: true
description: >
Synergies entre collectifs, associations et coopératives — autour de trois axes :
numérique, économique, politique. « Je subis ou je m'affranchis. »
axes:
- icon: cpu
label: Numérique
items:
- Affranchissement GAFAM — logiciels libres, Linux, migration
- Cloud local décentralisé — Nextcloud, IPFS, Matrix, PeerTube
- IA collective locale — Mistral, UPlanet
- icon: coins
label: Économique
items:
- Monnaie libre (June Ğ1) vs monnaie-dette
- Économie du don — amorcer les filières, couvrir besoins et plaisirs
- Productions versatiles — énergie, alimentaire, agricole
- icon: landmark
label: Politique
items:
- Autonomie d'un bassin de vie — accessible et reproductible
- "Pragmatique : on parle chiffres, terrain, zéro étiquette"
- Feuilles de route et graphe des synergies
espaces:
- icon: cpu
label: Salle des machines
desc: Install Linux, on-boarding June Ğ1 et cryptos, FabLab ouvert, visios
- icon: shopping-bag
label: "Ğ(marché)"
desc: Expérience laboratoire in vivo — liberté de choisir sa monnaie, construction collective d'échelles de valeurs relatives
- icon: users
label: Ateliers & feuilles de route
desc: Animateurs initiés, préparés en amont — cartographies, synergies, restitutions collectives
- icon: music-2
label: Scène musicale & théâtrale
desc: Performances, concerts, lectures — chill out en préau
- icon: utensils
label: Buvette & restauration
desc: Lié au Ğ(marché) — expérience de l'économie du don en direct
- icon: gamepad-2
label: Géconomicus
desc: "Jeu économique — 3 parties dont 1 sans monnaie, 15 joueurs, 3 animateurs, spectateurs"
config:
- icon: calendar
label: Ven → Sam → Dim
detail: "5 jours, 4 nuits (+ jeudi install & répétitions + lundi after)"
- icon: users
label: 100 à 200 personnes
detail: Public 2 jours — junistes 3 jours
- icon: map-pin
label: Lieu privé — Drôme
detail: Événement sur invitation, lieu en cours de négociation
- icon: wifi
label: Fibre + LAN wifi
detail: Salle des machines, salle de conf, salles de production, campement 40-60 tentes
+6
View File
@@ -109,6 +109,12 @@ axes:
description: Se donner les moyens de la décision collective. description: Se donner les moyens de la décision collective.
to: /citoyenne/decision to: /citoyenne/decision
icon: gavel icon: gavel
actions:
- id: open-libredecision
label: Ouvrir libreDecision
icon: external-link
highlight: true
href: https://decision.librodrome.org
- label: Tarifs de l'eau - label: Tarifs de l'eau
description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la
décision à la population des communes. décision à la population des communes.
+13 -13
View File
@@ -24,19 +24,19 @@ sections:
title: Mots de passe title: Mots de passe
text: > text: >
Stockés côté serveur, volés par millions. Have I Been Pwned recense Stockés côté serveur, volés par millions. Have I Been Pwned recense
des milliards de comptes compromis. Ta sécurité dépend intégralement des milliards de comptes compromis. Votre sécurité dépend intégralement
de la sécurité d'un tiers que tu ne contrôles pas. de la sécurité d'un tiers que vous ne contrôlez pas.
- icon: chrome - icon: chrome
title: "Se connecter avec Google" title: "Se connecter avec Google"
text: > text: >
Déléguer l'authentification à Google ou Meta, c'est en faire l'infrastructure Déléguer l'authentification à Google ou Meta, c'est en faire l'infrastructure
d'identité mondiale. Un compte suspendu = toutes tes connexions coupées. d'identité mondiale. Un compte suspendu = toutes vos connexions coupées.
Ces entreprises savent quand, où et à quoi tu te connectes. Ces entreprises savent quand, où et à quoi vous vous connectez.
- icon: fingerprint - icon: fingerprint
title: Biométrie — irrévocable title: Biométrie — irrévocable
text: > text: >
Un mot de passe compromis se change. Une empreinte digitale jamais. Un mot de passe compromis se change. Une empreinte digitale jamais.
Si tes données biométriques sont volées — et elles l'ont été Si vos données biométriques sont volées — et elles l'ont été
(OPM breach USA 2015, 5,6 millions d'empreintes) — la compromission est définitive. (OPM breach USA 2015, 5,6 millions d'empreintes) — la compromission est définitive.
- icon: building-2 - icon: building-2
title: Autorités de Certification (X.509) title: Autorités de Certification (X.509)
@@ -49,22 +49,22 @@ sections:
title: Comment fonctionne une Web of Trust title: Comment fonctionne une Web of Trust
steps: steps:
- n: 1 - n: 1
title: Générer une paire de clés sur ta machine title: Générer une paire de clés sur votre machine
text: > text: >
Clé privée (secrète, ne quitte jamais ton appareil) et clé publique Clé privée (secrète, ne quitte jamais votre appareil) et clé publique
(diffusée librement). Ce qui est signé avec la clé privée peut être (diffusée librement). Ce qui est signé avec la clé privée peut être
vérifié par n'importe qui avec la clé publique. vérifié par n'importe qui avec la clé publique.
tip: "Algorithme recommandé : Ed25519 — clé courte, rapide, sans paramètres douteux" tip: "Algorithme recommandé : Ed25519 — clé courte, rapide, sans paramètres douteux"
- n: 2 - n: 2
title: Publier ta clé publique title: Publier votre clé publique
text: > text: >
Dans un DID Document, sur un keyserver, dans ton profil Duniter. Dans un DID Document, sur un keyserver, dans votre profil Duniter.
N'importe qui peut vérifier tes signatures. Personne ne peut usurper N'importe qui peut vérifier vos signatures. Personne ne peut usurper
ton identité sans ta clé privée. votre identité sans votre clé privée.
- n: 3 - n: 3
title: Être certifié par des membres certifiés title: Être certifié par des membres certifiés
text: > text: >
Dans Duniter, 5 membres certifiés (sigQty=5) certifient que tu es une personne réelle, Dans Duniter, 5 membres certifiés (sigQty=5) certifient que vous êtes une personne réelle,
dans un rayon de 3 sauts du nœud sentinelle (stepMax=3). dans un rayon de 3 sauts du nœud sentinelle (stepMax=3).
La confiance émerge du graphe — pas d'un serveur central. La confiance émerge du graphe — pas d'un serveur central.
- n: 4 - n: 4
@@ -78,7 +78,7 @@ sections:
text: > text: >
La même clé permet de s'authentifier à des services La même clé permet de s'authentifier à des services
et de signer des actes (documents, transactions, votes). et de signer des actes (documents, transactions, votes).
Tout ce que tu fais est vérifiable. Rien n'est transmissible à ton insu. Tout ce que vous faites est vérifiable. Rien n'est transmissible à votre insu.
- type: insight - type: insight
title: Le seul système sans autorité centrale ni biométrie title: Le seul système sans autorité centrale ni biométrie
+11 -11
View File
@@ -6,8 +6,8 @@ title: Cloud libre
icon: cloud icon: cloud
description: > description: >
Tes données vivent dans des datacenters dont tu ne connais pas l'adresse, Vos données vivent dans des datacenters dont vous ne connaissez pas l'adresse,
sous des juridictions que tu ne contrôles pas. Sortir de là est possible — sous des juridictions que vous ne contrôlez pas. Sortir de là est possible —
en trois paliers : auto-hébergement, services fédérés, architectures décentralisées. en trois paliers : auto-hébergement, services fédérés, architectures décentralisées.
Ces dernières émergent à peine. Ces dernières émergent à peine.
@@ -23,13 +23,13 @@ sections:
- icon: shield - icon: shield
title: RGPD — conformité structurelle title: RGPD — conformité structurelle
text: > text: >
Google, Amazon, Microsoft stockent tes données sous juridiction américaine (CLOUD Act). Google, Amazon, Microsoft stockent vos données sous juridiction américaine (CLOUD Act).
L'auto-hébergement ou un hébergeur associatif européen offre L'auto-hébergement ou un hébergeur associatif européen offre
une conformité RGPD structurelle — pas seulement déclarative. une conformité RGPD structurelle — pas seulement déclarative.
- icon: eye-off - icon: eye-off
title: Vie privée des données title: Vie privée des données
text: > text: >
Dans un cloud commercial, tes documents, photos et communications Dans un cloud commercial, vos documents, photos et communications
sont analysés, profilés, monétisés. Ce n'est pas une théorie — sont analysés, profilés, monétisés. Ce n'est pas une théorie —
c'est le modèle économique déclaré. c'est le modèle économique déclaré.
- icon: zap - icon: zap
@@ -40,10 +40,10 @@ sections:
Héberger chez soi ou mutualiser dans un hébergeur associatif Héberger chez soi ou mutualiser dans un hébergeur associatif
réduit l'empreinte réelle. réduit l'empreinte réelle.
- icon: key - icon: key
title: Propriété réelle de tes données title: Propriété réelle de vos données
text: > text: >
Un service cloud peut fermer, changer ses CGU, couper ton compte. Un service cloud peut fermer, changer ses CGU, couper votre compte.
Tes données te reviennent quand tu les héberges toi-même — Vos données vous reviennent quand vous les hébergez vous-même —
portabilité réelle, pas juste un bouton "Exporter". portabilité réelle, pas juste un bouton "Exporter".
- icon: git-branch - icon: git-branch
title: Résilience décentralisée title: Résilience décentralisée
@@ -60,7 +60,7 @@ sections:
icon: server icon: server
badge: Maîtrise totale badge: Maîtrise totale
text: > text: >
Ton propre serveur chez toi ou chez un hébergeur associatif. Votre propre serveur chez vous ou chez un hébergeur associatif.
Contrôle absolu, RGPD pleinement compatible. Contrôle absolu, RGPD pleinement compatible.
YunoHost installe plus de 200 applications libres en un clic. YunoHost installe plus de 200 applications libres en un clic.
tools: tools:
@@ -74,8 +74,8 @@ sections:
badge: Interconnecté badge: Interconnecté
text: > text: >
Des instances indépendantes interconnectées via des protocoles ouverts Des instances indépendantes interconnectées via des protocoles ouverts
(ActivityPub, Matrix). Tu rejoins un serveur de confiance (ActivityPub, Matrix). Vous rejoignez un serveur de confiance
ou héberges le tien. Les données restent chez l'opérateur choisi. ou hébergez le vôtre. Les données restent chez l'opérateur choisi.
tools: tools:
- "Mastodon — réseau social fédéré" - "Mastodon — réseau social fédéré"
- "PeerTube — vidéo hébergée et fédérée" - "PeerTube — vidéo hébergée et fédérée"
@@ -175,7 +175,7 @@ sections:
C'est une tension réelle, non résolue, que le législateur européen commence à instruire C'est une tension réelle, non résolue, que le législateur européen commence à instruire
(lignes directrices CEPD 02/2025). (lignes directrices CEPD 02/2025).
points: points:
- "Auto-hébergement : conformité RGPD totale — tu effaces ce que tu veux" - "Auto-hébergement : conformité RGPD totale — vous effacez ce que vous voulez"
- "Services fédérés : dépend de l'opérateur de l'instance — choisir un hébergeur CHATONS" - "Services fédérés : dépend de l'opérateur de l'instance — choisir un hébergeur CHATONS"
- "IPFS / blockchain : chiffrer + détruire la clé (la donnée devient inintelligible). Accepté par la CNIL sous conditions strictes." - "IPFS / blockchain : chiffrer + détruire la clé (la donnée devient inintelligible). Accepté par la CNIL sous conditions strictes."
- "Off-chain data : stocker uniquement un hash sur la chaîne, les données personnelles sur un serveur effaçable — approche la plus propre" - "Off-chain data : stocker uniquement un hash sur la chaîne, les données personnelles sur un serveur effaçable — approche la plus propre"
+11 -11
View File
@@ -39,20 +39,20 @@ sections:
- icon: users - icon: users
title: Bien commun title: Bien commun
text: > text: >
Le logiciel libre est un commun numérique. Tu n'es pas client, tu participes. Le logiciel libre est un commun numérique. Vous n'êtes pas client, vous participez.
Cohérent avec l'économie du don — l'argent économisé sur les licences Cohérent avec l'économie du don — l'argent économisé sur les licences
peut financer des communs. peut financer des communs.
- icon: lock-open - icon: lock-open
title: Indépendance réelle title: Indépendance réelle
text: > text: >
Pas de mise à jour forcée qui casse le système. Pas d'abonnement qui expire Pas de mise à jour forcée qui casse le système. Pas d'abonnement qui expire
et verrouille tes fichiers. Ton OS t'appartient vraiment. et verrouille vos fichiers. Votre OS vous appartient vraiment.
- icon: bot - icon: bot
title: Assistance permanente gratuite title: Assistance permanente gratuite
text: > text: >
Un LLM open source installé localement (Ollama + phi3:mini, 4 Go de RAM) Un LLM open source installé localement (Ollama + phi3:mini, 4 Go de RAM)
peut rédiger les commandes shell dans ton terminal sur simple demande en français. peut rédiger les commandes shell dans votre terminal sur simple demande en français.
Sans connexion à aucun serveur. Sans abonnement. Sans envoyer tes données. Sans connexion à aucun serveur. Sans abonnement. Sans envoyer vos données.
- type: fiche - type: fiche
title: "Fiche de migration — 6 étapes" title: "Fiche de migration — 6 étapes"
@@ -65,8 +65,8 @@ sections:
- n: 2 - n: 2
title: Tester sans risque — Live USB title: Tester sans risque — Live USB
text: > text: >
Démarre Linux Mint depuis une clé USB. Tu explores, tu testes LibreOffice, Démarrez Linux Mint depuis une clé USB. Vous explorez, testez LibreOffice,
tu vérifies le WiFi — sans toucher à ton Windows existant. vérifiez le WiFi — sans toucher à votre Windows existant.
tip: "Créer la clé : balenaEtcher (gratuit) + image Linux Mint sur linuxmint.com" tip: "Créer la clé : balenaEtcher (gratuit) + image Linux Mint sur linuxmint.com"
- n: 3 - n: 3
title: Choisir sa distribution title: Choisir sa distribution
@@ -77,8 +77,8 @@ sections:
- n: 4 - n: 4
title: Installer — double-boot ou remplacement title: Installer — double-boot ou remplacement
text: > text: >
Double-boot si tu veux garder Windows le temps de la transition. Double-boot si vous voulez garder Windows le temps de la transition.
Remplacement complet quand tu es prêt·e — c'est le vrai départ. Remplacement complet quand vous êtes prêt·e — c'est le vrai départ.
- n: 5 - n: 5
title: Installer ses équivalents libres title: Installer ses équivalents libres
text: > text: >
@@ -175,9 +175,9 @@ sections:
- type: llm - type: llm
title: Un assistant shell local — sans cloud title: Un assistant shell local — sans cloud
text: > text: >
Un petit LLM open source sur ta machine répond à tes questions sur les commandes Linux Un petit LLM open source sur votre machine répond à vos questions sur les commandes Linux
en langage naturel, sans envoyer aucune donnée à un serveur distant. en langage naturel, sans envoyer aucune donnée à un serveur distant.
Il suggère. Toi tu décides. Il suggère. Vous décidez.
tool: Ollama tool: Ollama
tool_url: https://ollama.com tool_url: https://ollama.com
model: "phi3:mini" model: "phi3:mini"
@@ -187,7 +187,7 @@ sections:
- "ollama pull phi3:mini" - "ollama pull phi3:mini"
- "ollama run phi3:mini \"Comment trouver les fichiers modifiés depuis 7 jours ?\"" - "ollama run phi3:mini \"Comment trouver les fichiers modifiés depuis 7 jours ?\""
rules: rules:
- Lis chaque commande avant de l'exécuter — le LLM suggère, tu valides - Lisez chaque commande avant de l'exécuter — le LLM suggère, vous validez
- "Utilise --dry-run ou -n quand disponible (rsync, make…)" - "Utilise --dry-run ou -n quand disponible (rsync, make…)"
- Pour les commandes root/sudo, vérification manuelle obligatoire - Pour les commandes root/sudo, vérification manuelle obligatoire
- "Préfère phi3:mini ou qwen2.5-coder — pas les modèles uncensored" - "Préfère phi3:mini ou qwen2.5-coder — pas les modèles uncensored"