Compare commits

...

50 Commits

Author SHA1 Message Date
Yvv
b9e6b4a96c Messages : types, réponses, sauts de ligne, data volume
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- HomeMessages : type pill (Réaction/Question/Suggestion/Retour) + sélecteur dans le formulaire (sans Réaction)
- HomeMessages : white-space: pre-line sur les messages
- Page /messages : type pill + white-space: pre-line (idem home)
- Admin : badge type coloré + sélecteur d'édition + formulaire réponse
- API : type et reply dans PUT ; readDataYaml/writeDataYaml (data/ volume Docker)
- main.css : overrides light mode text-white/55, /75, /90

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 05:56:11 +01:00
Yvv
c52fa6007d Page /messages : afficher les réponses publiées
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Type badge + auteur + date comme sur la home
- Réponse avec liaison graphique (reply-thread/connector/block)
- Style adaptatif light/dark cohérent avec HomeMessages.vue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 05:41:24 +01:00
Yvv
d4ff840e13 bookplayer.config.yml : slugs 06-produire et 07-echanger
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- chapterSongs et chapterPages : 06-economie → 06-produire, 07-echange → 07-echanger
- Les liens "Lire dans le PDF" depuis les chapitres 6 et 7 s'ouvrent maintenant sur les bonnes pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 00:31:55 +01:00
Yvv
7691cc4139 Chapitres condensés, renommages, numéros de page
- 06-economie → 06-produire (titre "Produire"), 07-echange → 07-echanger
- Frontmatter : champ page: ajouté sur les 11 chapitres (p.9 à p.199)
- content.config.ts : page: z.number().optional() dans le schéma
- modele-eco/index.vue : numéro de page affiché sur chaque carte, hero réduit
- 11 chapitres condensés à ~moitié : voix de l'auteur conservée,
  répétitions et transitions secondaires supprimées, concepts clés préservés

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 00:28:09 +01:00
Yvv
088333e4d4 Monnaie libre : réécriture dans la voix du livre, liens forums, purge co-créer
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- monnaie-libre.yml : texte entièrement réécrit (cavalerie, double symétrie, DU,
  toile de confiance, mesure de gratitude) + 3 nouveaux liens (duniter.org,
  forum.duniter.org, forum.monnaie-libre.fr)
- Suppression de "co-créer/co-créée/co-création" dans tous les fichiers :
  economique.yml, home.yml, authentification-wot.yml, cloud-libre.yml,
  content/book/05-trm.md, content/book/11-annexes.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 22:51:28 +01:00
Yvv
07449de187 Pages détail numérique : sommaire flottant, nav ctx, shadoks geek, contenu enrichi
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- [slug].vue : sommaire sticky (overflow:clip sur parent), prev/next en haut, 6 shadoks geek (pinguin+USB, web-of-trust, rubber-duck, caféine, debugger loupe, rack serveur)
- Nouveaux types de sections : territoire (bouquet sweethomeCloud, 2 modèles éco, tableau matériel dépliable), projet (carte gestation)
- cloud-libre.yml : section sweethomeCloud complète avec infra 50 000 hab. (~2€/an/hab)
- authentification-wot.yml : trustWallet, correction WoT Duniter (Ed25519+Scrypt, sigQty=5, stepMax=3), DID/VC standards
- logiciel-libre.yml : carte projet wishBounty
- home.yml + numerique.yml : cloud-libre → sweethomeCloud, description RGPD/local-first
- AxisBlock.vue : bulles de présentation inline dans les cards (plus de tooltip absolu)
- Analytics : useTracking.ts (Umami), docker-compose.umami.yml, /api/stats fédération
- nuxt.config.ts : config Umami runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 04:40:48 +01:00
Yvv
9d92c4a5b3 Fix typos blanches admin lightmode + hero audience
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Remplace color:white → hsl(var(--color-text)) dans tous les composants admin
  (AdminFieldText, AdminFieldTextarea, AdminFormSection, AdminMarkdownEditor,
  AdminMediaBrowser, AdminSidebar, book/index, book/[slug], login, messages, site, songs)
- Conserve color:white uniquement sur fond primary (AdminSaveButton, login-btn)
- Hero home : ajout bloc audience/addressees (clé distincte pour éviter conflit YAML)
- home.yml : réordonne axes (citoyenne en premier — effet triangle)
- TypewriterText : affiche le second bloc avec séparateur fin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 16:08:03 +01:00
Yvv
8f548afb17 Palette dynamique dans BookPlayer et admin
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- BookPlayer : toutes les couleurs HSL en dur remplacées par variables CSS palette
- Admin (sidebar, formulaires, pages, book, songs, messages, media, login) : idem
- L'ambiance graphique suit maintenant la palette active partout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:41:23 +01:00
Yvv
9caf11c8ab Restructuration sections, contenu administrable, shadoks, palette été
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Structure par section : /numerique, /economique, /citoyenne (plus de /gestation)
- Chaque section a index + sous-pages avec contenu YAML administrable
- API content supporte les chemins imbriqués ([...path])
- Admin : liste des pages + éditeur par section
- Page /economique : monnaie libre (picto Ğ1), modèle éco, productions collectives, commande livre
- Page /citoyenne : decision (CTA Glibredecision), tarifs-eau (CTA SejeteralO)
- BookActions : composant partagé (player, PDF, chapitres, commande) sur home, eco et modele-eco
- GrateWizard remonté dans la section économique de la home
- Palette été par défaut, choix persisté en localStorage
- Fix lisibilité été (text-white/65 + variables CSS)
- Shadoks thématiques sur toutes les pages (8-10 par page, métiers variés)
- Redirections 301 : /gestation/*, /modele-eco/*, /decision, /lire/*
- README, CONTRIBUTING, CLAUDE.md mis à jour

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 16:13:46 +01:00
Yvv
c564e7be5f 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>
2026-03-05 03:06:48 +01:00
Yvv
3a5c40a886 Décision : picto gavel titre + parchemin docs, GrateWizard embed sans double scroll
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Page décision : icône gavel pour le titre, scroll-text pour Documents de référence
- GrateWizard : ouverture en mode embed (?embed=true&hideTabBar=true) avec
  scrollbars=no pour supprimer le double scroll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:48:52 +01:00
Yvv
fbc2867163 Refonte accueil : hero typo statique, axes icônes, menu italic, page numérique
- Hero : 5 lignes typographiques alternées (bold/light/accent/caps/italic),
  citations et axes dans un bloc discret dépliable
- Icônes axes : Ğ1 custom, balance (éco don), graphe (WoT), marteau (décision),
  pictos plus lumineux (glow)
- Menu : Autonomie en italique + grand, Événement majuscule
- Page /autonomie renommée /numerique avec redirect 301
- Sceau hexagramme 益 Yì dans le layout, BookSection dans /modele-eco
- Fonts Syne + Space Grotesk, dark theme éclairci
- Popup GrateWizard agrandie (480×860)
- Actions AxisBlock : primary côte à côte, secondary séparé dessous

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:34:30 +01:00
Yvv
082a17d09b Fix accueil : hero fade doux, icônes safelist, blocs cliquables, menu, dark fort
- Hero : réécriture composable timeout pur (plus de Transition callbacks)
  Animation fade opacity 1s très douce, lisible
- Icônes : safelist UnoCSS dans nuxt.config.ts (résout pastilles vides)
- Menu : mis à jour site.yml (Numérique/Économique/Citoyenne/Événement)
- Blocs : card entière cliquable, zone actions séparée (border-top)
- Économie du don : lié à /modele-eco (page chapitres préservée)
- Tarifs de l'eau : bouton SejeteralO (localhost:3009 / collectivites.librodrome.org)
- Dark theme fort : bg 220 12% 15%, surface 19%, surface-light 24%
- Config SejeteralO + Glibredecision dans app.config.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 04:08:47 +01:00
Yvv
97ba6dd04c Redesign accueil : grille 3 axes, hero fade/swipe, pages gestation et décision
- Hero : animation fade-in/fade-out + swipe (useTypewriter composable + TypewriterText)
- 3 axes : Autonomie numérique, économique, citoyenne (AxisBlock + AxisGrid)
- Pages gestation avec présentations (wishBounty, trustWallet, Cloud libre)
- Page /decision : plateforme Décision collective (lien Glibredecision)
- Bloc événement distinct en bas des axes
- Nav : Numérique / Économique / Citoyenne / Événement
- Dark theme éclairci (bg 7→10%, surface 12→14%)
- Suppression BookSection + GrateWizardTeaser (remplacés par AxisGrid)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 03:49:07 +01:00
Yvv
f0338cca5e Fix déroulant PDF en production : polyfills DOMMatrix + worker pdfjs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
pdfjs-dist en Node.js pur (hors Vite) nécessite :
- DOMMatrix, Path2D, ImageData polyfills (pas de DOM en Node)
- pdf.worker.mjs copié dans le build (traceInclude dans nitro config)
Testé : 61 entrées retournées en mode production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:24:59 +01:00
Yvv
c6b9abf2f3 Fix chemin PDF en production pour l'API pdf-outline
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
En prod le PDF est dans .output/public/, pas dans public/.
Cherche dans les deux emplacements (dev et prod).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:55:14 +01:00
Yvv
df0409fec3 Fix déroulant sommaire PDF : onMounted au lieu de import.meta.client
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Le $fetch dans import.meta.client ne se ré-exécutait pas après hydratation SSR.
onMounted garantit l'exécution côté client après le montage du composant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:42:58 +01:00
Yvv
1af00cc64c Admin : déroulant sommaire PDF par chapitre, transitions pages, URL GrateWizard
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Ajout API /api/admin/pdf-outline (extraction sommaire PDF côté serveur via pdfjs-dist)
- Déroulant <select> dans chaque ligne de chapitre admin avec les 61 titres/sous-titres du PDF
- Sauvegarde des associations chapitre→page PDF via config YAML
- Transition douce (fondu 1s/1.2s) pour le changement de pages dans le viewer PDF
- Correction des numéros de pages réels dans chapterPages (extraits du sommaire PDF)
- URL GrateWizard prod → gratewizard.axiom-team.fr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:29:54 +01:00
Yvv
8a38c86794 Fix prod 404 : retrait volumes git sync qui cassent le conteneur
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Les volumes ../site, ../content, ../public montés dans docker-compose
écrasaient les fichiers du conteneur avec des chemins host inexistants.
Retour à la config d'origine. Le git sync sera configuré ultérieurement
quand le serveur aura un clone avec accès push.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:44:44 +01:00
Yvv
17f39e735d Fix build prod : postinstall tolérant + copy-pdfjs dans Dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- postinstall : test -f avant d'appeler copy-pdfjs.sh (absent pendant pnpm install Docker)
- Dockerfile : RUN copy-pdfjs.sh après COPY . . pour copier les fichiers PDF.js
- Dockerfile : COPY content/ en production pour Nuxt Content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:26:05 +01:00
Yvv
7ea19e2247 Viewer PDF.js mode livre avec signets, fix hydratation SSR
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Viewer PDF.js mode livre : double page côte à côte, navigation prev/next visuelle et clavier
- Panneau signets (outline) avec tout déplier/replier, highlight du spread courant
- Page 1 = couverture seule, puis paires 2-3, 4-5, etc.
- Navigation clavier : flèches, espace, Home/End
- Redimensionnement auto des canvas au resize
- Fix hydratation SSR : bookData.init() sans await dans ChapterHeader et SongBadges
- BookPdfReader : iframe vers /pdfjs/viewer.html au lieu du viewer natif
- Script postinstall pour copier pdf.min.mjs depuis node_modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 18:16:47 +01:00
Yvv
9525ed3953 Bouton PDF par chapitre, badges morceaux améliorés, PDF configurable admin, git sync admin→prod
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Bouton PDF blanc par chapitre avec numéro de page (ChapterHeader)
- Badges morceaux plus visibles (bordure, poids, hover) dans ChapterHeader et SongBadges
- PDF viewer : page cible + panneau signets ouverts par défaut (BookPdfReader)
- Config YAML : pdfFile dans book, chapterPages pour le mapping chapitre→page
- Admin book : section PDF du livre avec chemin éditable et sauvegarde
- Git sync automatique : chaque sauvegarde admin commit+push en prod (ADMIN_GIT_SYNC=true)
- Docker : git installé en prod, volumes pour .git/site/content/public

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 15:32:38 +01:00
Yvv
b02368a15b Fix build prod : Pinia CJS default import crash Node 22+
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Pinia résout vers pinia.prod.cjs en production, Rollup convertit
require('vue') en default import ESM invalide. Script post-build
remplace par un namespace import (* as).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:36:37 +01:00
1f47533c77 Merge branch 'develop'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-02-27 19:40:43 +01:00
Yvv
e6c91fea7d Replie les paroles par défaut dans le player persistant
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Toggle discret pour les afficher au besoin (usage autonome).
Gain de place dans le panel déployé.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:11:32 +01:00
Yvv
25bfc07b59 Fix double-fire player, navigation par morceaux, admin labels morceaux
- BookPlayer : navigation par playlist (9 morceaux) au lieu de 11 chapitres
- stopPropagation clavier → plus de saut 1→3→5
- Sommaire aligné avec titres des morceaux
- Bouton back aligné avec clavier (toujours morceau précédent)
- Admin chapitres : tags morceaux cliquables avec étoile primary
- Admin liste chapitres : badges morceaux associés
- Éditeur markdown en vue split par défaut

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 14:08:58 +01:00
Yvv
8803087e77 Fix player : plus de saut de morceaux, mode scroll par défaut, supprime toggle paginé, media → sources
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:10:12 +01:00
Yvv
14d3a7b3e3 Logo § restauré, couleur unie partout, palettes printemps/été plus chaudes, rectifs admin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:42:44 +01:00
Yvv
d8439cba0f 9 morceaux : Relativité supprimé, Créer une économie = #8, Coder la liberté = #9
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 02:21:25 +01:00
Yvv
0308785de9 Supprime media/ du repo, gardé en local via .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 02:14:59 +01:00
Yvv
52c0af4c83 10 morceaux corrigés : titres, ordre, IDs et fichiers audio renommés
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:59:07 +01:00
Yvv
8d9feed760 Préfixe ED-## sur les fichiers audio et config à jour
Renomme les 9 morceaux avec préfixe album ED (Économie du Don) + numéro
d'ordre conforme aux sources dans media/musiques/. Met à jour les chemins
dans bookplayer.config.yml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:10:42 +01:00
Yvv
922afa2763 BookPlayer affiche les paroles du morceau, plus le contenu chapitre
Le BookPlayer chargeait les .md via Nuxt Content — qui contenaient avant
les paroles par erreur. Maintenant que les .md ont le vrai contenu du
livre, le BookPlayer doit afficher les lyrics depuis bookplayer.config.yml.

Supprime queryCollection('book') du BookPlayer, remplace ContentRenderer
par un rendu HTML des paroles avec tags stylisés.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:06:11 +01:00
Yvv
dd1d8baf4f Fix 404 chapitres : stem Nuxt Content inclut le dossier parent
Le `chapter.stem` de Nuxt Content renvoie `book/01-introduction` et non
`01-introduction`. Extraction du slug final via `.split('/').pop()` dans
les liens et la navigation prev/next.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:46:19 +01:00
Yvv
2f438d9d7a Refactoring complet : contenu livre, config unique, routes, admin et light mode
- Source unique : supprime app/data/librodrome.config.yml, renomme site/ en bookplayer.config.yml
- Morceaux : renommés avec slugs lisibles, fichiers audio renommés, inversion ch2↔ch3 corrigée
- Chapitres : 11 fichiers .md réécrits avec le vrai contenu du livre (synthèse fidèle du PDF)
- Routes : /lire → /modele-eco, /ecouter → /en-musique, redirections 301
- Admin chapitres : champs structurés (titre, description, temps lecture), compteur mots
- Éditeur markdown : mode split, plein écran, support Tab, meilleur rendu aperçu
- Admin morceaux : drag & drop, ajout/suppression, gestion playlist
- Light mode : palettes printemps/été plus saturées et contrastées, teintes primary
- Raccourcis clavier player : espace, flèches gauche/droite
- Paroles : toggle supprimé, toujours visibles et scrollables
- Nouvelles pages : autonomie, evenement

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:20:52 +01:00
Yvv
4fce862df6 Affiche le mot de passe admin sous la saisie en mode dev
Endpoint /api/admin/auth/hint (dev-only, 404 en prod via import.meta.dev).
Le hint est aussi éliminé côté client au build grâce à import.meta.dev.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
f5cf98ce15 Ajout CLAUDE.md : conventions projet et ports dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
acf66513af Fix ports dev : librodrome→3000, grateWizard→3001
Évite les conflits de ports entre projets :
- nuxt.config.ts : devServer.port fixé à 3000
- app.config.ts : URL dev grateWizard corrigée 3009→3001

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
d412975a4c Fix light mode nav invisible, logo § calligraphié, typo ronde fine
- Navigation : btn-ghost, active-class, footer, nav mobile → CSS vars adaptatifs
- Overrides light mode renforcés avec !important (scoped styles compatibles)
- btn-primary garde text-white en light mode (fond coloré)
- Logo : symbole § calligraphié SVG inline remplace lucide-book-open
- Logo text : font-display weight 300 (ronde fine) + text-gradient
- Logo SVG aussi exporté en /public/images/logo-section.svg
- Header/footer : backgrounds et bordures via CSS vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
326ae0ca77 Shadoks visibles : opacités ×3, tailles augmentées, fonds moins noirs
- Opacités Shadoks : 0.1 → 0.25-0.35 (enfin visibles)
- Tailles SVG augmentées (clamp min/max relevés de 20-40%)
- Fix pumper hors écran (right: -15% → 3%)
- Footer pattern : opacités internes ×3
- Fonds palettes dark éclaircis (bg 4% → 7%, surface 9% → 12%)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
ac4aff4985 Refonte UI complète : palettes saisonnières, typo moderne, paroles nettoyées, Shadoks
- Nettoyage paroles : suppression instructions Suno AI, corrections prononciation (11 fichiers)
- 4 palettes saisonnières (Automne/Hiver dark, Printemps/Été light) avec sélecteur
- Typographie modernisée : Outfit (display) + Inter (sans) remplacent Syne + Space Grotesk
- Styles adaptatifs : CSS vars pour couleurs, overrides light mode complets
- Mini-player : bouton Next ajouté, flèche expand plus visible
- BookPlayer : fix scroll mode paginé, croix de fermeture visible
- Illustrations Shadoks inline SVG dans 11 composants/pages
- Suppression soulignés navigation, reset boutons, bordures propres

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
6f422a7369 Fix affichage paroles : white-space pre-line + échappement crochets markdown
Le parseur markdown convertissait [Intro] en <span>Intro</span> (perte des
crochets) et les \n dans les nœuds texte étaient collapsés en espaces HTML.

- Échappe tous les crochets dans les 11 fichiers markdown (\[Intro\] etc.)
- Ajoute white-space: pre-line sur les paragraphes dans BookPlayer et
  ChapterContent pour que les sauts de ligne des paroles soient visibles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
b29fc77c60 Reordonner morceaux selon PDF, paroles dans les pages, sync player↔chapitres
- Reordonne les songs dans le config YAML selon l'ordre du PDF (01→09, 06 en dernier)
- Met à jour les titres avec les noms du PDF et la numérotation correcte
- Remplace le contenu des 11 pages markdown par les paroles des chansons associées
- Ajoute getChapterForSong() dans useBookData pour la recherche inverse
- Ajoute un watcher dans BookPlayer qui navigue au chapitre quand le morceau change
- Flag _skipSongWatch pour éviter les boucles infinies player↔navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
ac2b8040b1 Refonte mini-player flottant, nettoyage GrateWizard, corrections UI
- PlayerPersistent: widget compact pill + panneau extensible, aligné au contenu
- BookPlayer: ajustements scroll mode, suppression bordures boutons
- UnoCSS: ajout border-none au shortcut btn-ghost
- GrateWizard: suppression composants, services et utils obsolètes
- Ajout du PDF source des paroles (media/)
- Mises à jour config et dépendances

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
0e1e704319 Extraire les paroles des chansons du PDF et les intégrer au YAML
8 chansons sur 9 peuplées depuis media/Paroles Chansons.pdf.
chanson-06 (La croissance, une option ?) reste sans paroles (absente du PDF).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
3612cc17f8 Clean up BookPlayer UI: remove button borders, move close into top bar
Remove borders from all buttons (bar, nav) for a minimal ghost style,
remove the viewport border, relocate the close button from the overlay
corner into the reader bar, and tighten spacing around controls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
837b5394fe Add BookPlayer scroll reading mode and floating mini-player widget
Replace paginated-only reading with a toggle between paginated (CSS columns)
and continuous vertical scroll modes. Replace the full-width fixed footer
player bar with a compact floating pill in the bottom-right corner,
expandable to show full controls, visualizer, and playlist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
554602ad52 Pin Bloc 0 and Arrivant juste at top of relations list, sort others alphabetically
- Rename "Newbie" to "Arrivant juste"
- Base friends (Bloc 0, Arrivant juste) always stay at the top
- User-added relations are sorted below them

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
2b5543791f Migrate grateWizard from React/Next.js to native Nuxt integration
- Port all React components to Vue 3 (GwTabs, GwMN, GwCRA, GwCRS,
  GwMap, GwRelations, GwPerimeterList)
- Port hooks to Vue composables (useCesiumProfiles, useSavedPerimeters)
- Copy pure TS services and utils (duniter/, ss58, gratewizard utils)
- Add Leaflet + Geoman + MarkerCluster dependencies
- Serve grateWizard as popup via /gratewizard?popup (layout: false)
  and info page on /gratewizard (with Librodrome layout)
- Remove public/gratewizard-app/ static Next.js export
- Refine UI: compact tabs, buttons, inputs, cards, perimeter list
- Use Ğ1 breve everywhere, French locale for all dates and amounts
- Rename roles: vendeur→offre / acheteur→reçoit le produit ou service
- Rename prix→évaluation in all visible text
- Add calculated result column in CRA and CRS relation tables
- DU/Ğ1 selector uses toggle switch (same as role toggle)
- Auto-scroll to monetary data card on polygon selection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
Yvv
524c7a0fc2 Ouverture directe du BookPlayer en lecture, corrections éditoriales
- Suppression des phases intro (livre 3D) et cover (page intermédiaire)
  du BookPlayer : le reader s'ouvre directement depuis la home
- Corrections textuelles : about.md, app.config.ts, app.vue
- Mise à jour de GrateWizard app

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:05:43 +01:00
234 changed files with 15461 additions and 2782 deletions

7
.gitignore vendored
View File

@@ -18,6 +18,13 @@ logs
.fleet .fleet
.idea .idea
# PDF.js (copié depuis node_modules par postinstall)
public/pdfjs/pdf.min.mjs
public/pdfjs/pdf.worker.min.mjs
# Sources originales (PDF, JPG — pas servies par l'appli)
sources/
# Local env files # Local env files
.env .env
.env.* .env.*

82
CLAUDE.md Normal file
View File

@@ -0,0 +1,82 @@
# Librodrome
Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don.
## Stack
- **Nuxt 4** (Vue 3, TypeScript, Nitro)
- **Modules** : Nuxt Content, Pinia, UnoCSS, VueUse, Nuxt Image
- **Icônes** : Lucide + Phosphor (via @iconify-json)
- **Package manager** : pnpm
- **Déploiement** : Docker + Traefik, CI via Woodpecker
## Structure
```
app/
pages/
numerique/ # Autonomie numérique (index + [slug] détail)
economique/ # Autonomie économique (index, monnaie-libre, commande, productions-collectives)
modele-eco/ # Livre : sommaire + chapitres [slug]
citoyenne/ # Autonomie citoyenne (index + [slug] détail)
en-musique/ # Player audio
evenement.vue # Événement
admin/ # Back-office (pages/, book/, songs, messages, media)
components/
book/Actions.vue # Boutons partagés livre (player, PDF, chapitres, commande)
home/ # BookSection, AxisBlock, AxisGrid, HeroSection, Messages
admin/, player/, song/, ui/
composables/ # useAudioPlayer, useBookData, useGrateWizard, usePageContent...
stores/palette.ts # 4 palettes saisonnières (été par défaut, persisté localStorage)
assets/css/ # main.css (UnoCSS + overrides light mode)
site/
pages/ # Contenu YAML par section (numerique/, economique/, citoyenne/)
site.yml # Config globale (nav, footer, GrateWizard)
bookplayer.config.yml # Config player/chapitres
server/
api/content/pages/[...path].get.ts # GET pages YAML (chemins imbriqués)
api/admin/content/pages/[...path].put.ts # PUT pages YAML
api/admin/content/pages.get.ts # Liste toutes les pages
middleware/redirects.ts # 301 : /gestation, /modele-eco, /decision, /lire
docker/
Dockerfile, docker-compose.yml, docker-compose.dev.yml
```
## Ports dev (CRITIQUE)
| Projet | Port | Config |
|--------|------|--------|
| **librodrome** | **3000** | `nuxt.config.ts``devServer.port: 3000` |
| **GrateWizard** | **3001** | `package.json``next dev --port 3001` |
| **SejeteralO frontend** | **3009** | `frontend/nuxt.config.ts``devServer.port: 3009` |
| **SejeteralO backend** | **8000** | Makefile → `uvicorn --port 8000` |
**Ne jamais changer ces ports.**
## Intégration GrateWizard
- URL dev : `app/app.config.ts``localhost:3001`
- URL prod : `https://gratewizard.axiom-team.fr`
- Bloc GrateWizard dans la section économique de la home
## Contenu administrable
- YAML dans `site/pages/` organisé par section (sous-dossiers)
- API supporte les chemins imbriqués (`numerique/logiciel-libre`)
- Admin : `/admin/pages` liste toutes les pages, `/admin/pages/{path}` édite en YAML
- Git sync auto en prod (ADMIN_GIT_SYNC=true)
## Commandes
```bash
pnpm dev # Dev server sur :3000
pnpm build # Build production
```
## Conventions
- Langue du site : français
- Commits en français, style concis
- CSS via UnoCSS (utility-first) + variables CSS palettes
- Composants Vue SFC avec `<script setup lang="ts">`
- Shadoks SVG inline thématiques sur chaque page (hidden mobile)

101
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,101 @@
# Contributing — Le Librodrome
## Architecture
```
app/
pages/
numerique/ # Section autonomie numérique
index.vue # Vue d'ensemble (3 piliers)
[slug].vue # Détail pilier (contenu YAML)
economique/ # Section autonomie économique
index.vue # Vue d'ensemble (monnaie libre, livre, productions)
monnaie-libre.vue
commande.vue # Commande du livre (Bookelis + librairie)
productions-collectives.vue
modele-eco/
index.vue # Couverture livre + sommaire chapitres
[slug].vue # Lecteur de chapitre (Nuxt Content)
citoyenne/ # Section autonomie citoyenne
index.vue # Vue d'ensemble (decision, tarifs-eau)
[slug].vue # Détail (contenu YAML)
en-musique/ # Player audio
evenement.vue
admin/ # Back-office protégé
pages/
index.vue # Liste toutes les pages YAML
[...path].vue # Éditeur YAML (supporte les sous-dossiers)
components/
book/Actions.vue # Boutons partagés du livre (player, PDF, chapitres, commande)
home/BookSection.vue # Bloc couverture + CTAs (utilise BookActions)
home/AxisBlock.vue # Bloc axe sur la home (supporte actions lien via `to`)
home/AxisGrid.vue # Grille 3 axes + GrateWizard
stores/palette.ts # 4 palettes saisonnières (été par défaut)
```
## Contenu YAML
API : `GET /api/content/pages/{path}` — supporte les chemins imbriqués (`numerique/logiciel-libre`).
Admin : `PUT /api/admin/content/pages/{path}` + `GET /api/admin/content/pages` (liste).
Les fichiers YAML sont dans `site/pages/`, organisés en sous-dossiers par section.
## Palettes
4 palettes dans `stores/palette.ts` : automne, hiver, printemps, été.
- Défaut : **été** (premier visiteur)
- Persisté en `localStorage('palette')` pour les visites suivantes
- CSS : variables `--color-primary`, `--color-accent`, etc. + classe `.palette-light`/`.palette-dark`
- Overrides light mode dans `main.css` (`.palette-light .text-white` → couleur adaptive)
- Admin : tous les `color: white` dans les composants admin sont remplacés par `hsl(var(--color-text))` sauf les boutons sur fond `hsl(var(--color-primary))` (`AdminSaveButton`, `.login-btn`)
## Navigation contextuelle — pages détail
`[slug].vue` (numérique, citoyenne) embarque :
- **Prev/next** (`ctx-nav`) en haut de page — liens vers les pages adjacentes dans la section
- **Sommaire flottant** (`.sommaire-sidebar`, `position: sticky`) dans un aside absolu à droite — visible ≥ 1300px, `overflow: clip` sur le parent pour ne pas casser le sticky
Les ancres sont des `<div :id="\`s${si}\`" class="section-anchor">` avec `scroll-margin-top: 5.5rem` (offset header fixe).
## Types de sections YAML
`[slug].vue` supporte : `arguments`, `fiche`, `equivalents`, `llm`, `atelier`, `links`, `insight`, `tiers`, `projet`, `territoire`.
`type: territoire` : économies alternatives (euro/Ğ1), grille bouquet de services, tableau matériel dépliable (`<details>`).
`type: projet` : carte gestation avec grille de features.
## Shadoks
Illustrations SVG inline sur chaque page, thématiques par section :
- Numérique index : métiers tech/industrie (codeuse, électricien, soudeuse, cryptographe...)
- Numérique détail : postures geek (pinguin+USB, web-of-trust, rubber duck, caféine, rack serveur, debugger loupe)
- Économique : artisanat/agriculture (boulangère, potier, forgeron...)
- Citoyenne : navigation/justice/théâtre (capitaine, avocate, comédien...)
- Modèle éco : livre/édition (typographe, calligraphe, conteuse...)
- Événement : spectacle (funambule, accordéoniste, jongleur...)
Règles : corps compact, longues pattes, grands pieds plats, bec pointu, postures variées.
CSS : `position: absolute`, `pointer-events: none`, `opacity 0.18-0.28`, animation float, `display: none` sur mobile.
Parent avec `overflow: clip` (pas `overflow: hidden` qui casserait `position: sticky`).
## Analytics
`app/composables/useTracking.ts` — wrapper Umami. Activé si `NUXT_PUBLIC_UMAMI_WEBSITE_ID` est défini.
`server/api/stats/index.get.ts` — endpoint public pour la fédération inter-instances (observatoires territoire).
`docker/docker-compose.umami.yml` — stack Umami + PostgreSQL avec labels Traefik.
## Redirections
`server/middleware/redirects.ts` :
- `/gestation/*` → section appropriée
- `/modele-eco/*` → `/economique/modele-eco/*`
- `/decision` → `/citoyenne/decision`
- `/lire/*` → `/economique/modele-eco/*`
- `/ecouter/*` → `/en-musique/*`
## Conventions
- Commits en français, concis
- Build prod vérifié avant push (`PORT=3099 node .output/server/index.mjs`)
- Port dev : 3000 (fixe, jamais de fallback)

124
README.md
View File

@@ -1,75 +1,73 @@
# Nuxt Minimal Starter # Le Librodrome
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don.
## Setup ## Navigation
Make sure to install dependencies: Le site est organisé en 3 grandes sections :
| Section | Route | Contenu |
|---------|-------|---------|
| **Numérique** | `/numerique` | Autonomie numérique : logiciel libre, authentification WoT, cloud libre |
| **Économique** | `/economique` | Création monétaire, monnaie libre, modèle économique (livre + chapitres), productions collectives |
| **Citoyenne** | `/citoyenne` | Décision collective (Glibredecision), tarifs de l'eau (SejeteralO) |
Autres pages : `/en-musique` (player audio), `/evenement`, `/a-propos`, `/messages`.
Chaque section a une page index et des sous-pages de détail (`/numerique/logiciel-libre`, `/economique/modele-eco`, etc.).
## Le livre
- **Lire** : lecteur PDF intégré, chapitres Markdown sous `/economique/modele-eco/[slug]`
- **Écouter** : player audio avec 9 morceaux sous `/en-musique`
- **Commander** : page `/economique/commande` (Bookelis + librairie)
## Stack
- **Nuxt 4** (Vue 3, TypeScript, Nitro)
- **UnoCSS** (utility-first) + palettes saisonnières (été par défaut)
- **Nuxt Content** pour les chapitres du livre
- **Pinia** pour l'état (palette, player)
- **pnpm** comme package manager
## Contenu administrable
Le contenu des pages est dans `site/pages/` en YAML, organisé par section :
```
site/pages/
home.yml # Page d'accueil
numerique.yml # Index numérique
numerique/*.yml # Sous-pages
economique.yml # Index économique
economique/*.yml # Sous-pages (modele-eco, monnaie-libre, commande...)
citoyenne.yml # Index citoyenne
citoyenne/*.yml # Sous-pages (decision, tarifs-eau)
en-musique.yml, evenement.yml, gratewizard.yml
```
Administration via `/admin/pages` (éditeur YAML, authentifié).
Le hero de la home (`home.yml`) supporte deux blocs dépliables :
- `approach` + `axes` : approche par dimension (numérique → code source, etc.)
- `audience` + `addressees` : à qui s'adresse le projet (collectifs, entreprises, collectivités)
## Développement
```bash ```bash
# npm
npm install
# pnpm
pnpm install pnpm install
pnpm dev # Dev server sur :3000
# yarn pnpm build # Build production
yarn install
# bun
bun install
``` ```
## Development Server Port réservé : **3000** (ne pas changer).
Start the development server on `http://localhost:3000`: ## Analytics
```bash Umami self-hosted (optionnel). Configurer `NUXT_PUBLIC_UMAMI_WEBSITE_ID` et `NUXT_PUBLIC_UMAMI_URL` dans l'environnement.
# npm Déploiement séparé : `docker/docker-compose.umami.yml``stats.librodrome.org`.
npm run dev Stats publiques exposées via `/api/stats` pour la fédération inter-instances.
# pnpm ## Déploiement
pnpm dev
# yarn Docker + Traefik, CI via Woodpecker. Domaine : `librodrome.org`.
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@@ -1,29 +1,29 @@
export default defineAppConfig({ export default defineAppConfig({
site: { site: {
name: 'Le Librodrome', name: 'Le Librodrome',
description: 'Une économie du don — enfin concevable. Un livre et 9 chansons, lecture guidée et écoute libre.', description: 'Une économie du don — enfin concevable. Un livre et des chansons, lecture guidée et écoute libre.',
url: 'https://librodrome.org', url: 'https://librodrome.org',
}, },
header: { header: {
height: '4rem', height: '4rem',
nav: [
{ label: 'Accueil', to: '/' },
{ label: 'Lire', to: '/lire' },
{ label: 'Écouter', to: '/ecouter' },
{ label: 'À propos', to: '/a-propos' },
],
}, },
footer: { footer: {
credits: '© 2026 Le Librodrome — Production collective', credits: '© 2026 Le Librodrome — Productions collectives',
links: [ links: [
{ label: 'Mentions légales', to: '/mentions-legales' }, { label: 'Mentions légales', to: '/mentions-legales' },
], ],
}, },
gratewizard: { gratewizard: {
url: '/gratewizard-app/', url: import.meta.dev ? 'http://localhost:3001' : 'https://gratewizard.axiom-team.fr',
popup: { popup: {
width: 420, width: 480,
height: 720, height: 860,
}, },
}, },
libredecision: {
url: import.meta.dev ? 'http://localhost:3002' : 'https://decision.laplank.org',
},
sejeteral0: {
url: import.meta.dev ? 'http://localhost:3009' : 'https://collectivites.librodrome.org',
},
}) })

View File

@@ -14,15 +14,27 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: site } = await useSiteContent() const paletteStore = usePaletteStore()
onMounted(() => paletteStore.applyToDOM())
// Umami analytics — inject script only when configured
const runtimeConfig = useRuntimeConfig()
if (runtimeConfig.public.umamiWebsiteId && runtimeConfig.public.umamiUrl) {
useHead({
script: [{
src: `${runtimeConfig.public.umamiUrl}/script.js`,
defer: true,
'data-website-id': runtimeConfig.public.umamiWebsiteId,
}],
})
}
useHead({ useHead({
titleTemplate: (title) => { titleTemplate: (title) => {
const siteName = site.value?.identity.name ?? 'Le Librodrome' return title ? `${title} — Le Librodrome` : 'Le librodrome'
return title ? `${title}${siteName}` : siteName
}, },
meta: [ meta: [
{ name: 'description', content: site.value?.identity.description ?? '' }, { name: 'description', content: 'Une économie du don — enfin concevable. Un livre et des chansons, lecture guidée et écoute libre.' },
], ],
}) })
</script> </script>

View File

@@ -60,10 +60,10 @@
@keyframes glow-pulse { @keyframes glow-pulse {
0%, 100% { 0%, 100% {
box-shadow: 0 0 8px hsl(12 76% 48% / 0.3); box-shadow: 0 0 8px hsl(var(--color-primary) / 0.3);
} }
50% { 50% {
box-shadow: 0 0 24px hsl(12 76% 48% / 0.6); box-shadow: 0 0 24px hsl(var(--color-primary) / 0.6);
} }
} }

View File

@@ -3,16 +3,16 @@
@import './typography.css'; @import './typography.css';
:root { :root {
--color-primary: 12 76% 48%; --color-primary: 18 80% 45%;
--color-accent: 36 80% 52%; --color-accent: 32 85% 50%;
--color-bg: 20 8% 3.5%; --color-bg: 215 8% 22%;
--color-surface: 20 8% 8%; --color-surface: 213 7% 27%;
--color-surface-light: 20 8% 13%; --color-surface-light: 210 6% 32%;
--color-text: 0 0% 100%; --color-text: 0 0% 100%;
--color-text-muted: 0 0% 100% / 0.6; --color-text-muted: 0 0% 65%;
--header-height: 4rem; --header-height: 4rem;
--player-height: 5rem; --player-height: 0rem;
--sidebar-width: 280px; --sidebar-width: 280px;
--font-display: 'Syne', sans-serif; --font-display: 'Syne', sans-serif;
@@ -43,9 +43,22 @@ body {
min-height: 100dvh; min-height: 100dvh;
} }
button {
border: none;
background: none;
cursor: pointer;
font: inherit;
color: inherit;
}
a {
text-decoration: none;
color: inherit;
}
::selection { ::selection {
background-color: hsl(var(--color-primary) / 0.3); background-color: hsl(var(--color-primary) / 0.3);
color: white; color: hsl(var(--color-text));
} }
:focus-visible { :focus-visible {
@@ -72,6 +85,117 @@ body {
background: hsl(var(--color-text) / 0.25); background: hsl(var(--color-text) / 0.25);
} }
/* ═══ Light mode overrides ═══ */
.palette-light {
color-scheme: light;
}
/* Force all white text → adaptive text color in light mode.
Using !important to override scoped component styles and UnoCSS utilities. */
.palette-light,
.palette-light .text-white {
color: hsl(var(--color-text)) !important;
}
/* white with opacity → dark text with boosted opacity for punch */
.palette-light .text-white\/20 { color: hsl(var(--color-text) / 0.28) !important; }
.palette-light .text-white\/30 { color: hsl(var(--color-text) / 0.38) !important; }
.palette-light .text-white\/40 { color: hsl(var(--color-text) / 0.48) !important; }
.palette-light .text-white\/45 { color: hsl(var(--color-text) / 0.52) !important; }
.palette-light .text-white\/50 { color: hsl(var(--color-text) / 0.58) !important; }
.palette-light .text-white\/60 { color: hsl(var(--color-text) / 0.68) !important; }
.palette-light .text-white\/65 { color: hsl(var(--color-text) / 0.73) !important; }
.palette-light .text-white\/70 { color: hsl(var(--color-text) / 0.78) !important; }
.palette-light .text-white\/55 { color: hsl(var(--color-text) / 0.63) !important; }
.palette-light .text-white\/75 { color: hsl(var(--color-text) / 0.83) !important; }
.palette-light .text-white\/80 { color: hsl(var(--color-text) / 0.88) !important; }
.palette-light .text-white\/85 { color: hsl(var(--color-text) / 0.92) !important; }
.palette-light .text-white\/90 { color: hsl(var(--color-text) / 0.95) !important; }
/* white backgrounds → surface tones with more contrast */
.palette-light .bg-white\/5 { background-color: hsl(var(--color-primary) / 0.05) !important; }
.palette-light .bg-white\/8 { background-color: hsl(var(--color-primary) / 0.07) !important; }
.palette-light .bg-white\/10 { background-color: hsl(var(--color-primary) / 0.09) !important; }
/* borders with primary tint */
.palette-light .border-white\/8 { border-color: hsl(var(--color-primary) / 0.15) !important; }
/* hover overrides */
.palette-light .hover\:text-white:hover,
.palette-light .hover\:text-white\/70:hover,
.palette-light .hover\:text-white\/80:hover {
color: hsl(var(--color-text)) !important;
}
.palette-light .hover\:text-white\/60:hover {
color: hsl(var(--color-text) / 0.7) !important;
}
.palette-light .hover\:bg-white\/5:hover {
background-color: hsl(var(--color-primary) / 0.08) !important;
}
.palette-light .hover\:bg-white\/10:hover {
background-color: hsl(var(--color-primary) / 0.12) !important;
}
/* group-hover overrides */
.palette-light .group:hover .group-hover\:text-primary\/60 {
color: hsl(var(--color-primary) / 0.7) !important;
}
/* placeholder overrides */
.palette-light .placeholder\:text-white\/30::placeholder {
color: hsl(var(--color-text) / 0.35) !important;
}
/* Prose/content in light mode */
.palette-light .prose { color: hsl(var(--color-text)); }
.palette-light .prose :where(h1,h2,h3,h4,h5,h6) { color: hsl(var(--color-text)); }
/* text-gradient — solid primary color everywhere */
/* card surfaces — subtle shadow for depth */
.palette-light .card-surface {
background: hsl(var(--color-surface)) !important;
border-color: hsl(var(--color-primary) / 0.12) !important;
box-shadow: 0 1px 3px hsl(var(--color-text) / 0.06);
}
/* btn-primary text stays white on colored bg */
.palette-light .btn-primary {
color: white !important;
}
/* input fields — cleaner contrast */
.palette-light input,
.palette-light textarea {
color: hsl(var(--color-text));
background-color: white;
border-color: hsl(var(--color-text) / 0.18);
}
.palette-light input:focus,
.palette-light textarea:focus {
border-color: hsl(var(--color-primary) / 0.5);
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.1);
}
/* Ecouter view toggle buttons */
.palette-light .bg-white\/10 {
background-color: hsl(var(--color-primary) / 0.1) !important;
}
/* Light mode scrollbar — tinted with primary */
.palette-light ::-webkit-scrollbar-thumb {
background: hsl(var(--color-primary) / 0.2);
}
.palette-light ::-webkit-scrollbar-thumb:hover {
background: hsl(var(--color-primary) / 0.35);
}
/* Light mode selection — vivid */
.palette-light ::selection {
background-color: hsl(var(--color-accent) / 0.25);
}
/* Page transitions */ /* Page transitions */
.page-enter-active, .page-enter-active,
.page-leave-active { .page-leave-active {

View File

@@ -3,7 +3,7 @@
font-family: var(--font-sans); font-family: var(--font-sans);
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.8; line-height: 1.8;
color: hsl(0 0% 100% / 0.90); color: hsl(var(--color-text) / 0.90);
max-width: 65ch; max-width: 65ch;
} }
@@ -13,11 +13,11 @@
font-weight: 800; font-weight: 800;
line-height: 1.25; line-height: 1.25;
letter-spacing: -0.02em; letter-spacing: -0.02em;
color: white; color: hsl(var(--color-text));
margin-top: 0; margin-top: 0;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
border-bottom: 2px solid hsl(12 76% 48% / 0.4); border-bottom: 2px solid hsl(var(--color-primary) / 0.4);
} }
.prose h2 { .prose h2 {
@@ -26,11 +26,11 @@
font-weight: 700; font-weight: 700;
line-height: 1.3; line-height: 1.3;
letter-spacing: -0.01em; letter-spacing: -0.01em;
color: white; color: hsl(var(--color-text));
margin-top: 3.5rem; margin-top: 3.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
padding-left: 0.75rem; padding-left: 0.75rem;
border-left: 3px solid hsl(12 76% 48% / 0.5); border-left: 3px solid hsl(var(--color-primary) / 0.5);
} }
.prose h3 { .prose h3 {
@@ -38,7 +38,7 @@
font-size: clamp(1.25rem, 3vw, 1.625rem); font-size: clamp(1.25rem, 3vw, 1.625rem);
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
color: hsl(0 0% 100% / 0.92); color: hsl(var(--color-text) / 0.92);
margin-top: 3rem; margin-top: 3rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
@@ -49,7 +49,7 @@
width: 0.5rem; width: 0.5rem;
height: 0.5rem; height: 0.5rem;
border-radius: 50%; border-radius: 50%;
background: hsl(36 80% 52%); background: hsl(var(--color-accent));
margin-right: 0.625rem; margin-right: 0.625rem;
vertical-align: middle; vertical-align: middle;
position: relative; position: relative;
@@ -61,7 +61,7 @@
font-size: clamp(1.065rem, 2.5vw, 1.25rem); font-size: clamp(1.065rem, 2.5vw, 1.25rem);
font-weight: 600; font-weight: 600;
line-height: 1.45; line-height: 1.45;
color: hsl(0 0% 100% / 0.85); color: hsl(var(--color-text) / 0.85);
margin-top: 2.5rem; margin-top: 2.5rem;
margin-bottom: 0.625rem; margin-bottom: 0.625rem;
} }
@@ -69,7 +69,7 @@
.prose h4::before { .prose h4::before {
content: '//'; content: '//';
font-family: var(--font-mono); font-family: var(--font-mono);
color: hsl(36 80% 52%); color: hsl(var(--color-accent));
margin-right: 0.5rem; margin-right: 0.5rem;
font-weight: 500; font-weight: 500;
} }
@@ -78,7 +78,7 @@
.prose h2 + p, .prose h2 + p,
.prose h3 + p { .prose h3 + p {
font-size: 1.175rem; font-size: 1.175rem;
color: hsl(0 0% 100% / 0.75); color: hsl(var(--color-text) / 0.75);
line-height: 1.85; line-height: 1.85;
} }
@@ -88,25 +88,25 @@
} }
.prose a { .prose a {
color: hsl(12 76% 68%); color: hsl(var(--color-primary) / 0.85);
text-decoration: underline; text-decoration: underline;
text-decoration-color: hsl(12 76% 58% / 0.3); text-decoration-color: hsl(var(--color-primary) / 0.3);
text-underline-offset: 3px; text-underline-offset: 3px;
transition: text-decoration-color 0.2s; transition: text-decoration-color 0.2s;
} }
.prose a:hover { .prose a:hover {
text-decoration-color: hsl(12 76% 58%); text-decoration-color: hsl(var(--color-primary));
} }
.prose blockquote { .prose blockquote {
margin: 2rem 0; margin: 2rem 0;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
border-left: 3px solid hsl(12 76% 58%); border-left: 3px solid hsl(var(--color-primary));
background: hsl(240 10% 8%); background: hsl(var(--color-surface));
border-radius: 0 0.5rem 0.5rem 0; border-radius: 0 0.5rem 0.5rem 0;
font-style: italic; font-style: italic;
color: hsl(0 0% 100% / 0.75); color: hsl(var(--color-text) / 0.75);
} }
.prose blockquote p:last-child { .prose blockquote p:last-child {
@@ -116,17 +116,17 @@
.prose code { .prose code {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.875em; font-size: 0.875em;
background: hsl(240 10% 12%); background: hsl(var(--color-surface-light));
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
border-radius: 0.25rem; border-radius: 0.25rem;
color: hsl(31 97% 66%); color: hsl(var(--color-accent));
} }
.prose pre { .prose pre {
margin: 2rem 0; margin: 2rem 0;
padding: 1.5rem; padding: 1.5rem;
background: hsl(240 10% 6%); background: hsl(var(--color-bg));
border: 1px solid hsl(0 0% 100% / 0.08); border: 1px solid hsl(var(--color-text) / 0.08);
border-radius: 0.75rem; border-radius: 0.75rem;
overflow-x: auto; overflow-x: auto;
} }
@@ -134,7 +134,7 @@
.prose pre code { .prose pre code {
background: none; background: none;
padding: 0; padding: 0;
color: hsl(0 0% 100% / 0.87); color: hsl(var(--color-text) / 0.87);
} }
.prose ul, .prose ul,
@@ -149,22 +149,22 @@
} }
.prose li::marker { .prose li::marker {
color: hsl(12 76% 58%); color: hsl(var(--color-primary));
} }
.prose hr { .prose hr {
margin: 3rem 0; margin: 3rem 0;
border: none; border: none;
border-top: 1px solid hsl(0 0% 100% / 0.1); border-top: 1px solid hsl(var(--color-text) / 0.1);
} }
.prose strong { .prose strong {
color: white; color: hsl(var(--color-text));
font-weight: 600; font-weight: 600;
} }
.prose em { .prose em {
color: hsl(0 0% 100% / 0.9); color: hsl(var(--color-text) / 0.9);
} }
.prose img { .prose img {

View File

@@ -58,7 +58,7 @@ function addItem() {
.field-label { .field-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
} }
.list-item { .list-item {
@@ -66,9 +66,9 @@ function addItem() {
align-items: flex-start; align-items: flex-start;
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem; padding: 0.5rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
} }
.list-item > :deep(*:first-child) { .list-item > :deep(*:first-child) {
@@ -101,9 +101,9 @@ function addItem() {
gap: 0.375rem; gap: 0.375rem;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px dashed hsl(20 8% 22%); border: 1px dashed hsl(var(--color-surface-light));
background: none; background: none;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
@@ -111,7 +111,7 @@ function addItem() {
} }
.list-add:hover { .list-add:hover {
border-color: hsl(12 76% 48% / 0.4); border-color: hsl(var(--color-primary) / 0.4);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
</style> </style>

View File

@@ -36,21 +36,21 @@ const id = computed(() => `field-${props.label.toLowerCase().replace(/\s+/g, '-'
.field-label { .field-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
} }
.field-input { .field-input {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.875rem; font-size: 0.875rem;
transition: border-color 0.2s; transition: border-color 0.2s;
} }
.field-input:focus { .field-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -37,15 +37,15 @@ const id = computed(() => `field-${props.label.toLowerCase().replace(/\s+/g, '-'
.field-label { .field-label {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
} }
.field-textarea { .field-textarea {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.875rem; font-size: 0.875rem;
resize: vertical; resize: vertical;
min-height: 5rem; min-height: 5rem;
@@ -55,6 +55,6 @@ const id = computed(() => `field-${props.label.toLowerCase().replace(/\s+/g, '-'
.field-textarea:focus { .field-textarea:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -19,7 +19,7 @@ defineProps<{
<style scoped> <style scoped>
.admin-section { .admin-section {
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: hidden; overflow: hidden;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -32,14 +32,14 @@ defineProps<{
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
color: white; color: hsl(var(--color-text));
cursor: pointer; cursor: pointer;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
user-select: none; user-select: none;
} }
.admin-section-header:hover { .admin-section-header:hover {
background: hsl(20 8% 8%); background: hsl(var(--color-surface));
} }
.admin-section[open] .section-arrow { .admin-section[open] .section-arrow {

View File

@@ -1,35 +1,58 @@
<template> <template>
<div class="md-editor"> <div class="md-editor">
<div class="md-toolbar">
<div class="md-tabs"> <div class="md-tabs">
<button <button
class="md-tab" class="md-tab"
:class="{ 'md-tab--active': tab === 'edit' }" :class="{ 'md-tab--active': tab === 'edit' }"
@click="tab = 'edit'" @click="tab = 'edit'"
> >
<div class="i-lucide-pencil h-3.5 w-3.5" />
Édition Édition
</button> </button>
<button
class="md-tab"
:class="{ 'md-tab--active': tab === 'split' }"
@click="tab = 'split'"
>
<div class="i-lucide-columns-2 h-3.5 w-3.5" />
Split
</button>
<button <button
class="md-tab" class="md-tab"
:class="{ 'md-tab--active': tab === 'preview' }" :class="{ 'md-tab--active': tab === 'preview' }"
@click="tab = 'preview'" @click="tab = 'preview'"
> >
<div class="i-lucide-eye h-3.5 w-3.5" />
Aperçu Aperçu
</button> </button>
</div> </div>
<button
class="md-fullscreen"
:class="{ 'md-fullscreen--active': fullscreen }"
@click="fullscreen = !fullscreen"
>
<div :class="fullscreen ? 'i-lucide-minimize-2' : 'i-lucide-maximize-2'" class="h-3.5 w-3.5" />
</button>
</div>
<div class="md-body" :class="{ 'md-body--split': tab === 'split', 'md-body--fullscreen': fullscreen }">
<textarea <textarea
v-if="tab === 'edit'" v-if="tab === 'edit' || tab === 'split'"
ref="textareaRef"
:value="modelValue" :value="modelValue"
class="md-textarea" class="md-textarea"
:rows="rows" :rows="rows"
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)" @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
@keydown.tab.prevent="insertTab"
/> />
<div <div
v-else v-if="tab === 'preview' || tab === 'split'"
class="md-preview prose" class="md-preview prose"
v-html="renderedHtml" v-html="renderedHtml"
/> />
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -38,64 +61,123 @@ const props = defineProps<{
rows?: number rows?: number
}>() }>()
defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: string] 'update:modelValue': [value: string]
}>() }>()
const tab = ref<'edit' | 'preview'>('edit') const tab = ref<'edit' | 'preview' | 'split'>('split')
const fullscreen = ref(false)
const textareaRef = ref<HTMLTextAreaElement>()
function insertTab(e: KeyboardEvent) {
const ta = e.target as HTMLTextAreaElement
const start = ta.selectionStart
const end = ta.selectionEnd
const val = ta.value
const newVal = val.substring(0, start) + ' ' + val.substring(end)
emit('update:modelValue', newVal)
nextTick(() => {
ta.selectionStart = ta.selectionEnd = start + 2
})
}
const renderedHtml = computed(() => { const renderedHtml = computed(() => {
// Simple markdown rendering for preview
return props.modelValue return props.modelValue
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>') .replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>') .replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>') .replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>') .replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/\n\n/g, '</p><p>') .replace(/\n\n/g, '</p><p>')
.replace(/^(?!<[hp])(.+)/gm, '<p>$1</p>') .replace(/^(?!<[hpulob])(.+)/gm, '<p>$1</p>')
}) })
</script> </script>
<style scoped> <style scoped>
.md-editor { .md-editor {
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.md-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
background: hsl(var(--color-bg));
border-bottom: 1px solid hsl(var(--color-surface-light));
}
.md-tabs { .md-tabs {
display: flex; display: flex;
background: hsl(20 8% 6%);
border-bottom: 1px solid hsl(20 8% 14%);
} }
.md-tab { .md-tab {
padding: 0.5rem 1rem; display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: none; border: none;
background: none; background: none;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.md-tab--active { .md-tab--active {
color: white; color: hsl(var(--color-text));
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
}
.md-fullscreen {
padding: 0.5rem 0.75rem;
color: hsl(var(--color-text-muted));
transition: color 0.2s;
}
.md-fullscreen:hover,
.md-fullscreen--active { color: hsl(var(--color-text)); }
.md-body {
display: flex;
}
.md-body--split .md-textarea,
.md-body--split .md-preview {
width: 50%;
}
.md-body--split .md-preview {
border-left: 1px solid hsl(var(--color-surface-light));
}
.md-body--fullscreen {
position: fixed;
inset: 0;
z-index: 50;
background: hsl(var(--color-bg));
}
.md-body--fullscreen .md-textarea,
.md-body--fullscreen .md-preview {
height: 100vh;
} }
.md-textarea { .md-textarea {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
border: none; border: none;
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.7; line-height: 1.7;
resize: vertical; resize: vertical;
min-height: 20rem; min-height: 24rem;
tab-size: 2;
} }
.md-textarea:focus { .md-textarea:focus {
@@ -104,7 +186,9 @@ const renderedHtml = computed(() => {
.md-preview { .md-preview {
padding: 1rem; padding: 1rem;
min-height: 20rem; min-height: 24rem;
background: hsl(20 8% 4%); max-height: 70vh;
overflow-y: auto;
background: hsl(var(--color-bg));
} }
</style> </style>

View File

@@ -102,18 +102,18 @@ function formatSize(bytes: number): string {
.filter-btn { .filter-btn {
padding: 0.25rem 0.625rem; padding: 0.25rem 0.625rem;
border-radius: 9999px; border-radius: 9999px;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: none; background: none;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
font-size: 0.75rem; font-size: 0.75rem;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.filter-btn--active { .filter-btn--active {
background: hsl(12 76% 48% / 0.15); background: hsl(var(--color-primary) / 0.15);
border-color: hsl(12 76% 48% / 0.3); border-color: hsl(var(--color-primary) / 0.3);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.media-grid { .media-grid {
@@ -123,7 +123,7 @@ function formatSize(bytes: number): string {
} }
.media-card { .media-card {
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
@@ -131,18 +131,18 @@ function formatSize(bytes: number): string {
} }
.media-card:hover { .media-card:hover {
border-color: hsl(20 8% 22%); border-color: hsl(var(--color-surface-light));
} }
.media-card--selected { .media-card--selected {
border-color: hsl(12 76% 48%); border-color: hsl(var(--color-primary));
box-shadow: 0 0 0 1px hsl(12 76% 48% / 0.3); box-shadow: 0 0 0 1px hsl(var(--color-primary) / 0.3);
} }
.media-thumb { .media-thumb {
aspect-ratio: 1; aspect-ratio: 1;
overflow: hidden; overflow: hidden;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
} }
.media-thumb img { .media-thumb img {
@@ -156,8 +156,8 @@ function formatSize(bytes: number): string {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
} }
.media-info { .media-info {
@@ -169,7 +169,7 @@ function formatSize(bytes: number): string {
.media-name { .media-name {
font-size: 0.72rem; font-size: 0.72rem;
color: white; color: hsl(var(--color-text));
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -177,7 +177,7 @@ function formatSize(bytes: number): string {
.media-size { .media-size {
font-size: 0.65rem; font-size: 0.65rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
} }
.media-actions { .media-actions {
@@ -186,9 +186,9 @@ function formatSize(bytes: number): string {
justify-content: space-between; justify-content: space-between;
margin-top: 1rem; margin-top: 1rem;
padding: 0.75rem; padding: 0.75rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
} }
.delete-btn { .delete-btn {

View File

@@ -73,7 +73,7 @@ async function upload(files: FileList) {
<style scoped> <style scoped>
.upload-zone { .upload-zone {
border: 2px dashed hsl(20 8% 22%); border: 2px dashed hsl(var(--color-surface-light));
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@@ -82,12 +82,12 @@ async function upload(files: FileList) {
} }
.upload-zone:hover { .upload-zone:hover {
border-color: hsl(12 76% 48% / 0.4); border-color: hsl(var(--color-primary) / 0.4);
} }
.upload-zone--active { .upload-zone--active {
border-color: hsl(12 76% 48%); border-color: hsl(var(--color-primary));
background: hsl(12 76% 48% / 0.05); background: hsl(var(--color-primary) / 0.05);
} }
.upload-progress { .upload-progress {
@@ -95,7 +95,7 @@ async function upload(files: FileList) {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
font-size: 0.85rem; font-size: 0.85rem;
} }

View File

@@ -31,7 +31,7 @@ defineEmits<{
padding: 0.625rem 1.25rem; padding: 0.625rem 1.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background: hsl(12 76% 48%); background: hsl(var(--color-primary));
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 0.85rem; font-size: 0.85rem;
@@ -40,7 +40,7 @@ defineEmits<{
} }
.save-btn:hover:not(:disabled) { .save-btn:hover:not(:disabled) {
background: hsl(12 76% 42%); background: hsl(var(--color-primary));
} }
.save-btn:disabled { .save-btn:disabled {

View File

@@ -23,21 +23,33 @@
</NuxtLink> </NuxtLink>
<p class="sidebar-section">Pages</p> <p class="sidebar-section">Pages</p>
<NuxtLink to="/admin/pages" class="sidebar-link" exact-active-class="sidebar-link--active">
<div class="i-lucide-file-text h-4 w-4" />
Toutes les pages
</NuxtLink>
<NuxtLink to="/admin/pages/home" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/home" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-home h-4 w-4" /> <div class="i-lucide-home h-4 w-4" />
Accueil Accueil
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/lire" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/numerique" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-book-open h-4 w-4" /> <div class="i-lucide-monitor h-4 w-4" />
Lire Numérique
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/ecouter" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/economique" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-coins h-4 w-4" />
Économique
</NuxtLink>
<NuxtLink to="/admin/pages/citoyenne" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-landmark h-4 w-4" />
Citoyenne
</NuxtLink>
<NuxtLink to="/admin/pages/en-musique" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-headphones h-4 w-4" /> <div class="i-lucide-headphones h-4 w-4" />
Écouter En musique
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/gratewizard" class="sidebar-link" active-class="sidebar-link--active"> <NuxtLink to="/admin/pages/gratewizard" class="sidebar-link" active-class="sidebar-link--active">
<div class="i-lucide-sparkles h-4 w-4" /> <div class="i-lucide-sparkles h-4 w-4" />
GrateWizard grateWizard
</NuxtLink> </NuxtLink>
<p class="sidebar-section">Livre</p> <p class="sidebar-section">Livre</p>
@@ -79,8 +91,8 @@ async function logout() {
<style scoped> <style scoped>
.admin-sidebar { .admin-sidebar {
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
border-right: 1px solid hsl(20 8% 14%); border-right: 1px solid hsl(var(--color-surface-light));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100dvh; height: 100dvh;
@@ -90,7 +102,7 @@ async function logout() {
.sidebar-header { .sidebar-header {
padding: 1.25rem 1rem; padding: 1.25rem 1rem;
border-bottom: 1px solid hsl(20 8% 14%); border-bottom: 1px solid hsl(var(--color-surface-light));
} }
.sidebar-nav { .sidebar-nav {
@@ -104,7 +116,7 @@ async function logout() {
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
padding: 1rem 0.75rem 0.375rem; padding: 1rem 0.75rem 0.375rem;
margin: 0; margin: 0;
} }
@@ -116,7 +128,7 @@ async function logout() {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
text-decoration: none; text-decoration: none;
transition: all 0.2s; transition: all 0.2s;
border: none; border: none;
@@ -126,18 +138,18 @@ async function logout() {
} }
.sidebar-link:hover { .sidebar-link:hover {
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
color: white; color: hsl(var(--color-text));
} }
.sidebar-link--active { .sidebar-link--active {
background: hsl(12 76% 48% / 0.12); background: hsl(var(--color-primary) / 0.12);
color: hsl(12 76% 68%); color: hsl(var(--color-primary));
} }
.sidebar-footer { .sidebar-footer {
padding: 0.5rem; padding: 0.5rem;
border-top: 1px solid hsl(20 8% 14%); border-top: 1px solid hsl(var(--color-surface-light));
} }
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -0,0 +1,33 @@
<template>
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:gap-4">
<UiBaseButton @click="$emit('open-player')">
<div class="i-lucide-play mr-2 h-5 w-5" />
Présentation musicale
</UiBaseButton>
<UiBaseButton variant="accent" @click="$emit('open-pdf')">
<div class="i-lucide-book-open mr-2 h-5 w-5" />
Lire le livre
</UiBaseButton>
<UiBaseButton v-if="showChapters" variant="ghost" to="/economique/modele-eco">
<div class="i-lucide-list mr-2 h-5 w-5" />
Présentation des chapitres
</UiBaseButton>
<UiBaseButton variant="ghost" to="/economique/commande">
<div class="i-lucide-shopping-bag mr-2 h-5 w-5" />
Commandez le livre
</UiBaseButton>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
showChapters?: boolean
}>(), {
showChapters: true,
})
defineEmits<{
'open-player': []
'open-pdf': []
}>()
</script>

View File

@@ -22,6 +22,7 @@
<!-- PDF embed --> <!-- PDF embed -->
<div class="pdf-viewport"> <div class="pdf-viewport">
<iframe <iframe
:key="pdfUrl"
:src="pdfUrl" :src="pdfUrl"
class="pdf-frame" class="pdf-frame"
:title="bpContent?.pdf.iframeTitle" :title="bpContent?.pdf.iframeTitle"
@@ -33,10 +34,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean; page?: number }>()
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>() const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
const { data: bpContent } = await usePageContent('book-player') const { data: bpContent } = await usePageContent('book-player')
const bookData = useBookData()
await bookData.init()
const overlayRef = ref<HTMLElement>() const overlayRef = ref<HTMLElement>()
@@ -45,7 +48,12 @@ const isOpen = computed({
set: (v) => emit('update:modelValue', v), set: (v) => emit('update:modelValue', v),
}) })
const pdfUrl = '/pdf/une-economie-du-don.pdf' const pdfUrl = computed(() => {
const base = bookData.getPdfUrl()
const viewerBase = `/pdfjs/viewer.html?file=${encodeURIComponent(base)}`
if (props.page) return `${viewerBase}#page=${props.page}`
return viewerBase
})
function close() { function close() {
isOpen.value = false isOpen.value = false

View File

@@ -23,45 +23,8 @@
aria-hidden="true" aria-hidden="true"
/> />
<!-- Close --> <!-- READER -->
<button class="bp-close" @click="close" aria-label="Fermer"> <div class="bp-phase bp-reader">
<div class="i-lucide-x h-5 w-5" />
</button>
<!-- PHASE TRANSITIONS -->
<Transition name="phase" mode="out-in">
<!-- INTRO: 3D spinning book -->
<div v-if="phase === 'intro'" key="intro" class="bp-phase bp-intro">
<div class="spin-scene">
<div class="spin-book" @animationend="onSpinEnd">
<div class="spin-face spin-front">
<img src="/images/book-cover-spread.jpg" alt="Couverture" />
</div>
<div class="spin-face spin-back">
<img src="/images/book-cover-spread.jpg" alt="" />
</div>
</div>
</div>
</div>
<!-- COVER: title + CTA -->
<div v-else-if="phase === 'cover'" key="cover" class="bp-phase bp-cover">
<div class="cover-frame">
<img src="/images/book-cover-spread.jpg" :alt="bpContent?.cover.coverAlt ?? 'Couverture'" class="cover-img" />
</div>
<h1 class="cover-title text-gradient">{{ bpContent?.cover.title }}</h1>
<p class="cover-sub">{{ bpContent?.cover.subtitle }}</p>
<p class="cover-desc">
{{ bpContent?.cover.description }}
</p>
<button class="cover-cta" @click="startReading">
{{ bpContent?.cover.cta }}
<div class="i-lucide-arrow-right ml-2 h-5 w-5 inline-block align-middle" />
</button>
</div>
<!-- READING: paginated book reader -->
<div v-else key="reading" class="bp-phase bp-reader">
<!-- Top bar --> <!-- Top bar -->
<div class="reader-bar"> <div class="reader-bar">
<button <button
@@ -72,12 +35,13 @@
<div class="i-lucide-list h-5 w-5" /> <div class="i-lucide-list h-5 w-5" />
</button> </button>
<div class="reader-bar-title"> <div class="reader-bar-title">
<span class="reader-bar-num">{{ chapterIdx + 1 }}.</span> <span class="reader-bar-num">{{ trackIdx + 1 }}.</span>
{{ chapters[chapterIdx].title }} {{ currentTrack?.title ?? '' }}
</div> </div>
<span class="reader-bar-pages"> <span class="reader-bar-pages">{{ scrollPercent }}%</span>
{{ currentPage + 1 }}<span class="op-40">/</span>{{ totalPages }} <button class="reader-bar-btn reader-bar-close" @click="close" aria-label="Fermer">
</span> <div class="i-lucide-x h-5 w-5" />
</button>
</div> </div>
<!-- Sommaire sidebar --> <!-- Sommaire sidebar -->
@@ -86,73 +50,85 @@
<div class="sommaire-panel"> <div class="sommaire-panel">
<h4 class="sommaire-title">{{ bpContent?.reader.sommaireTitle ?? 'Sommaire' }}</h4> <h4 class="sommaire-title">{{ bpContent?.reader.sommaireTitle ?? 'Sommaire' }}</h4>
<button <button
v-for="(ch, i) in chapters" v-for="(track, i) in tracks"
:key="i" :key="track.id"
class="sommaire-item" class="sommaire-item"
:class="{ 'sommaire-item--active': chapterIdx === i }" :class="{ 'sommaire-item--active': trackIdx === i }"
@click="goToChapter(i)" @click="goToTrack(i)"
> >
<span class="sommaire-num">{{ i + 1 }}</span> <span class="sommaire-num">{{ i + 1 }}</span>
{{ ch.title }} {{ track.title }}
</button> </button>
</div> </div>
</aside> </aside>
</Transition> </Transition>
<!-- Content viewport --> <!-- Content viewport -->
<div class="reader-viewport" ref="viewportEl"> <div
class="reader-viewport"
:class="{ 'reader-viewport--scroll': isScrollMode }"
ref="viewportEl"
@scroll="onViewportScroll"
>
<div <div
class="reader-columns prose" class="reader-columns prose"
:class="{ 'reader-columns--scroll': isScrollMode }"
ref="contentEl" ref="contentEl"
:style="contentStyle" :style="contentStyle"
> >
<ContentRenderer v-if="activeChapter" :value="activeChapter" /> <div v-if="currentLyrics" class="lyrics-content" v-html="currentLyricsHtml" />
<div v-else-if="currentSong" class="lyrics-empty">
<p class="op-40 italic">Paroles à venir pour « {{ currentSong.title }} »</p>
</div> </div>
<!-- Page turn shadow overlay --> <div v-else class="lyrics-empty">
<div class="reader-shadow" :class="{ visible: isTurning }" /> <p class="op-40 italic">Aucun morceau sélectionné</p>
</div>
</div>
<!-- Page turn shadow overlay (paginated only) -->
<div v-if="!isScrollMode" class="reader-shadow" :class="{ visible: isTurning }" />
</div> </div>
<!-- Bottom navigation --> <!-- Bottom navigation -->
<div class="reader-nav"> <div class="reader-nav">
<button <button
class="reader-nav-btn" class="reader-nav-btn"
:class="{ 'reader-nav-btn--hidden': currentPage <= 0 && chapterIdx <= 0 }" :class="{ 'reader-nav-btn--hidden': isScrollMode ? trackIdx <= 0 : (currentPage <= 0 && trackIdx <= 0) }"
@click="prevPage" @click="isScrollMode ? prevTrack() : prevPage()"
aria-label="Page précédente" :aria-label="isScrollMode ? 'Morceau précédent' : 'Page précédente'"
> >
<div class="i-lucide-chevron-left h-5 w-5" /> <div class="i-lucide-chevron-left h-5 w-5" />
</button> </button>
<!-- Song disc (if chapter has a song) --> <!-- Song disc -->
<div v-if="chapterSong" class="reader-song"> <div v-if="currentSong" class="reader-song">
<div class="reader-disc" :class="{ spinning: playerStore.isPlaying }"> <div class="reader-disc" :class="{ spinning: playerStore.isPlaying }">
<img src="/images/book-cover-spread.jpg" alt="" class="reader-disc-img" /> <img src="/images/book-cover-spread.jpg" alt="" class="reader-disc-img" />
<div class="reader-disc-hole" /> <div class="reader-disc-hole" />
</div> </div>
<span class="reader-song-name">{{ chapterSong.title }}</span> <span class="reader-song-name">{{ currentSong.title }}</span>
</div> </div>
<div v-else class="reader-song" /> <div v-else class="reader-song" />
<button <button
class="reader-nav-btn" class="reader-nav-btn"
:class="{ 'reader-nav-btn--hidden': currentPage >= totalPages - 1 && chapterIdx >= chapters.length - 1 }" :class="{ 'reader-nav-btn--hidden': isScrollMode ? trackIdx >= tracks.length - 1 : (currentPage >= totalPages - 1 && trackIdx >= tracks.length - 1) }"
@click="nextPage" @click="isScrollMode ? nextTrack() : nextPage()"
aria-label="Page suivante" :aria-label="isScrollMode ? 'Morceau suivant' : 'Page suivante'"
> >
<div class="i-lucide-chevron-right h-5 w-5" /> <div class="i-lucide-chevron-right h-5 w-5" />
</button> </button>
</div> </div>
</div> </div>
</Transition>
<!-- Hint --> <!-- Hint -->
<p class="bp-hint"> <p class="bp-hint">
<template v-if="phase === 'reading'"> <template v-if="isScrollMode">
<span class="hidden md:inline">{{ bpContent?.reader.hints.desktop }}</span> <span class="hidden md:inline">{{ bpContent?.reader.hints.desktopScroll ?? '← → morceaux · Défilement libre · Esc fermer' }}</span>
<span class="md:hidden">{{ bpContent?.reader.hints.mobile }}</span> <span class="md:hidden">{{ bpContent?.reader.hints.mobileScroll ?? 'Défilez pour lire' }}</span>
</template> </template>
<template v-else> <template v-else>
<span class="hidden md:inline">{{ bpContent?.reader.hints.default }}</span> <span class="hidden md:inline">{{ bpContent?.reader.hints.desktop }}</span>
<span class="md:hidden">{{ bpContent?.reader.hints.mobile }}</span>
</template> </template>
</p> </p>
</div> </div>
@@ -177,90 +153,97 @@ const overlayRef = ref<HTMLElement>()
const viewportEl = ref<HTMLElement>() const viewportEl = ref<HTMLElement>()
const contentEl = ref<HTMLElement>() const contentEl = ref<HTMLElement>()
// ── Phase state ── const trackIdx = ref(0)
const phase = ref<'intro' | 'cover' | 'reading'>('intro')
const chapterIdx = ref(0)
const currentPage = ref(0) const currentPage = ref(0)
const totalPages = ref(1) const totalPages = ref(1)
const colWidth = ref(500) const colWidth = ref(500)
const showSommaire = ref(false) const showSommaire = ref(false)
const isTurning = ref(false) const isTurning = ref(false)
const { init: initBookData, getSongs, getPrimarySong, getPlaylistOrder } = useBookData() // ── Reading mode ──
const readingMode = ref<'paginated' | 'scroll'>('scroll')
const isScrollMode = computed(() => readingMode.value === 'scroll')
const scrollPercent = ref(0)
// When switching back to paginated, recalc pages
watch(readingMode, async (mode) => {
if (mode === 'paginated') {
await nextTick()
await nextTick()
setTimeout(recalcPages, 100)
}
})
function onViewportScroll() {
if (!isScrollMode.value || !viewportEl.value) return
const el = viewportEl.value
const max = el.scrollHeight - el.clientHeight
scrollPercent.value = max > 0 ? Math.round((el.scrollTop / max) * 100) : 0
}
const { init: initBookData, getPlaylistOrder } = useBookData()
const audioPlayer = useAudioPlayer() const audioPlayer = useAudioPlayer()
const playerStore = usePlayerStore() const playerStore = usePlayerStore()
// ── Content from Nuxt Content ── // ── Tracks: built from playlist order (songs), not chapters ──
const chaptersContent = ref<any[]>([]) const tracks = computed(() => {
const contentLoaded = ref(false) return playerStore.playlist.map(song => ({
id: song.id,
async function loadContent() { title: song.title,
if (contentLoaded.value) return song,
try { }))
const data = await queryCollection('book').order('order', 'ASC').all()
chaptersContent.value = data
contentLoaded.value = true
}
catch (err) {
console.error('Failed to load book content:', err)
}
}
const activeChapter = computed(() => {
if (chapterIdx.value < 0 || !chaptersContent.value.length) return null
return chaptersContent.value[chapterIdx.value] ?? null
}) })
// ── Chapter metadata ── const currentTrack = computed(() => tracks.value[trackIdx.value] ?? null)
const chapters = [ const currentSong = computed(() => currentTrack.value?.song ?? null)
{ slug: 'introduction', title: 'Introduction' },
{ slug: 'de-quel-don-parlons-nous', title: 'De quel don parlons-nous ?' },
{ slug: 'la-mesure-du-don', title: 'La mesure du don' },
{ slug: 'raison-d-etre-d-une-monnaie', title: 'Raison d\'être d\'une monnaie' },
{ slug: 'la-trm', title: 'La TRM' },
{ slug: 'creer-une-economie', title: 'Créer une économie ?' },
{ slug: 'echanger', title: 'Échanger' },
{ slug: 'relation-institutionnelle', title: 'Relation institutionnelle' },
{ slug: 'autres-greffes', title: 'Autres greffes' },
{ slug: 'et-maintenant', title: 'Et maintenant ?… action ?' },
{ slug: 'annexes', title: 'Chapitres annexes' },
]
// ── Per-chapter color hues ── const currentLyrics = computed(() => {
const chapterHues: [number, number][] = [ return currentSong.value?.lyrics?.trim() || ''
[12, 36], // cover (intro + cover phases) })
[15, 35], // 1
[350, 15], // 2 const currentLyricsHtml = computed(() => {
[36, 50], // 3 if (!currentLyrics.value) return ''
[170, 200], // 4 return currentLyrics.value
[220, 250], // 5 .replace(/&/g, '&amp;')
[270, 300], // 6 .replace(/</g, '&lt;')
[320, 345], // 7 .replace(/>/g, '&gt;')
[150, 170], // 8 .replace(/\[([^\]]+)\]/g, '<span class="lyrics-tag">[$1]</span>')
[190, 220], // 9 .replace(/\n\n/g, '</p><p>')
[40, 20], // 10 .replace(/\n/g, '<br>')
[210, 240], // 11 .replace(/^/, '<p>')
.replace(/$/, '</p>')
})
// ── Per-track color hues (9 tracks) ──
const trackHues: [number, number][] = [
[15, 35], // 1 Ce livre est une façon
[350, 15], // 2 De quel don nous parlons
[36, 50], // 3 Les asymétries
[170, 200], // 4 Inverser les flux
[220, 250], // 5 Ainsi soit-il
[270, 300], // 6 La croissance
[320, 345], // 7 Monnaie libre
[150, 170], // 8 Créer une économie
[190, 220], // 9 Coder la liberté
] ]
const sceneVars = computed(() => { const sceneVars = computed(() => {
const idx = phase.value === 'reading' ? chapterIdx.value + 1 : 0 const [h1, h2] = trackHues[trackIdx.value] ?? trackHues[0]
const [h1, h2] = chapterHues[idx] ?? chapterHues[0]
return { '--scene-h1': h1, '--scene-h2': h2 } as Record<string, number> return { '--scene-h1': h1, '--scene-h2': h2 } as Record<string, number>
}) })
const chapterSong = computed(() => {
if (phase.value !== 'reading') return null
return getPrimarySong(chapters[chapterIdx.value].slug)
})
// ── CSS columns pagination ── // ── CSS columns pagination ──
const contentStyle = computed(() => ({ const contentStyle = computed(() => {
if (isScrollMode.value) return {}
return {
columnWidth: colWidth.value + 'px', columnWidth: colWidth.value + 'px',
columnGap: COL_GAP + 'px', columnGap: COL_GAP + 'px',
transform: `translateX(-${currentPage.value * (colWidth.value + COL_GAP)}px)`, transform: `translateX(-${currentPage.value * (colWidth.value + COL_GAP)}px)`,
})) }
})
function recalcPages() { function recalcPages() {
if (isScrollMode.value) return
if (!contentEl.value || !viewportEl.value) return if (!contentEl.value || !viewportEl.value) return
colWidth.value = viewportEl.value.offsetWidth colWidth.value = viewportEl.value.offsetWidth
const sw = contentEl.value.scrollWidth const sw = contentEl.value.scrollWidth
@@ -269,25 +252,17 @@ function recalcPages() {
let resizeObs: ResizeObserver | null = null let resizeObs: ResizeObserver | null = null
// Recalc when chapter content changes // Recalc when track changes
watch(activeChapter, async () => { watch(trackIdx, async () => {
currentPage.value = 0 currentPage.value = 0
// Wait for ContentRenderer to update DOM
await nextTick() await nextTick()
await nextTick() await nextTick()
setTimeout(recalcPages, 100) setTimeout(recalcPages, 100)
}) })
// ── Phase transitions ── async function initReading() {
function onSpinEnd() { trackIdx.value = 0
phase.value = 'cover'
}
async function startReading() {
await loadContent()
chapterIdx.value = 0
currentPage.value = 0 currentPage.value = 0
phase.value = 'reading'
await nextTick() await nextTick()
await nextTick() await nextTick()
// Set up ResizeObserver // Set up ResizeObserver
@@ -298,14 +273,36 @@ async function startReading() {
setTimeout(recalcPages, 200) setTimeout(recalcPages, 200)
} }
// ── Navigation ── // ── Navigation by tracks (songs) ──
function goToChapter(idx: number) { let _skipSongWatch = false
chapterIdx.value = idx
function goToTrack(idx: number) {
if (idx < 0 || idx >= tracks.value.length) return
trackIdx.value = idx
currentPage.value = 0 currentPage.value = 0
showSommaire.value = false showSommaire.value = false
// Play chapter song // Scroll to top in scroll mode
const song = getPrimarySong(chapters[idx].slug) if (isScrollMode.value && viewportEl.value) {
if (song) audioPlayer.loadAndPlay(song) viewportEl.value.scrollTop = 0
}
// Play the song
const song = tracks.value[idx]?.song
if (song) {
_skipSongWatch = true
audioPlayer.loadAndPlay(song)
}
}
function nextTrack() {
if (trackIdx.value < tracks.value.length - 1) {
goToTrack(trackIdx.value + 1)
}
}
function prevTrack() {
if (trackIdx.value > 0) {
goToTrack(trackIdx.value - 1)
}
} }
function nextPage() { function nextPage() {
@@ -313,9 +310,8 @@ function nextPage() {
triggerTurn() triggerTurn()
currentPage.value++ currentPage.value++
} }
else if (chapterIdx.value < chapters.length - 1) { else if (trackIdx.value < tracks.value.length - 1) {
// Next chapter goToTrack(trackIdx.value + 1)
goToChapter(chapterIdx.value + 1)
} }
} }
@@ -324,22 +320,8 @@ function prevPage() {
triggerTurn() triggerTurn()
currentPage.value-- currentPage.value--
} }
else if (chapterIdx.value > 0) { else if (trackIdx.value > 0) {
// Previous chapter, go to last page goToTrack(trackIdx.value - 1)
chapterIdx.value--
currentPage.value = 0
showSommaire.value = false
const song = getPrimarySong(chapters[chapterIdx.value].slug)
if (song) audioPlayer.loadAndPlay(song)
// After content loads, go to last page
watch(activeChapter, async () => {
await nextTick()
await nextTick()
setTimeout(() => {
recalcPages()
currentPage.value = Math.max(0, totalPages.value - 1)
}, 150)
}, { once: true })
} }
} }
@@ -353,15 +335,26 @@ function close() {
} }
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (phase.value === 'reading') { // CRITICAL: stop propagation so useKeyboardShortcuts doesn't also fire
e.stopPropagation()
if (e.key === 'Escape') { close(); return }
if (e.key === ' ') {
e.preventDefault()
audioPlayer.togglePlayPause()
return
}
if (isScrollMode.value) {
if (e.key === 'ArrowRight') { e.preventDefault(); nextTrack() }
else if (e.key === 'ArrowLeft') { e.preventDefault(); prevTrack() }
}
else {
if (e.key === 'ArrowRight') { e.preventDefault(); nextPage() } if (e.key === 'ArrowRight') { e.preventDefault(); nextPage() }
else if (e.key === 'ArrowLeft') { e.preventDefault(); prevPage() } else if (e.key === 'ArrowLeft') { e.preventDefault(); prevPage() }
else if (e.key === 'ArrowDown') { e.preventDefault(); if (chapterIdx.value < chapters.length - 1) goToChapter(chapterIdx.value + 1) } else if (e.key === 'ArrowDown') { e.preventDefault(); nextTrack() }
else if (e.key === 'ArrowUp') { e.preventDefault(); if (chapterIdx.value > 0) goToChapter(chapterIdx.value - 1) } else if (e.key === 'ArrowUp') { e.preventDefault(); prevTrack() }
else if (e.key === 'Escape') close()
}
else if (e.key === 'Escape') {
close()
} }
} }
@@ -373,6 +366,7 @@ function onTouchStart(e: TouchEvent) {
} }
function onTouchEnd(e: TouchEvent) { function onTouchEnd(e: TouchEvent) {
if (isScrollMode.value) return
const diff = touchStartX - (e.changedTouches[0]?.screenX ?? 0) const diff = touchStartX - (e.changedTouches[0]?.screenX ?? 0)
if (Math.abs(diff) > 50) { if (Math.abs(diff) > 50) {
if (diff > 0) nextPage() if (diff > 0) nextPage()
@@ -383,11 +377,7 @@ function onTouchEnd(e: TouchEvent) {
// ── Lifecycle ── // ── Lifecycle ──
watch(isOpen, async (open) => { watch(isOpen, async (open) => {
if (open) { if (open) {
phase.value = 'intro'
chapterIdx.value = 0
currentPage.value = 0
showSommaire.value = false showSommaire.value = false
contentLoaded.value = false
await initBookData() await initBookData()
await nextTick() await nextTick()
overlayRef.value?.focus() overlayRef.value?.focus()
@@ -396,8 +386,11 @@ watch(isOpen, async (open) => {
// Load playlist & play first song // Load playlist & play first song
const playlist = getPlaylistOrder() const playlist = getPlaylistOrder()
if (playlist.length) playerStore.setPlaylist(playlist) if (playlist.length) playerStore.setPlaylist(playlist)
const first = getSongs().find(s => s.id === 'chanson-01') if (playlist.length) {
if (first) audioPlayer.loadAndPlay(first) _skipSongWatch = true
audioPlayer.loadAndPlay(playlist[0])
}
await initReading()
} }
else { else {
overlayRef.value?.removeEventListener('touchstart', onTouchStart) overlayRef.value?.removeEventListener('touchstart', onTouchStart)
@@ -406,6 +399,24 @@ watch(isOpen, async (open) => {
} }
}) })
// ── Sync: when song changes externally (persistent player controls), update trackIdx ──
watch(() => playerStore.currentSong, (song) => {
if (!song || !isOpen.value) return
if (_skipSongWatch) {
_skipSongWatch = false
return
}
const idx = tracks.value.findIndex(t => t.id === song.id)
if (idx !== -1 && idx !== trackIdx.value) {
trackIdx.value = idx
currentPage.value = 0
showSommaire.value = false
if (isScrollMode.value && viewportEl.value) {
viewportEl.value.scrollTop = 0
}
}
})
watch(isOpen, (open) => { watch(isOpen, (open) => {
if (import.meta.client) document.body.style.overflow = open ? 'hidden' : '' if (import.meta.client) document.body.style.overflow = open ? 'hidden' : ''
}) })
@@ -436,13 +447,13 @@ onUnmounted(() => {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 60; z-index: 60;
background: hsl(20 8% 3%); background: hsl(var(--color-bg));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
outline: none; outline: none;
overflow: hidden; overflow: hidden;
padding-bottom: 4.5rem; padding-bottom: 1rem;
transition: --scene-h1 1.6s ease, --scene-h2 1.6s ease; transition: --scene-h1 1.6s ease, --scene-h2 1.6s ease;
} }
@@ -519,38 +530,15 @@ onUnmounted(() => {
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.04); } 50% { opacity: 1; transform: translate(-50%, -50%) scale(1.04); }
} }
/* ─── CLOSE ─── */
.bp-close {
position: absolute;
top: 1rem; right: 1rem;
z-index: 75;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem; height: 2.5rem;
border-radius: 50%;
background: hsl(20 8% 8% / 0.6);
backdrop-filter: blur(12px);
color: hsl(20 8% 55%);
border: 1px solid hsl(20 8% 18% / 0.5);
cursor: pointer;
transition: all 0.3s;
}
.bp-close:hover {
background: hsl(var(--scene-h1) 60% 45% / 0.25);
color: white;
border-color: hsl(var(--scene-h1) 60% 50% / 0.4);
}
/* ─── HINT ─── */ /* ─── HINT ─── */
.bp-hint { .bp-hint {
position: absolute; position: absolute;
bottom: 5rem; bottom: 0.5rem;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 10; z-index: 10;
font-size: 0.68rem; font-size: 0.68rem;
color: hsl(20 8% 28%); color: hsl(var(--color-text-muted));
white-space: nowrap; white-space: nowrap;
} }
@@ -565,135 +553,7 @@ onUnmounted(() => {
align-items: center; align-items: center;
width: 100%; width: 100%;
flex: 1; flex: 1;
} min-height: 0;
/* Phase transitions */
.phase-enter-active { animation: phase-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; }
.phase-leave-active { animation: phase-out 0.3s cubic-bezier(0.7, 0, 0.84, 0) both; }
@keyframes phase-in {
from { opacity: 0; transform: scale(0.97); filter: blur(4px); }
to { opacity: 1; transform: scale(1); filter: blur(0); }
}
@keyframes phase-out {
from { opacity: 1; transform: scale(1); filter: blur(0); }
to { opacity: 0; transform: scale(0.97); filter: blur(4px); }
}
/* ═══════════════════════════════════════
INTRO: 3D SPINNING BOOK
═══════════════════════════════════════ */
.bp-intro {
justify-content: center;
}
.spin-scene {
perspective: 1200px;
}
.spin-book {
position: relative;
width: min(220px, 45vw);
aspect-ratio: 3 / 4;
transform-style: preserve-3d;
animation: book-spin 2.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.spin-face {
position: absolute;
inset: 0;
backface-visibility: hidden;
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid hsl(20 8% 18%);
box-shadow: 0 20px 60px hsl(0 0% 0% / 0.5);
}
.spin-front img {
width: 200%;
height: 100%;
object-fit: cover;
transform: translateX(-50%);
}
.spin-back {
transform: rotateY(180deg);
}
.spin-back img {
width: 200%;
height: 100%;
object-fit: cover;
}
@keyframes book-spin {
0% { transform: rotateY(0deg) scale(0.65); opacity: 0; }
8% { opacity: 1; }
45% { transform: rotateY(180deg) scale(0.9); }
75% { transform: rotateY(320deg) scale(1); }
90% { transform: rotateY(352deg) scale(1); }
100% { transform: rotateY(360deg) scale(1); }
}
/* ═══════════════════════════════════════
COVER
═══════════════════════════════════════ */
.bp-cover {
justify-content: center;
text-align: center;
}
.cover-frame {
width: min(200px, 42vw);
aspect-ratio: 3 / 4;
border-radius: 0.625rem;
overflow: hidden;
border: 1px solid hsl(20 8% 18%);
box-shadow:
0 25px 60px hsl(0 0% 0% / 0.5),
0 0 40px hsl(var(--scene-h1) 60% 40% / 0.1);
margin-bottom: 2rem;
animation: cover-float 7s ease-in-out infinite;
}
.cover-img {
width: 200%; height: 100%;
object-fit: cover;
transform: translateX(-50%);
}
@keyframes cover-float {
0%, 100% { transform: translateY(0) rotate(-0.5deg); }
50% { transform: translateY(-10px) rotate(0.5deg); }
}
.cover-title {
font-family: var(--font-display, 'Syne', sans-serif);
font-size: clamp(1.75rem, 5vw, 2.75rem);
font-weight: 800;
line-height: 1.1;
margin-bottom: 0.25rem;
}
.cover-sub {
font-family: var(--font-display, 'Syne', sans-serif);
font-size: clamp(1rem, 3vw, 1.4rem);
color: hsl(20 8% 55%);
margin-bottom: 1.5rem;
}
.cover-desc {
font-size: 0.9rem;
color: hsl(20 8% 45%);
max-width: 26rem;
line-height: 1.65;
margin-bottom: 2rem;
}
.cover-cta {
display: inline-flex;
align-items: center;
padding: 0.75rem 2rem;
border-radius: 9999px;
background: hsl(var(--scene-h1) 70% 45%);
color: white;
font-weight: 600;
font-size: 0.95rem;
border: none;
cursor: pointer;
transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 0 24px hsl(var(--scene-h1) 70% 45% / 0.3);
}
.cover-cta:hover {
background: hsl(var(--scene-h1) 70% 52%);
box-shadow: 0 0 36px hsl(var(--scene-h1) 70% 50% / 0.45);
transform: translateY(-2px);
} }
/* ═══════════════════════════════════════ /* ═══════════════════════════════════════
@@ -704,14 +564,15 @@ onUnmounted(() => {
max-width: 52rem; max-width: 52rem;
padding: 0 1rem; padding: 0 1rem;
gap: 0; gap: 0;
min-height: 0;
} }
/* ─── Top bar ─── */ /* ─── Top bar ─── */
.reader-bar { .reader-bar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.5rem;
padding: 0.75rem 0; padding: 0.5rem 0;
width: 100%; width: 100%;
} }
.reader-bar-btn { .reader-bar-btn {
@@ -720,18 +581,24 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
width: 2.25rem; height: 2.25rem; width: 2.25rem; height: 2.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 8% / 0.5); background: transparent;
backdrop-filter: blur(8px); color: hsl(var(--color-text-muted));
color: hsl(20 8% 55%); border: none;
border: 1px solid hsl(20 8% 16% / 0.5);
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
flex-shrink: 0; flex-shrink: 0;
} }
.reader-bar-btn:hover { .reader-bar-btn:hover {
color: white; color: white;
background: hsl(var(--scene-h1) 50% 40% / 0.2); background: hsl(0 0% 100% / 0.06);
border-color: hsl(var(--scene-h1) 50% 50% / 0.3); }
.reader-bar-close {
color: hsl(0 0% 100% / 0.7);
background: hsl(0 0% 100% / 0.08);
}
.reader-bar-close:hover {
color: white;
background: hsl(0 70% 55% / 0.3);
} }
.reader-bar-title { .reader-bar-title {
flex: 1; flex: 1;
@@ -750,7 +617,7 @@ onUnmounted(() => {
.reader-bar-pages { .reader-bar-pages {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
flex-shrink: 0; flex-shrink: 0;
} }
@@ -759,16 +626,16 @@ onUnmounted(() => {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 72; z-index: 72;
background: hsl(20 8% 3% / 0.6); background: hsl(var(--color-bg) / 0.6);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
display: flex; display: flex;
} }
.sommaire-panel { .sommaire-panel {
width: min(300px, 80vw); width: min(300px, 80vw);
height: 100%; height: 100%;
background: hsl(20 8% 6% / 0.95); background: hsl(var(--color-surface) / 0.95);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
border-right: 1px solid hsl(20 8% 14%); border-right: 1px solid hsl(var(--color-surface-light));
padding: 1.5rem; padding: 1.5rem;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
@@ -779,7 +646,7 @@ onUnmounted(() => {
font-family: var(--font-display, 'Syne', sans-serif); font-family: var(--font-display, 'Syne', sans-serif);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
color: hsl(20 8% 45%); color: hsl(var(--color-text-muted));
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
@@ -791,7 +658,7 @@ onUnmounted(() => {
padding: 0.625rem 0.75rem; padding: 0.625rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: hsl(20 8% 55%); color: hsl(var(--color-text-muted));
background: transparent; background: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -800,7 +667,7 @@ onUnmounted(() => {
width: 100%; width: 100%;
} }
.sommaire-item:hover { .sommaire-item:hover {
background: hsl(20 8% 12%); background: hsl(var(--color-surface));
color: white; color: white;
} }
.sommaire-item--active { .sommaire-item--active {
@@ -816,8 +683,8 @@ onUnmounted(() => {
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 700; font-weight: 700;
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
background: hsl(20 8% 12%); background: hsl(var(--color-surface));
flex-shrink: 0; flex-shrink: 0;
} }
.sommaire-item--active .sommaire-num { .sommaire-item--active .sommaire-num {
@@ -844,12 +711,11 @@ onUnmounted(() => {
position: relative; position: relative;
flex: 1; flex: 1;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden auto;
border-radius: 0.75rem; border-radius: 0.75rem;
background: hsl(20 8% 5% / 0.4); background: hsl(var(--color-bg) / 0.4);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border: 1px solid hsl(20 8% 14% / 0.4);
} }
.reader-columns { .reader-columns {
@@ -860,6 +726,23 @@ onUnmounted(() => {
max-width: none; max-width: none;
} }
/* ─── Scroll mode overrides ─── */
.reader-viewport--scroll {
overflow: hidden auto;
min-height: 0;
}
.reader-columns--scroll {
height: auto;
column-fill: unset;
column-width: unset !important;
transition: none;
}
/* Lyrics: preserve line breaks from \n in text nodes */
.reader-columns :deep(p) {
white-space: pre-line;
}
/* Tighten prose for column context */ /* Tighten prose for column context */
.reader-columns :deep(h1) { .reader-columns :deep(h1) {
font-size: clamp(1.5rem, 3.5vw, 2rem); font-size: clamp(1.5rem, 3.5vw, 2rem);
@@ -880,12 +763,42 @@ onUnmounted(() => {
.reader-columns :deep(h3) { .reader-columns :deep(h3) {
break-after: avoid; break-after: avoid;
} }
.reader-columns :deep(p),
.reader-columns :deep(blockquote), .reader-columns :deep(blockquote),
.reader-columns :deep(ul), .reader-columns :deep(ul),
.reader-columns :deep(ol) { .reader-columns :deep(ol) {
break-inside: avoid; break-inside: avoid;
} }
/* Lyrics content */
.lyrics-content {
white-space: pre-line;
line-height: 1.9;
font-size: clamp(0.9rem, 2vw, 1.05rem);
}
.lyrics-content :deep(.lyrics-tag) {
display: block;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.35;
}
.lyrics-content :deep(p) {
break-inside: auto;
overflow-y: auto;
}
.lyrics-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
.reader-columns :deep(p) {
break-inside: auto;
overflow-y: auto;
}
/* Page-turn shadow overlay */ /* Page-turn shadow overlay */
.reader-shadow { .reader-shadow {
@@ -896,7 +809,7 @@ onUnmounted(() => {
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
background: linear-gradient(to left, hsl(20 8% 3% / 0.4), transparent); background: linear-gradient(to left, hsl(var(--color-bg) / 0.4), transparent);
transition: opacity 0.15s; transition: opacity 0.15s;
border-radius: 0 0.75rem 0.75rem 0; border-radius: 0 0.75rem 0.75rem 0;
} }
@@ -910,7 +823,7 @@ onUnmounted(() => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
padding: 0.5rem 0; padding: 0.25rem 0;
gap: 1rem; gap: 1rem;
} }
.reader-nav-btn { .reader-nav-btn {
@@ -919,18 +832,16 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
width: 2.5rem; height: 2.5rem; width: 2.5rem; height: 2.5rem;
border-radius: 50%; border-radius: 50%;
background: hsl(20 8% 8% / 0.5); background: transparent;
backdrop-filter: blur(8px); color: hsl(var(--color-text-muted));
color: hsl(20 8% 55%); border: none;
border: 1px solid hsl(20 8% 16% / 0.5);
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
flex-shrink: 0; flex-shrink: 0;
} }
.reader-nav-btn:hover { .reader-nav-btn:hover {
background: hsl(var(--scene-h1) 60% 42% / 0.2); background: hsl(0 0% 100% / 0.06);
color: white; color: white;
border-color: hsl(var(--scene-h1) 60% 50% / 0.35);
} }
.reader-nav-btn--hidden { .reader-nav-btn--hidden {
opacity: 0; opacity: 0;
@@ -950,7 +861,7 @@ onUnmounted(() => {
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
border: 2px solid hsl(20 8% 22%); border: 2px solid hsl(var(--color-surface-light));
box-shadow: 0 0 10px hsl(var(--scene-h1) 50% 40% / 0.15); box-shadow: 0 0 10px hsl(var(--scene-h1) 50% 40% / 0.15);
} }
.reader-disc.spinning { .reader-disc.spinning {
@@ -967,12 +878,12 @@ onUnmounted(() => {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 0.5rem; height: 0.5rem; width: 0.5rem; height: 0.5rem;
border-radius: 50%; border-radius: 50%;
background: hsl(20 8% 5%); background: hsl(var(--color-bg));
border: 1.5px solid hsl(20 8% 18%); border: 1.5px solid hsl(var(--color-surface-light));
} }
.reader-song-name { .reader-song-name {
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 50%); color: hsl(var(--color-text-muted));
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -1,5 +1,5 @@
<template> <template>
<article class="prose"> <article class="prose prose-lyrics">
<ContentRenderer :value="content" /> <ContentRenderer :value="content" />
</article> </article>
</template> </template>
@@ -9,3 +9,9 @@ defineProps<{
content: any content: any
}>() }>()
</script> </script>
<style scoped>
.prose-lyrics :deep(p) {
white-space: pre-line;
}
</style>

View File

@@ -15,18 +15,28 @@
{{ description }} {{ description }}
</p> </p>
<!-- Songs + PDF actions row -->
<div class="mt-4 flex flex-wrap items-center gap-2">
<!-- Associated songs badges --> <!-- Associated songs badges -->
<div v-if="songs.length > 0" class="mt-4 flex flex-wrap gap-2">
<button <button
v-for="song in songs" v-for="song in songs"
:key="song.id" :key="song.id"
class="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary transition-colors hover:bg-primary/20" class="song-badge-btn"
@click="playSong(song)" @click="playSong(song)"
> >
<div class="i-lucide-music h-3 w-3" /> <div class="i-lucide-music h-3 w-3" />
{{ song.title }} {{ song.title }}
</button> </button>
<!-- PDF button -->
<button v-if="pdfPage" class="pdf-btn" @click="showPdf = true">
<div class="i-lucide-book-open h-3.5 w-3.5" />
<span>Lire dans le PDF</span>
<span class="pdf-page-badge">p.{{ pdfPage }}</span>
</button>
</div> </div>
<LazyBookPdfReader v-if="showPdf" v-model="showPdf" :page="pdfPage" />
</header> </header>
</template> </template>
@@ -44,9 +54,12 @@ const props = defineProps<{
const bookData = useBookData() const bookData = useBookData()
const { loadAndPlay } = useAudioPlayer() const { loadAndPlay } = useAudioPlayer()
await bookData.init() bookData.init()
const songs = computed(() => bookData.getChapterSongs(props.chapterSlug)) const songs = computed(() => bookData.getChapterSongs(props.chapterSlug))
const pdfPage = computed(() => bookData.getChapterPage(props.chapterSlug))
const showPdf = ref(false)
function playSong(song: Song) { function playSong(song: Song) {
loadAndPlay(song) loadAndPlay(song)
@@ -57,6 +70,59 @@ function playSong(song: Song) {
.chapter-title { .chapter-title {
font-size: clamp(2rem, 5vw, 2.75rem); font-size: clamp(2rem, 5vw, 2.75rem);
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
border-bottom: 2px solid hsl(12 76% 48% / 0.4); border-bottom: 2px solid hsl(var(--color-primary) / 0.4);
}
.song-badge-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
border-radius: 9999px;
padding: 0.375rem 0.875rem;
font-size: 0.8rem;
font-weight: 600;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary) / 0.85);
border: 1px solid hsl(var(--color-primary) / 0.25);
cursor: pointer;
transition: all 0.2s;
}
.song-badge-btn:hover {
background: hsl(var(--color-primary) / 0.2);
border-color: hsl(var(--color-primary) / 0.4);
box-shadow: 0 2px 8px hsl(var(--color-primary) / 0.15);
transform: translateY(-1px);
}
.pdf-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: hsl(0 0% 96%);
color: hsl(0 0% 15%);
font-size: 0.8rem;
font-weight: 600;
border: 1px solid hsl(0 0% 88%);
cursor: pointer;
transition: all 0.2s;
}
.pdf-btn:hover {
background: white;
box-shadow: 0 2px 12px hsl(0 0% 100% / 0.25);
transform: translateY(-1px);
}
.pdf-page-badge {
font-family: var(--font-mono, monospace);
font-size: 0.7rem;
font-weight: 500;
padding: 0.1rem 0.4rem;
border-radius: 0.25rem;
background: hsl(0 0% 88%);
color: hsl(0 0% 35%);
} }
</style> </style>

View File

@@ -6,7 +6,7 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
<li v-for="chapter in chapters" :key="chapter.path"> <li v-for="chapter in chapters" :key="chapter.path">
<NuxtLink <NuxtLink
:to="`/lire/${chapter.stem}`" :to="`/economique/modele-eco/${chapter.stem?.split('/').pop()}`"
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-white/5" class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors hover:bg-white/5"
active-class="bg-primary/10 text-primary font-medium" active-class="bg-primary/10 text-primary font-medium"
> >

View File

@@ -0,0 +1,159 @@
<template>
<div class="chapter-toc" ref="tocRoot">
<button class="toc-toggle" @click="open = !open" :aria-expanded="open">
<div class="i-lucide-list h-4 w-4" />
<span>Sommaire</span>
<div class="i-lucide-chevron-down h-3.5 w-3.5 toc-chevron" :class="{ 'toc-chevron--open': open }" />
</button>
<Transition name="toc-dropdown">
<div v-if="open" class="toc-panel">
<nav>
<a
v-for="item in headings"
:key="item.id"
:href="`#${item.id}`"
class="toc-item"
:class="{ 'toc-item--h2': item.level === 2 }"
@click="open = false"
>
{{ item.text }}
</a>
</nav>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
interface TocHeading {
id: string
text: string
level: number
}
const open = ref(false)
const headings = ref<TocHeading[]>([])
const tocRoot = ref<HTMLElement>()
function extractHeadings() {
const article = document.querySelector('.prose-lyrics')
if (!article) return
const nodes = article.querySelectorAll('h1, h2')
headings.value = Array.from(nodes)
.filter(el => el.id)
.map(el => ({
id: el.id,
text: el.textContent?.trim() ?? '',
level: parseInt(el.tagName[1]),
}))
}
// Close on click outside
function onClickOutside(e: MouseEvent) {
if (tocRoot.value && !tocRoot.value.contains(e.target as Node)) {
open.value = false
}
}
onMounted(() => {
// Wait for content to render
nextTick(() => {
extractHeadings()
// Retry after a short delay in case content loads async
setTimeout(extractHeadings, 500)
})
document.addEventListener('click', onClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', onClickOutside)
})
</script>
<style scoped>
.chapter-toc {
position: relative;
display: inline-block;
}
.toc-toggle {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
background: hsl(20 8% 10%);
border: 1px solid hsl(20 8% 18%);
color: hsl(20 8% 60%);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.toc-toggle:hover {
border-color: hsl(20 8% 30%);
color: white;
}
.toc-chevron {
transition: transform 0.2s;
}
.toc-chevron--open {
transform: rotate(180deg);
}
.toc-panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
z-index: 30;
min-width: 280px;
max-width: 400px;
max-height: 60vh;
overflow-y: auto;
padding: 0.5rem;
border-radius: 0.5rem;
background: hsl(20 8% 8%);
border: 1px solid hsl(20 8% 16%);
box-shadow: 0 8px 32px hsl(0 0% 0% / 0.4);
}
.toc-item {
display: block;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.8rem;
color: hsl(20 8% 65%);
text-decoration: none;
transition: all 0.15s;
line-height: 1.3;
}
.toc-item:hover {
background: hsl(20 8% 12%);
color: white;
}
.toc-item--h2 {
padding-left: 1.5rem;
font-size: 0.75rem;
color: hsl(20 8% 50%);
}
/* Dropdown transition */
.toc-dropdown-enter-active {
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.toc-dropdown-leave-active {
transition: all 0.15s ease-in;
}
.toc-dropdown-enter-from,
.toc-dropdown-leave-to {
opacity: 0;
transform: translateY(-4px);
}
</style>

View File

@@ -0,0 +1,353 @@
<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 }"
>
<!-- Clickable card body -->
<component
:is="itemTag(item)"
v-bind="itemAttrs(item)"
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>
</component>
<!-- Actions zone (separate from card link) -->
<div v-if="item.actions?.length" class="axis-actions">
<!-- Primary row -->
<div class="axis-actions-row">
<button
v-for="action in primaryActions(item.actions)"
:key="action.id"
class="axis-action-btn"
:class="{ 'axis-action-btn--highlight': action.highlight }"
@click.stop="handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</button>
</div>
<!-- Secondary row -->
<div v-if="secondaryActions(item.actions).length" class="axis-actions-secondary">
<component
:is="action.to ? resolveComponent('NuxtLink') : 'button'"
v-for="action in secondaryActions(item.actions)"
:key="action.label"
:to="action.to"
class="axis-action-btn axis-action-btn--secondary"
@click.stop="!action.to && handleAction(action.id)"
>
<div :class="iconClass(action.icon)" class="h-3.5 w-3.5" />
{{ action.label }}
</component>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface AxisAction {
id: string
label: string
icon: string
highlight?: boolean
secondary?: boolean
to?: 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;
/* overflow: visible pour laisser le tooltip sortir du cadre */
}
.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;
text-decoration: none;
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;
}
.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>

View File

@@ -0,0 +1,237 @@
<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>

View File

@@ -1,5 +1,56 @@
<template> <template>
<section class="section-padding"> <section class="relative overflow-hidden section-padding">
<!-- Shadok thinker: ovoid character sitting, hand on chin, thinking bubble -->
<svg class="shadok-thinker" viewBox="0 0 220 280" fill="none" aria-hidden="true">
<!-- Body (seated, leaning forward) -->
<ellipse cx="100" cy="160" rx="42" ry="50" fill="currentColor" opacity="0.85"/>
<!-- Head (tilted) -->
<ellipse cx="110" cy="95" rx="25" ry="24" fill="currentColor" opacity="0.8"/>
<!-- Neck -->
<path d="M100 118 Q105 110 108 105" stroke="currentColor" stroke-width="6" stroke-linecap="round" opacity="0.6" fill="none"/>
<!-- Eyes (contemplative) -->
<circle cx="103" cy="90" r="4.5" fill="currentColor" opacity="0.2"/>
<circle cx="120" cy="90" r="4.5" fill="currentColor" opacity="0.2"/>
<circle cx="104" cy="89" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="121" cy="89" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Arm to chin -->
<line x1="140" y1="145" x2="130" y2="108" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Hand on chin -->
<circle cx="130" cy="105" r="5" fill="currentColor" opacity="0.45"/>
<!-- Seated legs (crossed/bent) -->
<path d="M75 205 Q60 230 50 240" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6" fill="none"/>
<path d="M120 205 Q140 220 145 240" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6" fill="none"/>
<!-- Feet -->
<path d="M50 240 L38 243 M50 240 L45 246" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M145 240 L133 243 M145 240 L140 246" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<!-- Thinking bubbles -->
<circle cx="150" cy="78" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="165" cy="62" r="8" fill="currentColor" opacity="0.25"/>
<circle cx="185" cy="42" r="12" fill="currentColor" opacity="0.2"/>
</svg>
<!-- Shadok menuisier: character with plane and plank -->
<svg class="shadok-menuisier" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<ellipse cx="120" cy="155" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
<path d="M98 82 Q120 68 142 82" fill="currentColor" opacity="0.35"/>
<rect x="100" y="80" width="40" height="5" rx="1" fill="currentColor" opacity="0.3"/>
<circle cx="112" cy="90" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="130" cy="90" r="4" fill="currentColor" opacity="0.2"/>
<circle cx="113" cy="89" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="131" cy="89" r="1.8" fill="currentColor" opacity="0.5"/>
<line x1="114" y1="103" x2="126" y2="103" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<line x1="160" y1="145" x2="195" y2="160" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<rect x="190" y="155" width="30" height="12" rx="2" fill="currentColor" opacity="0.4"/>
<rect x="200" y="150" width="8" height="8" rx="1" fill="currentColor" opacity="0.35"/>
<line x1="80" y1="148" x2="50" y2="168" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="105" y1="200" x2="95" y2="258" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="135" y1="200" x2="145" y2="258" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<rect x="35" y="170" width="80" height="8" rx="1" fill="currentColor" opacity="0.3"/>
<path d="M60 168 Q55 162 62 160" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<path d="M80 166 Q76 158 83 157" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.18"/>
</svg>
<div class="container-content"> <div class="container-content">
<div class="grid items-center gap-12 md:grid-cols-2"> <div class="grid items-center gap-12 md:grid-cols-2">
<!-- Book cover --> <!-- Book cover -->
@@ -63,10 +114,10 @@ const { data: content } = await usePageContent('home')
aspect-ratio: 3 / 4; aspect-ratio: 3 / 4;
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: hidden; overflow: hidden;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-text) / 0.1);
box-shadow: box-shadow:
0 12px 40px hsl(0 0% 0% / 0.5), 0 12px 40px hsl(var(--color-text) / 0.15),
0 0 0 1px hsl(20 8% 15%); 0 0 0 1px hsl(var(--color-text) / 0.08);
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1), transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
box-shadow 0.5s ease; box-shadow 0.5s ease;
max-width: 360px; max-width: 360px;
@@ -75,8 +126,8 @@ const { data: content } = await usePageContent('home')
.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:
12px 16px 48px hsl(0 0% 0% / 0.6), 12px 16px 48px hsl(var(--color-text) / 0.2),
0 0 0 1px hsl(12 76% 48% / 0.2); 0 0 0 1px hsl(var(--color-primary) / 0.2);
} }
.book-cover-img { .book-cover-img {
@@ -89,4 +140,41 @@ const { data: content } = await usePageContent('home')
.heading-section { .heading-section {
font-size: clamp(1.625rem, 4vw, 2.125rem); font-size: clamp(1.625rem, 4vw, 2.125rem);
} }
.shadok-thinker {
position: absolute;
right: 2%;
bottom: 6%;
width: clamp(110px, 14vw, 200px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-thinker 10s ease-in-out infinite;
}
@keyframes shadok-float-thinker {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.shadok-menuisier {
position: absolute;
left: 2%;
top: 5%;
width: clamp(100px, 13vw, 190px);
opacity: 0.25;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-menuisier 10s ease-in-out infinite;
}
@keyframes shadok-float-menuisier {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@media (max-width: 768px) {
.shadok-thinker { display: none; }
.shadok-menuisier { display: none; }
}
</style> </style>

View File

@@ -4,7 +4,7 @@
<div class="grid items-center gap-12 md:grid-cols-2"> <div class="grid items-center gap-12 md:grid-cols-2">
<!-- Book cover --> <!-- Book cover -->
<UiScrollReveal> <UiScrollReveal>
<div class="book-cover-wrapper"> <div class="book-cover-wrapper relative">
<div class="book-cover-3d"> <div class="book-cover-3d">
<img <img
:src="content?.book.coverImage" :src="content?.book.coverImage"
@@ -31,16 +31,11 @@
</UiScrollReveal> </UiScrollReveal>
<UiScrollReveal :delay="200"> <UiScrollReveal :delay="200">
<div class="mt-8 flex flex-col gap-3 sm:flex-row sm:gap-4"> <BookActions
<UiBaseButton @click="$emit('open-player')"> :show-chapters="showChapters"
<div class="i-lucide-play mr-2 h-5 w-5" /> @open-player="$emit('open-player')"
{{ content?.book.cta.player }} @open-pdf="$emit('open-pdf')"
</UiBaseButton> />
<UiBaseButton variant="accent" @click="$emit('open-pdf')">
<div class="i-lucide-book-open mr-2 h-5 w-5" />
{{ content?.book.cta.pdf }}
</UiBaseButton>
</div>
</UiScrollReveal> </UiScrollReveal>
</div> </div>
</div> </div>
@@ -49,6 +44,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
withDefaults(defineProps<{
showChapters?: boolean
}>(), {
showChapters: true,
})
defineEmits<{ defineEmits<{
'open-player': [] 'open-player': []
'open-pdf': [] 'open-pdf': []
@@ -68,10 +69,10 @@ const { data: content } = await usePageContent('home')
aspect-ratio: 3 / 4; aspect-ratio: 3 / 4;
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: hidden; overflow: hidden;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-text) / 0.1);
box-shadow: box-shadow:
0 12px 40px hsl(0 0% 0% / 0.5), 0 12px 40px hsl(var(--color-text) / 0.15),
0 0 0 1px hsl(20 8% 15%); 0 0 0 1px hsl(var(--color-text) / 0.08);
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1), transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
box-shadow 0.5s ease; box-shadow 0.5s ease;
max-width: 360px; max-width: 360px;
@@ -80,8 +81,8 @@ const { data: content } = await usePageContent('home')
.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:
12px 16px 48px hsl(0 0% 0% / 0.6), 12px 16px 48px hsl(var(--color-text) / 0.2),
0 0 0 1px hsl(12 76% 48% / 0.2); 0 0 0 1px hsl(var(--color-primary) / 0.2);
} }
.book-cover-img { .book-cover-img {

View File

@@ -1,5 +1,37 @@
<template> <template>
<section class="section-padding"> <section class="relative overflow-hidden section-padding">
<!-- Shadok scale: balance with absurd objects -->
<svg class="shadok-scale" viewBox="0 0 260 280" fill="none" aria-hidden="true">
<!-- Vertical pole -->
<line x1="130" y1="40" x2="130" y2="240" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.8"/>
<!-- Base triangle -->
<polygon points="100,240 160,240 130,220" fill="currentColor" opacity="0.5"/>
<!-- Horizontal beam -->
<line x1="40" y1="80" x2="220" y2="60" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.7"/>
<!-- Pivot circle -->
<circle cx="130" cy="70" r="8" fill="currentColor" opacity="0.6"/>
<!-- Left pan (chain lines) -->
<line x1="40" y1="80" x2="30" y2="120" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="40" y1="80" x2="70" y2="120" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<!-- Left pan dish -->
<path d="M20 120 Q50 135 80 120" stroke="currentColor" stroke-width="2.5" fill="currentColor" opacity="0.35"/>
<!-- Absurd object on left: a snail -->
<ellipse cx="50" cy="112" rx="14" ry="8" fill="currentColor" opacity="0.5"/>
<path d="M60 108 Q68 95 58 92 Q48 90 52 100" stroke="currentColor" stroke-width="2" fill="none" opacity="0.4"/>
<!-- Right pan (chain lines) -->
<line x1="220" y1="60" x2="210" y2="100" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<line x1="220" y1="60" x2="250" y2="100" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<!-- Right pan dish -->
<path d="M200 100 Q230 115 260 100" stroke="currentColor" stroke-width="2.5" fill="currentColor" opacity="0.35"/>
<!-- Absurd object on right: a star/coin -->
<circle cx="230" cy="92" r="10" fill="currentColor" opacity="0.4"/>
<circle cx="230" cy="92" r="5" fill="currentColor" opacity="0.2"/>
<!-- Tiny Shadok perched on top -->
<ellipse cx="130" cy="35" rx="12" ry="10" fill="currentColor" opacity="0.6"/>
<circle cx="130" cy="22" r="7" fill="currentColor" opacity="0.55"/>
<circle cx="133" cy="20" r="2" fill="currentColor" opacity="0.3"/>
</svg>
<div class="container-content"> <div class="container-content">
<div class="mx-auto max-w-3xl text-center"> <div class="mx-auto max-w-3xl text-center">
<UiScrollReveal> <UiScrollReveal>
@@ -41,4 +73,24 @@ const { data: content } = await usePageContent('home')
.heading-section { .heading-section {
font-size: clamp(1.625rem, 4vw, 2.125rem); font-size: clamp(1.625rem, 4vw, 2.125rem);
} }
.shadok-scale {
position: absolute;
left: 2%;
top: 8%;
width: clamp(120px, 16vw, 240px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-scale 9s ease-in-out infinite;
}
@keyframes shadok-float-scale {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-10px) rotate(2deg); }
}
@media (max-width: 768px) {
.shadok-scale { display: none; }
}
</style> </style>

View File

@@ -1,78 +0,0 @@
<template>
<section class="section-padding">
<div class="container-content">
<UiScrollReveal>
<div class="gw-card">
<div class="flex flex-col items-center text-center gap-4 md:flex-row md:text-left md:gap-8">
<!-- Icon -->
<div class="gw-icon-wrapper">
<div class="i-lucide-sparkles h-8 w-8 text-amber-400" />
</div>
<!-- Content -->
<div class="flex-1">
<span class="inline-block mb-2 rounded-full bg-amber-400/15 px-3 py-0.5 font-mono text-xs tracking-widest text-amber-400 uppercase">
{{ content?.grateWizardTeaser.kicker }}
</span>
<h3 class="heading-h3 font-display font-bold text-white">
{{ content?.grateWizardTeaser.title }}
</h3>
<p class="mt-2 text-sm text-white/60 md:text-base leading-relaxed">
{{ content?.grateWizardTeaser.description }}
</p>
</div>
<!-- CTAs -->
<div class="shrink-0 flex flex-col gap-2">
<UiBaseButton @click="launch">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
{{ content?.grateWizardTeaser.cta.launch }}
</UiBaseButton>
<UiBaseButton variant="ghost" :to="content?.grateWizardTeaser.cta.more.to">
{{ content?.grateWizardTeaser.cta.more.label }}
<div class="i-lucide-arrow-right ml-2 h-4 w-4" />
</UiBaseButton>
</div>
</div>
</div>
</UiScrollReveal>
</div>
</section>
</template>
<script setup lang="ts">
const { launch } = useGrateWizard()
const { data: content } = await usePageContent('home')
</script>
<style scoped>
.gw-card {
border: 1px solid hsl(40 80% 50% / 0.2);
border-radius: 1rem;
padding: 1.5rem 2rem;
background: linear-gradient(135deg, hsl(40 80% 50% / 0.05), hsl(40 80% 50% / 0.02));
box-shadow: 0 0 40px hsl(40 80% 50% / 0.05);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.gw-card:hover {
border-color: hsl(40 80% 50% / 0.35);
box-shadow: 0 0 60px hsl(40 80% 50% / 0.1);
}
.heading-h3 {
font-size: clamp(1.25rem, 3vw, 1.625rem);
}
.gw-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
background: hsl(40 80% 50% / 0.1);
border: 1px solid hsl(40 80% 50% / 0.15);
flex-shrink: 0;
}
</style>

View File

@@ -1,46 +1,54 @@
<template> <template>
<section class="relative overflow-hidden section-padding"> <section class="relative overflow-hidden section-padding hero-section">
<!-- Background gradient --> <!-- 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-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.15),transparent_70%)]" /> <div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(12_76%_48%/0.12),transparent_70%)]" />
<!-- 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"/>
<circle cx="130" cy="60" r="22" fill="currentColor" opacity="0.8"/>
<path d="M110 85 Q125 70 128 63" stroke="currentColor" stroke-width="8" stroke-linecap="round" opacity="0.7" fill="none"/>
<circle cx="136" cy="55" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="137" cy="54" r="2" fill="currentColor" opacity="0.6"/>
<polygon points="150,58 175,50 152,65" fill="currentColor" opacity="0.6"/>
<line x1="75" y1="138" x2="60" y2="230" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="105" y1="138" x2="115" y2="230" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<circle cx="66" cy="190" r="4" fill="currentColor" opacity="0.4"/>
<circle cx="111" cy="190" r="4" fill="currentColor" opacity="0.4"/>
<path d="M60 230 L45 233 M60 230 L55 236 M60 230 L65 235" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M115 230 L100 233 M115 230 L110 236 M115 230 L120 235" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M48 95 Q20 80 15 65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5" fill="none"/>
<path d="M48 100 Q22 92 10 85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4" fill="none"/>
<path d="M48 105 Q25 102 12 100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3" fill="none"/>
</svg>
<!-- Shadok boulanger -->
<svg class="shadok-boulanger" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<ellipse cx="120" cy="155" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
<ellipse cx="120" cy="68" rx="18" ry="22" fill="currentColor" opacity="0.35"/>
<rect x="105" y="78" width="30" height="5" rx="1" fill="currentColor" opacity="0.4"/>
<path d="M110 88 Q114 84 118 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M124 88 Q128 84 132 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M112 102 Q120 108 128 102" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.35"/>
<line x1="160" y1="145" x2="190" y2="135" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<rect x="185" y="125" width="40" height="10" rx="5" fill="currentColor" opacity="0.4" transform="rotate(-15 205 130)"/>
<line x1="80" y1="148" x2="55" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="105" y1="200" x2="95" y2="258" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="135" y1="200" x2="145" y2="258" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<rect x="30" y="180" width="50" height="40" rx="4" fill="currentColor" opacity="0.25"/>
<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 --> <!-- Content -->
<div class="container-content relative z-10 px-4"> <div class="container-content relative z-10 px-4">
<div class="mx-auto max-w-3xl text-center"> <div class="mx-auto max-w-2xl">
<UiScrollReveal> <HomeTypewriterText
<p class="mb-3 font-mono text-sm tracking-widest text-primary uppercase"> v-if="hero"
{{ content?.hero.kicker }} :hero="hero"
</p> />
</UiScrollReveal>
<UiScrollReveal :delay="100">
<h1 class="font-display font-extrabold leading-tight tracking-tight">
<span class="hero-title text-gradient">{{ content?.hero.title }}</span>
</h1>
</UiScrollReveal>
<UiScrollReveal :delay="200">
<p class="mt-6 text-lg leading-relaxed text-white/60 md:text-xl">
{{ content?.hero.subtitle }}
</p>
</UiScrollReveal>
<UiScrollReveal :delay="300">
<p class="mt-4 text-base leading-relaxed text-white/45">
{{ content?.hero.footnote }}
</p>
</UiScrollReveal>
<UiScrollReveal :delay="400">
<div class="mt-8 flex justify-center">
<UiBaseButton variant="ghost" :to="content?.hero.cta.to">
{{ content?.hero.cta.label }}
<div class="i-lucide-arrow-right ml-2 h-4 w-4" />
</UiBaseButton>
</div>
</UiScrollReveal>
<HomeMessages />
</div> </div>
</div> </div>
</section> </section>
@@ -48,10 +56,65 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('home') 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> </script>
<style scoped> <style scoped>
.hero-title { .hero-section {
font-size: clamp(2.25rem, 7vw, 4rem); min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
}
.shadok-bird {
position: absolute;
right: 4%;
top: 12%;
width: clamp(110px, 15vw, 220px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-primary));
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;
}
@keyframes shadok-float-boulanger {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@media (max-width: 768px) {
.shadok-bird { display: none; }
.shadok-boulanger { display: none; }
} }
</style> </style>

View File

@@ -1,59 +1,75 @@
<template> <template>
<div class="mt-16"> <section class="section-padding">
<div class="container-content mx-auto max-w-3xl">
<!-- Formulaire --> <!-- Formulaire -->
<UiScrollReveal :delay="500"> <UiScrollReveal>
<div class="message-form-card"> <div class="message-form-card">
<h3 class="font-display text-lg font-bold text-white mb-4">Laisser un message</h3> <h3 class="font-display text-lg font-bold form-title 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"
required
class="msg-input" class="msg-input"
/> />
<input <p class="hint-text text-xs -mt-1 px-1">Pour recevoir une réponse, laissez votre e-mail dans le message.</p>
v-model="form.email" <select v-model="form.type" class="msg-input msg-input--select">
type="email" <option value="question">Question</option>
placeholder="Email (optionnel)" <option value="suggestion">Suggestion</option>
class="msg-input" <option value="retour">Retour d'expérience</option>
/> </select>
</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="msg-confirm-title 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="msg-confirm-sub 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="msg-confirm-note text-xs mt-4 italic">
Chaque message est un premier pas dans l'aventure.
</p>
</div> </div>
</div> </div>
</UiScrollReveal> </UiScrollReveal>
<!-- 2 derniers messages publiés --> <!-- 2 derniers messages publiés -->
<UiScrollReveal v-if="messages?.length" :delay="600"> <UiScrollReveal v-if="messages?.length" :delay="100">
<div class="mt-8 space-y-4"> <div class="mt-8 space-y-4">
<h3 class="font-display text-lg font-bold text-white/80 text-center">Derniers messages</h3> <h3 class="font-display text-lg font-bold section-title text-center">Derniers messages</h3>
<div v-for="msg in messages.slice(0, 2)" :key="msg.id" class="message-card"> <div v-for="msg in messages.slice(0, 2)" :key="msg.id" class="message-card">
<p class="text-white/80 text-sm leading-relaxed">{{ msg.text }}</p> <!-- En-tête auteur -->
<div class="mt-2 flex items-center gap-2 text-xs text-white/40"> <div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-white/60">{{ msg.author }}</span> <span class="msg-author font-semibold text-sm">{{ msg.author }}</span>
<span>&middot;</span> <span class="type-pill">{{ typeLabel(msg.type) }}</span>
<span>{{ formatDate(msg.createdAt) }}</span> <span class="msg-date text-xs ml-auto">{{ formatDate(msg.createdAt) }}</span>
</div>
<!-- Texte du message -->
<p class="msg-text text-sm leading-relaxed">{{ msg.text }}</p>
<!-- Réponse -->
<div v-if="msg.reply?.text" class="reply-thread">
<div class="reply-connector" aria-hidden="true" />
<div class="reply-block">
<div class="flex items-center gap-1.5 mb-1">
<div class="i-lucide-corner-down-right h-3 w-3 reply-icon" />
<span class="reply-author text-xs font-semibold">Le Librodrome</span>
</div>
<p class="reply-text text-sm leading-relaxed italic">{{ msg.reply.text }}</p>
</div>
</div> </div>
</div> </div>
<div class="text-center"> <div class="text-center">
@@ -65,16 +81,31 @@
</div> </div>
</UiScrollReveal> </UiScrollReveal>
</div> </div>
</section>
</template> </template>
<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: '', type: 'question' })
const sending = ref(false) const sending = ref(false)
const submitted = ref(false) const submitted = ref(false)
const canSend = computed(() => form.text.trim().length > 0)
const TYPE_LABELS: Record<string, string> = {
reaction: 'Réaction',
question: 'Question',
suggestion: 'Suggestion',
retour: 'Retour',
}
function typeLabel(type: string) {
return TYPE_LABELS[type] ?? type
}
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 })
@@ -105,9 +136,24 @@ function formatDate(iso: string) {
</script> </script>
<style scoped> <style scoped>
/* ── Couleurs adaptatives (dark + light) ── */
.form-title { color: hsl(var(--color-text)); }
.hint-text { color: hsl(var(--color-text) / 0.35); }
.section-title { color: hsl(var(--color-text) / 0.85); }
.msg-author { color: hsl(var(--color-text) / 0.75); }
.msg-date { color: hsl(var(--color-text) / 0.38); }
.msg-text { color: hsl(var(--color-text) / 0.78); white-space: pre-line; }
.msg-confirm-title { color: hsl(var(--color-text) / 0.95); }
.msg-confirm-sub { color: hsl(var(--color-text) / 0.6); }
.msg-confirm-note { color: hsl(var(--color-primary) / 0.65); }
.reply-icon { color: hsl(var(--color-primary) / 0.5); }
.reply-author { color: hsl(var(--color-primary) / 0.8); }
.reply-text { color: hsl(var(--color-text) / 0.62); }
/* ── Carte formulaire ── */
.message-form-card { .message-form-card {
background: hsl(20 8% 6%); background: hsl(var(--color-surface));
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-text) / 0.1);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1.5rem; padding: 1.5rem;
} }
@@ -116,26 +162,75 @@ function formatDate(iso: string) {
width: 100%; width: 100%;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-text) / 0.12);
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.875rem; font-size: 0.875rem;
font-family: inherit;
transition: border-color 0.2s; transition: border-color 0.2s;
} }
.msg-input--select {
width: auto;
cursor: pointer;
}
.msg-input::placeholder { .msg-input::placeholder {
color: hsl(20 8% 40%); color: hsl(var(--color-text) / 0.35);
} }
.msg-input:focus { .msg-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
/* ── Carte message ── */
.message-card { .message-card {
background: hsl(20 8% 6%); background: hsl(var(--color-surface));
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-text) / 0.1);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
} }
.type-pill {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.45rem;
border-radius: 9999px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary) / 0.7);
}
/* ── Réponse avec liaison graphique ── */
.reply-thread {
position: relative;
display: flex;
gap: 0;
margin-top: 0.75rem;
padding-left: 1.25rem;
}
.reply-connector {
position: absolute;
left: 0.5rem;
top: -0.5rem;
bottom: 0.5rem;
width: 2px;
background: linear-gradient(
to bottom,
hsl(var(--color-primary) / 0.35),
hsl(var(--color-primary) / 0.15)
);
border-radius: 2px;
}
.reply-block {
flex: 1;
background: hsl(var(--color-primary) / 0.05);
border-left: 2px solid hsl(var(--color-primary) / 0.25);
border-radius: 0 0.5rem 0.5rem 0;
padding: 0.6rem 0.875rem;
}
</style> </style>

View File

@@ -1,5 +1,37 @@
<template> <template>
<section class="section-padding bg-surface-600/50"> <section class="relative overflow-hidden section-padding bg-surface-600/50">
<!-- Shadok musician: round character playing a trumpet -->
<svg class="shadok-musician" viewBox="0 0 220 280" fill="none" aria-hidden="true">
<!-- Body (ovoid) -->
<ellipse cx="100" cy="150" rx="45" ry="55" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="100" cy="80" r="28" fill="currentColor" opacity="0.8"/>
<!-- Eyes -->
<circle cx="90" cy="74" r="5" fill="currentColor" opacity="0.2"/>
<circle cx="110" cy="74" r="5" fill="currentColor" opacity="0.2"/>
<circle cx="91" cy="73" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="111" cy="73" r="2" fill="currentColor" opacity="0.5"/>
<!-- Mouth (blowing) -->
<circle cx="125" cy="86" r="4" fill="currentColor" opacity="0.3"/>
<!-- Trumpet -->
<line x1="128" y1="86" x2="185" y2="78" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
<path d="M185 68 Q200 78 185 88" stroke="currentColor" stroke-width="3" fill="currentColor" opacity="0.45"/>
<!-- Trumpet valves -->
<circle cx="155" cy="80" r="3" fill="currentColor" opacity="0.3"/>
<circle cx="165" cy="79" r="3" fill="currentColor" opacity="0.3"/>
<!-- Music notes floating -->
<circle cx="205" cy="60" r="4" fill="currentColor" opacity="0.4"/>
<line x1="209" y1="60" x2="209" y2="42" stroke="currentColor" stroke-width="1.5" opacity="0.4"/>
<circle cx="195" cy="45" r="3" fill="currentColor" opacity="0.3"/>
<line x1="198" y1="45" x2="198" y2="30" stroke="currentColor" stroke-width="1.5" opacity="0.3"/>
<!-- Legs -->
<line x1="82" y1="202" x2="72" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="118" y1="202" x2="128" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Feet -->
<path d="M72 255 L58 258 M72 255 L66 261" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M128 255 L114 258 M128 255 L122 261" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
</svg>
<div class="container-content"> <div class="container-content">
<UiScrollReveal> <UiScrollReveal>
<div class="text-center mb-12"> <div class="text-center mb-12">
@@ -48,4 +80,24 @@ const featuredSongs = computed(() => bookData.getSongs().slice(0, 6))
.heading-section { .heading-section {
font-size: clamp(1.625rem, 4vw, 2.125rem); font-size: clamp(1.625rem, 4vw, 2.125rem);
} }
.shadok-musician {
position: absolute;
right: 2%;
top: 4%;
width: clamp(110px, 15vw, 210px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-musician 8s ease-in-out infinite;
}
@keyframes shadok-float-musician {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok-musician { display: none; }
}
</style> </style>

View File

@@ -0,0 +1,279 @@
<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>

View File

@@ -11,7 +11,7 @@
<Transition name="slide-menu"> <Transition name="slide-menu">
<nav <nav
v-if="open" v-if="open"
class="fixed inset-y-0 right-0 z-50 w-72 bg-surface-600 border-l border-white/8 p-6 shadow-2xl" class="fixed inset-y-0 right-0 z-50 w-72 bg-[hsl(var(--color-surface))] border-l border-[hsl(var(--color-text)/0.1)] p-6 shadow-2xl"
aria-label="Menu mobile" aria-label="Menu mobile"
> >
<div class="flex items-center justify-between mb-8"> <div class="flex items-center justify-between mb-8">
@@ -29,7 +29,7 @@
<li v-for="item in nav" :key="item.to"> <li v-for="item in nav" :key="item.to">
<NuxtLink <NuxtLink
:to="item.to" :to="item.to"
class="flex items-center gap-3 rounded-lg px-4 py-3 text-base font-medium transition-colors hover:bg-white/5" class="flex items-center gap-3 rounded-lg px-4 py-3 text-base font-medium text-[hsl(var(--color-text)/0.7)] transition-colors hover:bg-[hsl(var(--color-text)/0.06)] hover:text-[hsl(var(--color-text))]"
active-class="bg-primary/10 text-primary" active-class="bg-primary/10 text-primary"
@click="emit('update:open', false)" @click="emit('update:open', false)"
> >

View File

@@ -1,9 +1,48 @@
<template> <template>
<footer class="border-t border-white/8 bg-surface-600 pb-[var(--player-height)]"> <footer class="footer-wrap border-t border-[hsl(var(--color-text)/0.1)] bg-[hsl(var(--color-surface))]">
<div class="container-content px-4 py-8"> <!-- Shadok pattern -->
<svg class="footer-shadok-pattern" viewBox="0 0 400 80" fill="none" aria-hidden="true">
<g transform="translate(20,10)">
<ellipse cx="15" cy="25" rx="12" ry="14" fill="currentColor" opacity="0.2"/>
<circle cx="15" cy="10" r="7" fill="currentColor" opacity="0.16"/>
<line x1="10" y1="38" x2="8" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="20" y1="38" x2="22" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
</g>
<g transform="translate(80,15)">
<ellipse cx="15" cy="22" rx="10" ry="12" fill="currentColor" opacity="0.16"/>
<circle cx="15" cy="8" r="6" fill="currentColor" opacity="0.13"/>
<line x1="10" y1="33" x2="8" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
<line x1="20" y1="33" x2="22" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
</g>
<g transform="translate(140,8)">
<ellipse cx="15" cy="25" rx="11" ry="13" fill="currentColor" opacity="0.18"/>
<circle cx="15" cy="10" r="6.5" fill="currentColor" opacity="0.15"/>
<line x1="10" y1="37" x2="7" y2="54" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
<line x1="20" y1="37" x2="23" y2="54" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
</g>
<g transform="translate(210,18)">
<ellipse cx="15" cy="20" rx="10" ry="11" fill="currentColor" opacity="0.14"/>
<circle cx="15" cy="7" r="5.5" fill="currentColor" opacity="0.11"/>
<line x1="10" y1="30" x2="9" y2="44" stroke="currentColor" stroke-width="1.5" opacity="0.1"/>
<line x1="20" y1="30" x2="21" y2="44" stroke="currentColor" stroke-width="1.5" opacity="0.1"/>
</g>
<g transform="translate(270,12)">
<ellipse cx="15" cy="24" rx="12" ry="14" fill="currentColor" opacity="0.18"/>
<circle cx="15" cy="9" r="7" fill="currentColor" opacity="0.15"/>
<line x1="10" y1="37" x2="7" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
<line x1="20" y1="37" x2="23" y2="55" stroke="currentColor" stroke-width="1.5" opacity="0.14"/>
</g>
<g transform="translate(340,16)">
<ellipse cx="15" cy="22" rx="10" ry="12" fill="currentColor" opacity="0.16"/>
<circle cx="15" cy="8" r="6" fill="currentColor" opacity="0.13"/>
<line x1="10" y1="33" x2="8" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
<line x1="20" y1="33" x2="22" y2="48" stroke="currentColor" stroke-width="1.5" opacity="0.12"/>
</g>
</svg>
<div class="container-content px-4 py-8 relative z-1">
<div class="flex flex-col items-center gap-4 md:flex-row md:justify-between"> <div class="flex flex-col items-center gap-4 md:flex-row md:justify-between">
<!-- Credits --> <!-- Credits -->
<p class="text-sm text-white/40"> <p class="text-sm text-[hsl(var(--color-text)/0.4)]">
{{ site?.footer.credits }} {{ site?.footer.credits }}
</p> </p>
@@ -13,7 +52,7 @@
v-for="link in site?.footer.links" v-for="link in site?.footer.links"
:key="link.to" :key="link.to"
:to="link.to" :to="link.to"
class="text-sm text-white/40 transition-colors hover:text-white/70" class="text-sm text-[hsl(var(--color-text)/0.4)] transition-colors hover:text-[hsl(var(--color-text)/0.7)]"
> >
{{ link.label }} {{ link.label }}
</NuxtLink> </NuxtLink>
@@ -26,3 +65,21 @@
<script setup lang="ts"> <script setup lang="ts">
const { data: site } = await useSiteContent() const { data: site } = await useSiteContent()
</script> </script>
<style scoped>
.footer-wrap {
position: relative;
overflow: hidden;
}
.footer-shadok-pattern {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: auto;
opacity: 1;
pointer-events: none;
color: hsl(var(--color-primary));
}
</style>

View File

@@ -1,23 +1,46 @@
<template> <template>
<header class="sticky top-0 z-40 border-b border-white/8 bg-surface-bg/80 backdrop-blur-xl"> <header class="sticky top-0 z-40 border-b border-[hsl(var(--color-text)/0.08)] bg-[hsl(var(--color-bg)/0.85)] backdrop-blur-xl">
<div class="container-content flex h-[var(--header-height)] items-center justify-between px-4"> <div class="container-content flex h-[var(--header-height)] items-center justify-between px-4">
<!-- Logo --> <!-- Logo -->
<NuxtLink to="/" class="flex items-center gap-2 font-display text-lg font-bold tracking-tight"> <NuxtLink to="/" class="logo-link flex items-center gap-2.5">
<div class="i-lucide-book-open h-6 w-6 text-primary" /> <svg class="logo-icon" viewBox="0 0 64 80" fill="none" aria-hidden="true">
<span class="text-gradient">{{ site?.identity.name }}</span> <path d="M38 8 C28 6 18 10 18 20 C18 28 26 32 34 34 C42 36 48 40 48 48 C48 52 46 55 42 57 L44 40 C44 36 40 32 34 30 C28 28 22 24 22 18 C22 14 24 11 28 10Z" fill="currentColor" opacity="0.9"/>
<path d="M26 72 C36 74 46 70 46 60 C46 52 38 48 30 46 C22 44 16 40 16 32 C16 28 18 25 22 23 L20 40 C20 44 24 48 30 50 C36 52 42 56 42 62 C42 66 40 69 36 70Z" fill="currentColor" opacity="0.9"/>
<path d="M20 16 C20 8 28 4 36 6 C42 8 46 14 44 20" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0.7"/>
<path d="M44 64 C44 72 36 76 28 74 C22 72 18 66 20 60" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0.7"/>
<path d="M36 4 Q42 2 46 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
<path d="M28 76 Q22 78 18 74" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
</svg>
<span class="logo-text">{{ site?.identity.name }}</span>
</NuxtLink> </NuxtLink>
<!-- Desktop navigation --> <!-- Desktop navigation -->
<nav class="hidden md:flex items-center gap-1" aria-label="Navigation principale"> <nav class="hidden md:flex items-center gap-1" aria-label="Navigation principale">
<!-- Autonomie : prefix + axis buttons -->
<span class="nav-prefix">Autonomie&thinsp;:</span>
<NuxtLink <NuxtLink
v-for="item in site?.navigation" v-for="item in axes"
:key="item.to" :key="item.to"
:to="item.to" :to="item.to"
class="btn-ghost text-sm" class="btn-ghost text-sm"
active-class="text-white! bg-white/5" active-class="!text-[hsl(var(--color-text))] bg-[hsl(var(--color-text)/0.06)]"
> >
{{ item.label }} {{ item.label }}
</NuxtLink> </NuxtLink>
<!-- Separator + extra nav -->
<span class="nav-sep" />
<NuxtLink
v-for="item in extra"
:key="item.to"
:to="item.to"
class="btn-ghost btn-ghost--muted text-sm"
active-class="!text-[hsl(var(--color-text))] bg-[hsl(var(--color-text)/0.06)]"
>
{{ item.label }}
</NuxtLink>
<UiPaletteSelector />
</nav> </nav>
<!-- Mobile menu button --> <!-- Mobile menu button -->
@@ -31,11 +54,64 @@
</div> </div>
<!-- Mobile menu --> <!-- Mobile menu -->
<LayoutNavMobile v-model:open="isMobileMenuOpen" :nav="site?.navigation ?? []" /> <LayoutNavMobile v-model:open="isMobileMenuOpen" :nav="allNav" />
</header> </header>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: site } = await useSiteContent() const { data: site } = await useSiteContent()
const isMobileMenuOpen = ref(false) 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])
</script> </script>
<style scoped>
.logo-link {
transition: opacity 0.2s;
}
.logo-link:hover {
opacity: 0.8;
}
.logo-icon {
width: 1.6rem;
height: 2rem;
color: hsl(var(--color-primary));
flex-shrink: 0;
}
.logo-text {
font-family: var(--font-display);
font-weight: 400;
font-size: 1.25rem;
letter-spacing: 0.04em;
background-image: linear-gradient(to right, hsl(var(--color-primary)), hsl(var(--color-accent)));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-prefix {
font-family: var(--font-display);
font-size: 0.92rem;
font-weight: 500;
font-style: italic;
letter-spacing: 0.03em;
color: hsl(var(--color-text) / 0.4);
margin-right: 0.125rem;
}
.nav-sep {
display: block;
width: 1px;
height: 1.25rem;
background: hsl(var(--color-text) / 0.12);
margin: 0 0.375rem;
}
.btn-ghost--muted {
color: hsl(var(--color-text) / 0.45);
}
</style>

View File

@@ -2,41 +2,43 @@
<Transition name="player-slide"> <Transition name="player-slide">
<div <div
v-if="store.currentSong" v-if="store.currentSong"
class="player-bar fixed inset-x-0 bottom-0 z-70 border-t border-white/8 bg-surface-600/80 backdrop-blur-xl" ref="widgetRef"
class="mini-player"
> >
<!-- Expanded panel --> <!-- EXPANDED PANEL -->
<Transition name="panel-expand"> <Transition name="panel-expand">
<div v-if="store.isExpanded" class="border-b border-white/8"> <div v-if="isExpanded" class="mini-panel">
<div class="container-content grid gap-4 p-4 md:grid-cols-2"> <!-- Track info + visualizer -->
<div class="panel-top">
<div class="panel-track">
<p class="panel-title">{{ store.currentSong.title }}</p>
<p class="panel-artist">{{ store.currentSong.artist }}</p>
</div>
<div class="panel-viz">
<KeepAlive>
<PlayerVisualizer /> <PlayerVisualizer />
<PlayerPlaylist /> </KeepAlive>
</div> </div>
</div> </div>
</Transition>
<!-- Progress bar (top of player) --> <!-- Progress -->
<div class="panel-progress">
<PlayerProgress /> <PlayerProgress />
<div class="panel-times">
<!-- Main player bar --> <span>{{ store.formattedCurrentTime }}</span>
<div class="container-content flex items-center gap-4 px-4 py-2"> <span>{{ store.formattedDuration }}</span>
<!-- Track info --> </div>
<div class="flex-1 min-w-0">
<PlayerTrackInfo />
</div> </div>
<!-- Controls --> <!-- Controls -->
<div class="flex items-center gap-4"> <div class="panel-controls">
<PlayerControls /> <PlayerControls />
</div> </div>
<!-- Right section: mode + volume + expand -->
<div class="hidden md:flex items-center gap-3 flex-shrink-0">
<PlayerModeToggle />
<!-- Volume --> <!-- Volume -->
<div class="flex items-center gap-2"> <div class="panel-volume-row">
<button class="btn-ghost !p-1" @click="toggleMute"> <button class="panel-vol-btn" @click="toggleMute">
<div :class="volumeIcon" class="h-4 w-4" /> <div :class="volumeIcon" class="h-3.5 w-3.5" />
</button> </button>
<input <input
type="range" type="range"
@@ -44,39 +46,101 @@
max="1" max="1"
step="0.01" step="0.01"
:value="store.volume" :value="store.volume"
class="volume-slider w-20" class="volume-slider"
@input="handleVolumeChange" @input="handleVolumeChange"
> >
</div> </div>
<!-- Time display --> <!-- Lyrics (collapsed by default, available for standalone use) -->
<span class="font-mono text-xs text-white/40 w-24 text-center"> <div v-if="store.currentSong.lyrics && showLyrics" class="panel-lyrics">
{{ store.formattedCurrentTime }} / {{ store.formattedDuration }} <pre class="panel-lyrics-text">{{ store.currentSong.lyrics }}</pre>
</span>
<!-- Expand toggle -->
<button
class="btn-ghost !p-2"
:aria-label="store.isExpanded ? 'Réduire' : 'Développer'"
@click="store.toggleExpanded()"
>
<div :class="store.isExpanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'" class="h-4 w-4" />
</button>
</div> </div>
<button
v-if="store.currentSong.lyrics"
class="panel-lyrics-toggle"
@click="showLyrics = !showLyrics"
>
<div :class="showLyrics ? 'i-lucide-chevron-up' : 'i-lucide-text'" class="h-3 w-3" />
{{ showLyrics ? 'Masquer les paroles' : 'Paroles' }}
</button>
<!-- Playlist -->
<div class="panel-playlist">
<PlayerPlaylist />
</div>
</div>
</Transition>
<!-- COMPACT PILL -->
<div class="mini-pill" @click="toggleExpanded">
<!-- Progress ring -->
<div class="pill-ring">
<svg viewBox="0 0 36 36" class="pill-ring-svg">
<circle
cx="18" cy="18" r="16"
fill="none"
stroke="hsl(0 0% 100% / 0.06)"
stroke-width="2"
/>
<circle
cx="18" cy="18" r="16"
fill="none"
stroke="hsl(12 76% 48%)"
stroke-width="2"
stroke-linecap="round"
:stroke-dasharray="circumference"
:stroke-dashoffset="circumference - (circumference * store.progress / 100)"
class="pill-ring-progress"
/>
</svg>
</div>
<!-- Title -->
<span class="pill-title">{{ store.currentSong.title }}</span>
<!-- Play/Pause -->
<button
class="pill-play"
:aria-label="store.isPlaying ? 'Pause' : 'Lecture'"
@click.stop="togglePlayPause"
>
<div :class="store.isPlaying ? 'i-lucide-pause' : 'i-lucide-play'" class="h-4 w-4" />
</button>
<!-- Next -->
<button class="pill-next" aria-label="Suivant" @click.stop="playNext" :disabled="!store.hasNext">
<div class="i-lucide-skip-forward h-3.5 w-3.5" />
</button>
<!-- Expand -->
<button
class="pill-expand"
:aria-label="isExpanded ? 'Réduire' : 'Développer'"
@click.stop="toggleExpanded"
>
<div :class="isExpanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'" class="h-4 w-4" />
</button>
</div> </div>
</div> </div>
</Transition> </Transition>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
const store = usePlayerStore() const store = usePlayerStore()
const { setVolume } = useAudioPlayer() const { setVolume, togglePlayPause, playNext } = useAudioPlayer()
// Initialize media session
useMediaSession() useMediaSession()
useKeyboardShortcuts()
const widgetRef = ref<HTMLElement>()
const isExpanded = ref(false)
const showLyrics = ref(false)
let previousVolume = 0.8 let previousVolume = 0.8
const circumference = 2 * Math.PI * 16
const volumeIcon = computed(() => { const volumeIcon = computed(() => {
if (store.volume === 0) return 'i-lucide-volume-x' if (store.volume === 0) return 'i-lucide-volume-x'
if (store.volume < 0.3) return 'i-lucide-volume' if (store.volume < 0.3) return 'i-lucide-volume'
@@ -98,62 +162,322 @@ function toggleMute() {
setVolume(previousVolume) setVolume(previousVolume)
} }
} }
function toggleExpanded() {
isExpanded.value = !isExpanded.value
}
onClickOutside(widgetRef, () => {
if (isExpanded.value) isExpanded.value = false
})
</script> </script>
<style scoped> <style scoped>
.player-slide-enter-active, /* ═══════════════════════════════════════
.player-slide-leave-active { POSITION
transition: transform 0.3s var(--ease-out-expo); ═══════════════════════════════════════ */
.mini-player {
position: fixed;
bottom: 1rem;
right: max(1rem, calc((100vw - 80rem) / 2));
z-index: 70;
display: flex;
flex-direction: column;
align-items: flex-end;
} }
.player-slide-enter-from, /* ═══════════════════════════════════════
.player-slide-leave-to { PILL
transform: translateY(100%); ═══════════════════════════════════════ */
} .mini-pill {
display: flex;
.panel-expand-enter-active, align-items: center;
.panel-expand-leave-active { gap: 0.625rem;
padding: 0.375rem 0.5rem 0.375rem 0.5rem;
border-radius: 9999px;
background: hsl(20 8% 7% / 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
cursor: pointer;
transition: all 0.3s var(--ease-out-expo); transition: all 0.3s var(--ease-out-expo);
box-shadow: 0 4px 20px hsl(0 0% 0% / 0.35);
}
.mini-pill:hover {
background: hsl(20 8% 9% / 0.96);
}
/* Progress ring */
.pill-ring {
width: 1.75rem;
height: 1.75rem;
flex-shrink: 0;
}
.pill-ring-svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.pill-ring-progress {
transition: stroke-dashoffset 0.3s ease;
}
/* Title */
.pill-title {
font-size: 0.8rem;
font-weight: 500;
color: hsl(0 0% 100% / 0.8);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
/* Play/Pause — white circle */
.pill-play {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: white;
border: none;
color: hsl(20 8% 6%);
cursor: pointer;
transition: transform 0.15s var(--ease-out-expo);
flex-shrink: 0;
}
.pill-play:hover { transform: scale(1.08); }
.pill-play:active { transform: scale(0.94); }
/* Next */
.pill-next {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: transparent;
border: none;
color: hsl(0 0% 100% / 0.6);
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
}
.pill-next:hover { color: white; }
.pill-next:disabled { opacity: 0.3; cursor: default; }
/* Expand chevron */
.pill-expand {
display: flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
background: hsl(0 0% 100% / 0.08);
border: none;
color: hsl(0 0% 100% / 0.5);
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.pill-expand:hover { color: hsl(0 0% 100% / 0.9); background: hsl(0 0% 100% / 0.15); }
/* ═══════════════════════════════════════
PANEL
═══════════════════════════════════════ */
.mini-panel {
width: 360px;
margin-bottom: 0.5rem;
border-radius: 1rem;
background: hsl(20 8% 6% / 0.94);
backdrop-filter: blur(32px);
-webkit-backdrop-filter: blur(32px);
box-shadow: 0 8px 40px hsl(0 0% 0% / 0.4);
overflow: hidden; overflow: hidden;
} }
.panel-expand-enter-from, /* ─── Track + visualizer ─── */
.panel-expand-leave-to { .panel-top {
max-height: 0; padding: 1rem 1.25rem 0.5rem;
opacity: 0; }
.panel-track {
margin-bottom: 0.5rem;
}
.panel-title {
font-size: 0.95rem;
font-weight: 600;
color: white;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.panel-artist {
font-size: 0.75rem;
color: hsl(0 0% 100% / 0.35);
margin-top: 0.125rem;
}
.panel-viz {
opacity: 0.6;
} }
.panel-expand-enter-to, /* ─── Progress ─── */
.panel-expand-leave-from { .panel-progress {
max-height: 400px; padding: 0.5rem 1.25rem 0;
opacity: 1;
} }
.panel-times {
display: flex;
justify-content: space-between;
margin-top: 0.25rem;
font-family: var(--font-mono, monospace);
font-size: 0.625rem;
color: hsl(0 0% 100% / 0.25);
letter-spacing: 0.02em;
}
/* ─── Controls ─── */
.panel-controls {
display: flex;
justify-content: center;
padding: 0.25rem 1.25rem 0.375rem;
}
/* ─── Volume ─── */
.panel-volume-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 1.25rem 0.75rem;
}
.panel-vol-btn {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: hsl(0 0% 100% / 0.35);
cursor: pointer;
padding: 0;
transition: color 0.2s;
}
.panel-vol-btn:hover { color: hsl(0 0% 100% / 0.7); }
.volume-slider { .volume-slider {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
height: 4px; flex: 1;
background: hsl(0 0% 100% / 0.15); height: 3px;
background: hsl(0 0% 100% / 0.08);
border-radius: 2px; border-radius: 2px;
outline: none; outline: none;
} }
.volume-slider::-webkit-slider-thumb { .volume-slider::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 12px; width: 10px;
height: 12px; height: 10px;
background: white; background: hsl(0 0% 100% / 0.7);
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
transition: transform 0.15s;
} }
.volume-slider::-webkit-slider-thumb:hover { transform: scale(1.3); }
.volume-slider::-moz-range-thumb { .volume-slider::-moz-range-thumb {
width: 12px; width: 10px;
height: 12px; height: 10px;
background: white; background: hsl(0 0% 100% / 0.7);
border: none; border: none;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
} }
/* ─── Lyrics ─── */
.panel-lyrics {
max-height: 160px;
overflow-y: auto;
padding: 0.75rem 1.25rem;
border-top: 1px solid hsl(0 0% 100% / 0.04);
}
.panel-lyrics-text {
font-family: var(--font-sans, sans-serif);
font-size: 0.75rem;
line-height: 1.6;
color: hsl(0 0% 100% / 0.4);
white-space: pre-wrap;
margin: 0;
}
/* ─── Lyrics toggle ─── */
.panel-lyrics-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
width: 100%;
padding: 0.375rem;
border: none;
border-top: 1px solid hsl(0 0% 100% / 0.04);
background: none;
color: hsl(0 0% 100% / 0.25);
font-size: 0.65rem;
cursor: pointer;
transition: color 0.15s;
}
.panel-lyrics-toggle:hover {
color: hsl(0 0% 100% / 0.5);
}
/* ─── Playlist ─── */
.panel-playlist {
max-height: 200px;
overflow-y: auto;
border-top: 1px solid hsl(0 0% 100% / 0.04);
}
/* ═══════════════════════════════════════
TRANSITIONS
═══════════════════════════════════════ */
.player-slide-enter-active,
.player-slide-leave-active {
transition: transform 0.35s var(--ease-out-expo), opacity 0.35s var(--ease-out-expo);
}
.player-slide-enter-from,
.player-slide-leave-to {
transform: translateY(16px);
opacity: 0;
}
.panel-expand-enter-active,
.panel-expand-leave-active {
transition: all 0.35s var(--ease-out-expo);
overflow: hidden;
}
.panel-expand-enter-from,
.panel-expand-leave-to {
max-height: 0;
opacity: 0;
transform: translateY(8px);
}
.panel-expand-enter-to,
.panel-expand-leave-from {
max-height: 800px;
opacity: 1;
transform: translateY(0);
}
/* ═══════════════════════════════════════
MOBILE
═══════════════════════════════════════ */
@media (max-width: 768px) {
.mini-player {
right: 0.75rem;
left: 0.75rem;
align-items: stretch;
}
.mini-panel { width: auto; }
.pill-title { max-width: none; flex: 1; }
}
</style> </style>

View File

@@ -3,7 +3,7 @@
<span <span
v-for="song in songs" v-for="song in songs"
:key="song.id" :key="song.id"
class="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary/70" class="inline-flex items-center gap-1 rounded-full border border-primary/20 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary/80"
> >
<div class="i-lucide-music h-2.5 w-2.5" /> <div class="i-lucide-music h-2.5 w-2.5" />
{{ song.title }} {{ song.title }}
@@ -17,7 +17,7 @@ const props = defineProps<{
}>() }>()
const bookData = useBookData() const bookData = useBookData()
await bookData.init() bookData.init()
const songs = computed(() => bookData.getChapterSongs(props.chapterSlug)) const songs = computed(() => bookData.getChapterSongs(props.chapterSlug))
</script> </script>

View File

@@ -1,4 +1,5 @@
<template> <template>
<div class="song-item-wrapper">
<div <div
class="card-surface flex cursor-pointer items-center gap-4" class="card-surface flex cursor-pointer items-center gap-4"
:class="{ 'border-primary/40! shadow-primary/10!': isCurrent }" :class="{ 'border-primary/40! shadow-primary/10!': isCurrent }"
@@ -34,6 +35,12 @@
{{ formatDuration(song.duration) }} {{ formatDuration(song.duration) }}
</span> </span>
</div> </div>
<!-- Lyrics panel (always visible) -->
<div v-if="song.lyrics" class="lyrics-panel">
<pre class="lyrics-text">{{ song.lyrics }}</pre>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -63,3 +70,23 @@ function formatDuration(seconds: number): string {
return `${mins}:${secs.toString().padStart(2, '0')}` return `${mins}:${secs.toString().padStart(2, '0')}`
} }
</script> </script>
<style scoped>
.lyrics-panel {
margin-top: 0.25rem;
padding: 1rem 1.25rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-text) / 0.08);
max-height: 24rem;
overflow-y: auto;
}
.lyrics-text {
white-space: pre-wrap;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.7;
color: hsl(var(--color-text) / 0.6);
}
</style>

View File

@@ -1,21 +1,9 @@
<template> <template>
<div v-if="song.lyrics" class="rounded-xl bg-surface p-6"> <div v-if="song.lyrics" class="rounded-xl bg-surface p-6">
<button
class="flex w-full items-center justify-between text-left"
@click="isOpen = !isOpen"
>
<span class="font-display text-sm font-semibold text-white/70">Paroles</span> <span class="font-display text-sm font-semibold text-white/70">Paroles</span>
<div <div class="mt-4 max-h-96 overflow-y-auto">
:class="isOpen ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
class="h-4 w-4 text-white/40 transition-transform"
/>
</button>
<Transition name="lyrics-expand">
<div v-if="isOpen" class="mt-4">
<pre class="whitespace-pre-wrap font-sans text-sm leading-relaxed text-white/60">{{ song.lyrics }}</pre> <pre class="whitespace-pre-wrap font-sans text-sm leading-relaxed text-white/60">{{ song.lyrics }}</pre>
</div> </div>
</Transition>
</div> </div>
</template> </template>
@@ -25,26 +13,4 @@ import type { Song } from '~/types/song'
defineProps<{ defineProps<{
song: Song song: Song
}>() }>()
const isOpen = ref(false)
</script> </script>
<style scoped>
.lyrics-expand-enter-active,
.lyrics-expand-leave-active {
transition: all 0.3s var(--ease-out-expo);
overflow: hidden;
}
.lyrics-expand-enter-from,
.lyrics-expand-leave-to {
max-height: 0;
opacity: 0;
}
.lyrics-expand-enter-to,
.lyrics-expand-leave-from {
max-height: 500px;
opacity: 1;
}
</style>

View File

@@ -0,0 +1,261 @@
<template>
<div class="settings-selector" ref="selectorRef">
<button
class="settings-trigger"
aria-label="Réglages d'affichage"
@click="isOpen = !isOpen"
>
<div class="i-lucide-settings h-5 w-5" />
</button>
<Transition name="settings-dropdown">
<div v-if="isOpen" class="settings-dropdown">
<h4 class="settings-title">Affichage</h4>
<!-- Palette grid : 4 saisons -->
<div class="settings-section">
<span class="settings-label">Ambiance</span>
<div class="settings-palette-grid">
<button
v-for="name in paletteNames"
:key="name"
class="settings-palette-btn"
:class="{
'settings-palette-btn--active': paletteStore.currentPalette === name,
'settings-palette-btn--light': paletteStore.palettes[name].isLight,
}"
:title="paletteStore.palettes[name].label"
@click="paletteStore.setPalette(name)"
>
<span class="settings-palette-preview">
<span class="settings-palette-dot" :style="{ background: `hsl(${paletteStore.palettes[name].primary})` }" />
<span class="settings-palette-dot" :style="{ background: `hsl(${paletteStore.palettes[name].accent})` }" />
</span>
<span class="settings-palette-info">
<span class="settings-palette-name">{{ paletteStore.palettes[name].label }}</span>
<span class="settings-palette-mode">{{ paletteStore.palettes[name].isLight ? 'Clair' : 'Sombre' }}</span>
</span>
</button>
</div>
</div>
<!-- Font size -->
<div class="settings-section">
<span class="settings-label">Taille texte</span>
<div class="settings-toggle-group">
<button
v-for="size in fontSizes"
:key="size.value"
class="settings-toggle"
:class="{ 'settings-toggle--active': currentFontSize === size.value }"
@click="setFontSize(size.value)"
>
{{ size.label }}
</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import type { PaletteName } from '~/stores/palette'
const paletteStore = usePaletteStore()
const selectorRef = ref<HTMLElement>()
const isOpen = ref(false)
const paletteNames: PaletteName[] = ['automne', 'hiver', 'printemps', 'ete']
const currentFontSize = ref(
(import.meta.client && localStorage.getItem('fontSize')) || 'normal',
)
const fontSizes = [
{ label: 'A-', value: 'small' },
{ label: 'A', value: 'normal' },
{ label: 'A+', value: 'large' },
]
function setFontSize(size: string) {
currentFontSize.value = size
if (import.meta.client) {
localStorage.setItem('fontSize', size)
const root = document.documentElement
const map: Record<string, string> = { small: '14px', normal: '16px', large: '18px' }
root.style.fontSize = map[size] || '16px'
}
}
// Apply font size on mount
onMounted(() => {
const saved = localStorage.getItem('fontSize')
if (saved) setFontSize(saved)
})
onClickOutside(selectorRef, () => { isOpen.value = false })
</script>
<style scoped>
.settings-selector {
position: relative;
}
.settings-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
color: hsl(var(--color-text) / 0.7);
transition: all 0.2s;
}
.settings-trigger:hover {
color: hsl(var(--color-text));
background: hsl(var(--color-text) / 0.1);
}
.settings-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
width: 260px;
padding: 0.75rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
backdrop-filter: blur(16px);
border: 1px solid hsl(var(--color-text) / 0.08);
box-shadow: 0 8px 32px hsl(0 0% 0% / 0.3);
display: flex;
flex-direction: column;
gap: 0.75rem;
z-index: 50;
}
.settings-title {
font-family: var(--font-display);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--color-text) / 0.4);
margin: 0;
}
.settings-section {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.settings-label {
font-size: 0.7rem;
font-weight: 600;
color: hsl(var(--color-text) / 0.5);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.settings-toggle-group {
display: flex;
gap: 0.25rem;
background: hsl(var(--color-text) / 0.04);
border-radius: 0.5rem;
padding: 0.125rem;
}
.settings-toggle {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--color-text) / 0.5);
transition: all 0.15s;
}
.settings-toggle:hover {
color: hsl(var(--color-text) / 0.8);
}
.settings-toggle--active {
background: hsl(var(--color-primary));
color: white;
box-shadow: 0 1px 4px hsl(var(--color-primary) / 0.3);
}
.settings-palette-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.375rem;
}
.settings-palette-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 0.5rem;
transition: all 0.2s;
color: hsl(var(--color-text) / 0.6);
background: hsl(var(--color-text) / 0.02);
}
.settings-palette-btn:hover {
background: hsl(var(--color-text) / 0.06);
color: hsl(var(--color-text) / 0.9);
}
.settings-palette-btn--active {
background: hsl(var(--color-text) / 0.1);
color: hsl(var(--color-text));
box-shadow: inset 0 0 0 1.5px hsl(var(--color-primary) / 0.5);
}
.settings-palette-preview {
display: flex;
gap: 0.125rem;
flex-shrink: 0;
}
.settings-palette-dot {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
box-shadow: 0 1px 3px hsl(0 0% 0% / 0.25);
}
.settings-palette-info {
display: flex;
flex-direction: column;
gap: 0;
min-width: 0;
}
.settings-palette-name {
font-size: 0.75rem;
font-weight: 600;
line-height: 1.2;
}
.settings-palette-mode {
font-size: 0.6rem;
opacity: 0.5;
line-height: 1.2;
}
.settings-dropdown-enter-active {
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.settings-dropdown-leave-active {
transition: all 0.15s ease;
}
.settings-dropdown-enter-from,
.settings-dropdown-leave-to {
opacity: 0;
transform: translateY(-4px) scale(0.96);
}
</style>

View File

@@ -124,15 +124,9 @@ export function useAudioPlayer() {
function playPrev() { function playPrev() {
const song = store.prevSong() const song = store.prevSong()
if (song) { if (song) {
if (song === store.currentSong && store.currentTime <= 3) {
// prevSong already reset time
seek(0)
}
else {
loadAndPlay(song) loadAndPlay(song)
} }
} }
}
// Watch volume changes from store // Watch volume changes from store
watch(() => store.volume, (vol) => { watch(() => store.volume, (vol) => {

View File

@@ -1,23 +1,23 @@
import yaml from 'yaml'
import type { Song } from '~/types/song' import type { Song } from '~/types/song'
import type { ChapterSongLink, BookConfig } from '~/types/book' import type { ChapterSongLink, ChapterPageLink, BookConfig } from '~/types/book'
let _configCache: BookConfig | null = null let _configCache: BookConfig | null = null
async function loadConfig(): Promise<BookConfig> { async function loadConfig(): Promise<BookConfig> {
if (_configCache) return _configCache if (_configCache) return _configCache
const raw = await import('~/data/librodrome.config.yml?raw').then(m => m.default) const parsed = await $fetch<any>('/api/content/config')
const parsed = yaml.parse(raw)
_configCache = { _configCache = {
title: parsed.book.title, title: parsed.book.title,
author: parsed.book.author, author: parsed.book.author,
description: parsed.book.description, description: parsed.book.description,
coverImage: parsed.book.coverImage, coverImage: parsed.book.coverImage,
pdfFile: parsed.book.pdfFile,
chapters: [], chapters: [],
songs: parsed.songs as Song[], songs: parsed.songs as Song[],
chapterSongs: parsed.chapterSongs as ChapterSongLink[], chapterSongs: parsed.chapterSongs as ChapterSongLink[],
chapterPages: (parsed.chapterPages ?? []) as ChapterPageLink[],
defaultPlaylistOrder: parsed.defaultPlaylistOrder as string[], defaultPlaylistOrder: parsed.defaultPlaylistOrder as string[],
} }
@@ -70,6 +70,22 @@ export function useBookData() {
.filter((s): s is Song => !!s) .filter((s): s is Song => !!s)
} }
function getChapterForSong(songId: string): string | undefined {
if (!config.value) return undefined
const link = config.value.chapterSongs.find(
cs => cs.songId === songId && cs.primary,
)
return link?.chapterSlug
}
function getChapterPage(chapterSlug: string): number | undefined {
return config.value?.chapterPages.find(cp => cp.chapterSlug === chapterSlug)?.page
}
function getPdfUrl(): string {
return config.value?.pdfFile || '/pdf/une-economie-du-don.pdf'
}
function getBookMeta() { function getBookMeta() {
if (!config.value) return null if (!config.value) return null
return { return {
@@ -89,7 +105,10 @@ export function useBookData() {
getChapterSongs, getChapterSongs,
getPrimarySong, getPrimarySong,
getChapterSongLinks, getChapterSongLinks,
getChapterForSong,
getPlaylistOrder, getPlaylistOrder,
getBookMeta, getBookMeta,
getChapterPage,
getPdfUrl,
} }
} }

View File

@@ -1,16 +1,11 @@
export function useGrateWizard() { export function useGrateWizard() {
const appConfig = useAppConfig() const appConfig = useAppConfig()
const { url } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } }
function launch() { function launch(e?: Event) {
const { url, popup } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } } window.open(url, '_blank', 'noopener,noreferrer')
const left = Math.round((window.screen.width - popup.width) / 2) e?.preventDefault()
const top = Math.round((window.screen.height - popup.height) / 2)
window.open(
url,
'GrateWizard',
`width=${popup.width},height=${popup.height},left=${left},top=${top},scrollbars=yes,resizable=yes`,
)
} }
return { launch } return { url, launch }
} }

View File

@@ -0,0 +1,41 @@
export function useKeyboardShortcuts() {
const store = usePlayerStore()
const { togglePlayPause, playNext, playPrev } = useAudioPlayer()
function isInputFocused(): boolean {
const el = document.activeElement
if (!el) return false
const tag = el.tagName.toLowerCase()
if (tag === 'input' || tag === 'textarea' || tag === 'select') return true
if ((el as HTMLElement).isContentEditable) return true
return false
}
function onKeyDown(e: KeyboardEvent) {
if (isInputFocused()) return
if (!store.currentSong) return
switch (e.code) {
case 'Space':
e.preventDefault()
togglePlayPause()
break
case 'ArrowRight':
e.preventDefault()
playNext()
break
case 'ArrowLeft':
e.preventDefault()
playPrev()
break
}
}
onMounted(() => {
window.addEventListener('keydown', onKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown)
})
}

View File

@@ -0,0 +1,20 @@
/**
* Umami analytics wrapper — safe server-side, no-op when not configured.
* Usage: const { track } = useTracking()
* track('player:open')
* track('axis:navigate', { axis: 'numerique' })
*/
export function useTracking() {
const runtimeConfig = useRuntimeConfig()
const enabled = !!runtimeConfig.public.umamiWebsiteId
function track(event: string, data?: Record<string, unknown>) {
if (!import.meta.client || !enabled) return
const umami = (window as Record<string, unknown>).umami as
| { track: (event: string, data?: unknown) => void }
| undefined
umami?.track(event, data)
}
return { track, enabled }
}

View File

@@ -1,180 +0,0 @@
book:
title: "Une économie du don — enfin concevable"
author: "Yvv"
description: "Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don."
coverImage: "/images/book-cover.jpg"
license: "CC-BY-NC"
isbn: "979-1-042-45206-3"
songs:
- id: chanson-01
title: "1. Ce livre est une façon"
artist: Yvv
file: /audio/chanson-01.mp3
duration: 718
lyrics: ""
tags: [introduction, livre, don]
- id: chanson-02
title: "2. Un don qui se mesure"
artist: Yvv
file: /audio/chanson-02.mp3
duration: 589
lyrics: ""
tags: [don, mesure, valeur]
- id: chanson-03
title: "3. Les asymétries"
artist: Yvv
file: /audio/chanson-03.mp3
duration: 727
lyrics: ""
tags: [asymétrie, communauté, philosophie]
- id: chanson-04
title: "4. Inverser les flux"
artist: Yvv
file: /audio/chanson-04.mp3
duration: 610
lyrics: ""
tags: [flux, économie, production]
- id: chanson-05
title: "5. Ainsi soit-il"
artist: Yvv
file: /audio/chanson-05.mp3
duration: 545
lyrics: ""
tags: [action, engagement, avenir]
- id: chanson-06
title: "6. La croissance, une option ?"
artist: Yvv
file: /audio/chanson-06.mp3
duration: 510
lyrics: ""
tags: [croissance, monnaie, questionnement]
- id: chanson-07
title: "7. Monnaie libre essence"
artist: Yvv
file: /audio/chanson-07.mp3
duration: 475
lyrics: ""
tags: [monnaie libre, TRM, June]
- id: chanson-08
title: "8. Des cercles qui se croisent"
artist: Yvv
file: /audio/chanson-08.mp3
duration: 496
lyrics: ""
tags: [échange, réseau, cercles]
- id: chanson-09
title: "9. Coder la liberté"
artist: Yvv
file: /audio/chanson-09.mp3
duration: 376
lyrics: ""
tags: [logiciel libre, code, liberté]
chapterSongs:
# Chapitre 1 — Introduction
- chapterSlug: introduction
songId: chanson-01
primary: true
- chapterSlug: introduction
songId: chanson-02
primary: false
# Chapitre 2 — De quel don parlons-nous ?
- chapterSlug: de-quel-don-parlons-nous
songId: chanson-03
primary: true
- chapterSlug: de-quel-don-parlons-nous
songId: chanson-01
primary: false
# Chapitre 3 — La mesure du don
- chapterSlug: la-mesure-du-don
songId: chanson-02
primary: true
- chapterSlug: la-mesure-du-don
songId: chanson-03
primary: false
# Chapitre 4 — Raison d'être d'une monnaie
- chapterSlug: raison-d-etre-d-une-monnaie
songId: chanson-06
primary: true
- chapterSlug: raison-d-etre-d-une-monnaie
songId: chanson-07
primary: false
# Chapitre 5 — La TRM
- chapterSlug: la-trm
songId: chanson-07
primary: true
- chapterSlug: la-trm
songId: chanson-06
primary: false
# Chapitre 6 — Créer une économie ?
- chapterSlug: creer-une-economie
songId: chanson-04
primary: true
- chapterSlug: creer-une-economie
songId: chanson-07
primary: false
# Chapitre 7 — Échanger
- chapterSlug: echanger
songId: chanson-08
primary: true
- chapterSlug: echanger
songId: chanson-04
primary: false
# Chapitre 8 — Relation institutionnelle
- chapterSlug: relation-institutionnelle
songId: chanson-05
primary: false
- chapterSlug: relation-institutionnelle
songId: chanson-08
primary: false
# Chapitre 9 — Autres greffes
- chapterSlug: autres-greffes
songId: chanson-04
primary: false
- chapterSlug: autres-greffes
songId: chanson-08
primary: false
# Chapitre 10 — Et maintenant ?
- chapterSlug: et-maintenant
songId: chanson-05
primary: true
- chapterSlug: et-maintenant
songId: chanson-09
primary: false
# Chapitre 11 — Annexes
- chapterSlug: annexes
songId: chanson-09
primary: true
- chapterSlug: annexes
songId: chanson-07
primary: false
defaultPlaylistOrder:
- chanson-01
- chanson-02
- chanson-03
- chanson-04
- chanson-05
- chanson-06
- chanson-07
- chanson-08
- chanson-09

View File

@@ -1,8 +1,27 @@
<template> <template>
<div class="app-layout grid grid-cols-1 min-h-dvh"> <div class="app-layout grid grid-cols-1 min-h-dvh">
<LayoutTheHeader /> <LayoutTheHeader />
<main class="pb-[var(--player-height)]"> <main class="app-main">
<slot /> <slot />
<!-- (Increase, #42) sceau hexagramme -->
<svg class="app-seal" viewBox="0 0 130 100" fill="currentColor" aria-hidden="true">
<!-- Line 6 (top) yang -->
<rect x="5" y="5" width="120" height="5" rx="1"/>
<!-- Line 5 yang -->
<rect x="5" y="22" width="120" height="5" rx="1"/>
<!-- Line 4 yin -->
<rect x="5" y="39" width="49" height="5" rx="1"/>
<rect x="76" y="39" width="49" height="5" rx="1"/>
<!-- Line 3 yin -->
<rect x="5" y="56" width="49" height="5" rx="1"/>
<rect x="76" y="56" width="49" height="5" rx="1"/>
<!-- Line 2 yin -->
<rect x="5" y="73" width="49" height="5" rx="1"/>
<rect x="76" y="73" width="49" height="5" rx="1"/>
<!-- Line 1 (bottom) yang -->
<rect x="5" y="90" width="120" height="5" rx="1"/>
</svg>
</main> </main>
<LayoutTheFooter /> <LayoutTheFooter />
</div> </div>
@@ -12,4 +31,15 @@
.app-layout { .app-layout {
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
} }
/* === Seal — 益 Yì (Increase) === */
.app-seal {
display: block;
width: 44px;
margin: 2rem 1.5rem 1rem auto;
color: hsl(var(--color-accent));
opacity: 0.28;
filter: drop-shadow(1px 1px 0.5px rgba(0,0,0,0.25))
drop-shadow(-0.5px -0.5px 0.5px rgba(255,255,255,0.15));
}
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="app-layout grid grid-cols-1 min-h-dvh"> <div class="app-layout grid grid-cols-1 min-h-dvh">
<LayoutTheHeader /> <LayoutTheHeader />
<div class="reading-layout pb-[var(--player-height)]"> <div class="reading-layout">
<aside class="chapter-sidebar hidden lg:block"> <aside class="chapter-sidebar hidden lg:block">
<BookChapterNav /> <BookChapterNav />
</aside> </aside>
@@ -35,7 +35,7 @@
top: var(--header-height); top: var(--header-height);
height: calc(100dvh - var(--header-height)); height: calc(100dvh - var(--header-height));
overflow-y: auto; overflow-y: auto;
border-right: 1px solid hsl(0 0% 100% / 0.08); border-right: 1px solid hsl(var(--color-text) / 0.08);
padding: 1.5rem; padding: 1.5rem;
} }

View File

@@ -1,5 +1,34 @@
<template> <template>
<div class="section-padding"> <div class="relative overflow-hidden section-padding">
<!-- Shadok philosopher: character sitting cross-legged, floating -->
<svg class="shadok-philosopher" viewBox="0 0 220 280" fill="none" aria-hidden="true">
<!-- Body (round, serene) -->
<ellipse cx="110" cy="150" rx="48" ry="55" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<ellipse cx="110" cy="82" rx="26" ry="25" fill="currentColor" opacity="0.8"/>
<!-- Closed eyes (meditating) -->
<path d="M96 80 Q100 84 105 80" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M115 80 Q119 84 124 80" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Serene smile -->
<path d="M102 93 Q110 98 118 93" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Arms resting on knees -->
<path d="M64 155 Q55 180 70 200" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M156 155 Q165 180 150 200" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<!-- Hands on knees -->
<circle cx="70" cy="200" r="5" fill="currentColor" opacity="0.4"/>
<circle cx="150" cy="200" r="5" fill="currentColor" opacity="0.4"/>
<!-- Crossed legs -->
<path d="M80 205 Q90 230 120 235" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<path d="M140 205 Q130 230 100 235" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<!-- Floating aura lines -->
<path d="M50 245 Q110 260 170 245" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 4" fill="none" opacity="0.25"/>
<path d="M60 255 Q110 268 160 255" stroke="currentColor" stroke-width="1" stroke-dasharray="3 5" fill="none" opacity="0.18"/>
<!-- Small sparkles around -->
<circle cx="55" cy="110" r="2.5" fill="currentColor" opacity="0.2"/>
<circle cx="170" cy="95" r="2" fill="currentColor" opacity="0.18"/>
<circle cx="160" cy="130" r="3" fill="currentColor" opacity="0.15"/>
</svg>
<div class="container-content mx-auto max-w-3xl"> <div class="container-content mx-auto max-w-3xl">
<ContentRenderer v-if="page" :value="page" class="prose" /> <ContentRenderer v-if="page" :value="page" class="prose" />
</div> </div>
@@ -19,3 +48,25 @@ const { data: page } = await useAsyncData('about', () =>
queryCollection('pages').path('/pages/about').first(), queryCollection('pages').path('/pages/about').first(),
) )
</script> </script>
<style scoped>
.shadok-philosopher {
position: absolute;
right: 3%;
bottom: 6%;
width: clamp(120px, 15vw, 220px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-philosopher 11s ease-in-out infinite;
}
@keyframes shadok-float-philosopher {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@media (max-width: 768px) {
.shadok-philosopher { display: none; }
}
</style>

View File

@@ -6,24 +6,66 @@
Chapitres Chapitres
</NuxtLink> </NuxtLink>
<h1 class="font-display text-2xl font-bold text-white mt-1"> <h1 class="font-display text-2xl font-bold text-white mt-1">
{{ chapter?.slug }} {{ chapterTitle || slug }}
</h1> </h1>
<span class="text-xs text-white/30 font-mono">{{ slug }}</span>
</div> </div>
<div class="flex items-center gap-3">
<span v-if="wordCount" class="text-xs text-white/30">{{ wordCount }} mots</span>
<AdminSaveButton :saving="saving" :saved="saved" @save="save" /> <AdminSaveButton :saving="saving" :saved="saved" @save="save" />
</div> </div>
</div>
<template v-if="chapter"> <template v-if="chapter">
<AdminFormSection title="Frontmatter" open> <AdminFormSection title="Métadonnées">
<textarea <div class="grid gap-3 sm:grid-cols-2">
v-model="frontmatter" <div>
class="fm-textarea" <label class="field-label">Titre</label>
rows="6" <input v-model="title" class="field-input" placeholder="Titre du chapitre" />
spellcheck="false" </div>
/> <div>
<label class="field-label">Temps de lecture</label>
<input v-model="readingTime" class="field-input" placeholder="15 min" />
</div>
<div class="sm:col-span-2">
<label class="field-label">Description</label>
<input v-model="description" class="field-input" placeholder="Description courte pour le SEO" />
</div>
</div>
</AdminFormSection> </AdminFormSection>
<AdminFormSection title="Contenu Markdown" open> <AdminFormSection title="Morceaux associés">
<AdminMarkdownEditor v-model="body" :rows="30" /> <p class="text-xs text-white/40 mb-3">
Cliquez pour associer/dissocier. Cliquez sur l'étoile pour définir le morceau principal.
</p>
<div class="flex flex-wrap gap-2">
<div
v-for="song in allSongs"
:key="song.id"
class="song-tag"
:class="{
'song-tag--active': isLinked(song.id),
'song-tag--primary': isPrimary(song.id),
}"
>
<button
v-if="isLinked(song.id)"
class="song-star"
:class="{ 'song-star--active': isPrimary(song.id) }"
@click="setPrimary(song.id)"
aria-label="Définir comme principal"
>
<div class="i-lucide-star h-3 w-3" />
</button>
<button class="song-tag-label" @click="toggleSong(song.id)">
{{ song.title }}
</button>
</div>
</div>
</AdminFormSection>
<AdminFormSection title="Contenu" open>
<AdminMarkdownEditor v-model="body" :rows="35" />
</AdminFormSection> </AdminFormSection>
</template> </template>
</div> </div>
@@ -39,17 +81,80 @@ const route = useRoute()
const slug = computed(() => route.params.slug as string) const slug = computed(() => route.params.slug as string)
const { data: chapter } = await useFetch(() => `/api/admin/chapters/${slug.value}`) const { data: chapter } = await useFetch(() => `/api/admin/chapters/${slug.value}`)
const { data: bookConfig } = await useFetch<any>('/api/content/config')
const frontmatter = ref('') const title = ref('')
const description = ref('')
const readingTime = ref('')
const body = ref('') const body = ref('')
const chapterTitle = computed(() => title.value)
const wordCount = computed(() => {
if (!body.value) return 0
return body.value.trim().split(/\s+/).filter(Boolean).length
})
watch(chapter, (val) => { watch(chapter, (val) => {
if (val) { if (val) {
frontmatter.value = val.frontmatter ?? '' const fm = val.frontmatter ?? ''
title.value = extractFmField(fm, 'title')
description.value = extractFmField(fm, 'description')
readingTime.value = extractFmField(fm, 'readingTime')
body.value = val.body ?? '' body.value = val.body ?? ''
} }
}, { immediate: true }) }, { immediate: true })
function extractFmField(fm: string, field: string): string {
const match = fm.match(new RegExp(`^${field}:\\s*"?([^"\\n]*)"?`, 'm'))
return match ? match[1].trim() : ''
}
// ── Morceaux associés ──
const allSongs = computed(() => bookConfig.value?.songs ?? [])
const linkedSongIds = ref<Set<string>>(new Set())
const primarySongId = ref<string | null>(null)
watch(bookConfig, (val) => {
if (!val) return
const links = (val.chapterSongs ?? []).filter(
(cs: any) => cs.chapterSlug === slug.value,
)
linkedSongIds.value = new Set(links.map((l: any) => l.songId))
const primary = links.find((l: any) => l.primary)
primarySongId.value = primary?.songId ?? null
}, { immediate: true })
function isLinked(songId: string) {
return linkedSongIds.value.has(songId)
}
function isPrimary(songId: string) {
return primarySongId.value === songId
}
function toggleSong(songId: string) {
const next = new Set(linkedSongIds.value)
if (next.has(songId)) {
next.delete(songId)
if (primarySongId.value === songId) primarySongId.value = null
}
else {
next.add(songId)
if (!primarySongId.value) primarySongId.value = songId
}
linkedSongIds.value = next
}
function setPrimary(songId: string) {
if (!linkedSongIds.value.has(songId)) {
const next = new Set(linkedSongIds.value)
next.add(songId)
linkedSongIds.value = next
}
primarySongId.value = songId
}
// ── Save ──
const saving = ref(false) const saving = ref(false)
const saved = ref(false) const saved = ref(false)
@@ -57,13 +162,40 @@ async function save() {
saving.value = true saving.value = true
saved.value = false saved.value = false
try { try {
// 1. Sauvegarder le contenu du chapitre
const order = chapter.value?.frontmatter?.match(/order:\s*(\d+)/)?.[1] ?? '1'
const frontmatter = [
`title: "${title.value}"`,
`description: "${description.value}"`,
`order: ${order}`,
`readingTime: "${readingTime.value}"`,
].join('\n')
await $fetch(`/api/admin/chapters/${slug.value}`, { await $fetch(`/api/admin/chapters/${slug.value}`, {
method: 'PUT', method: 'PUT',
body: { body: { frontmatter, body: body.value },
frontmatter: frontmatter.value,
body: body.value,
},
}) })
// 2. Sauvegarder les liaisons morceaux dans la config
if (bookConfig.value) {
const otherLinks = (bookConfig.value.chapterSongs ?? []).filter(
(cs: any) => cs.chapterSlug !== slug.value,
)
const newLinks = [...linkedSongIds.value].map(songId => ({
chapterSlug: slug.value,
songId,
primary: songId === primarySongId.value,
}))
const updatedConfig = {
...bookConfig.value,
chapterSongs: [...otherLinks, ...newLinks],
}
await $fetch('/api/admin/content/config', {
method: 'PUT',
body: updatedConfig,
})
}
saved.value = true saved.value = true
setTimeout(() => { saved.value = false }, 2000) setTimeout(() => { saved.value = false }, 2000)
} }
@@ -74,21 +206,91 @@ async function save() {
</script> </script>
<style scoped> <style scoped>
.fm-textarea { .field-label {
width: 100%; display: block;
padding: 0.75rem; font-size: 0.75rem;
border: 1px solid hsl(20 8% 18%); color: hsl(var(--color-text-muted));
border-radius: 0.5rem; margin-bottom: 0.25rem;
background: hsl(20 8% 4%);
color: hsl(36 80% 76%);
font-family: var(--font-mono, monospace);
font-size: 0.85rem;
line-height: 1.7;
resize: vertical;
} }
.fm-textarea:focus { .field-input {
width: 100%;
padding: 0.5rem 0.625rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--color-surface-light));
background: hsl(var(--color-bg));
color: hsl(var(--color-text));
font-size: 0.85rem;
}
.field-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
}
/* ── Song tags ── */
.song-tag {
display: inline-flex;
align-items: center;
border-radius: 9999px;
border: 1px solid hsl(var(--color-surface-light));
transition: all 0.15s;
overflow: hidden;
}
.song-tag:hover {
border-color: hsl(var(--color-primary) / 0.4);
}
.song-tag--active {
border-color: hsl(var(--color-primary) / 0.6);
background: hsl(var(--color-primary) / 0.08);
}
.song-tag--primary {
border-color: hsl(var(--color-accent));
background: hsl(var(--color-accent) / 0.08);
}
.song-tag-label {
padding: 0.375rem 0.75rem;
background: none;
border: none;
color: hsl(var(--color-text-muted));
font-size: 0.8rem;
cursor: pointer;
transition: color 0.15s;
}
.song-tag--active .song-tag-label {
color: hsl(var(--color-primary));
}
.song-tag--primary .song-tag-label {
color: hsl(var(--color-accent));
}
.song-tag-label:hover {
color: hsl(var(--color-text));
}
.song-star {
display: flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0 0.375rem 0.625rem;
background: none;
border: none;
color: hsl(var(--color-text-muted));
cursor: pointer;
transition: color 0.15s;
}
.song-star:hover {
color: hsl(var(--color-accent));
}
.song-star--active {
color: hsl(var(--color-accent));
} }
</style> </style>

View File

@@ -1,20 +1,107 @@
<template> <template>
<div> <div>
<h1 class="font-display text-2xl font-bold text-white mb-6">Chapitres</h1> <div class="flex items-center justify-between mb-6">
<h1 class="font-display text-2xl font-bold text-white">Livre &amp; chapitres</h1>
<AdminSaveButton :saving="saving" :saved="saved" @save="saveOrder" />
</div>
<!-- PDF config -->
<div class="pdf-section">
<h2 class="font-display text-sm font-semibold text-white/60 mb-3">PDF du livre</h2>
<div class="flex items-end gap-3">
<div class="flex-1">
<label class="block text-xs text-white/40 mb-1">Chemin du fichier PDF</label>
<input
v-model="pdfPath"
class="admin-input w-full font-mono text-xs"
placeholder="/pdf/une-economie-du-don.pdf"
/>
</div>
<button class="save-pdf-btn" @click="savePdfConfig" :disabled="savingPdf">
<div v-if="savingPdf" class="i-lucide-loader-2 h-3.5 w-3.5 animate-spin" />
<div v-else-if="savedPdf" class="i-lucide-check h-3.5 w-3.5" />
<div v-else class="i-lucide-save h-3.5 w-3.5" />
{{ savedPdf ? 'Enregistré' : 'Enregistrer' }}
</button>
</div>
<p class="text-xs text-white/30 mt-2">
Uploadez le PDF via Médias, puis renseignez le chemin ici.
</p>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<NuxtLink <div
v-for="chapter in chapters" v-for="(chapter, i) in chapters"
:key="chapter.slug" :key="chapter.slug"
:to="`/admin/book/${chapter.slug}`"
class="chapter-item" class="chapter-item"
draggable="true"
@dragstart="onDragStart(i, $event)"
@dragover.prevent="onDragOver(i)"
@dragend="onDragEnd"
:class="{ 'chapter-item--dragging': dragIdx === i, 'chapter-item--over': dropIdx === i && dropIdx !== dragIdx }"
> >
<span class="chapter-order">{{ String(chapter.order ?? 0).padStart(2, '0') }}</span> <div class="drag-handle" aria-label="Réordonner">
<span class="chapter-title">{{ chapter.title }}</span> <div class="i-lucide-grip-vertical h-4 w-4" />
</div>
<span class="chapter-order">{{ String(i + 1).padStart(2, '0') }}</span>
<div class="chapter-info">
<NuxtLink
:to="`/admin/book/${chapter.slug}`"
class="chapter-title"
>
{{ chapter.title }}
</NuxtLink>
<div v-if="getChapterSongNames(chapter.slug).length" class="chapter-songs">
<span
v-for="name in getChapterSongNames(chapter.slug)"
:key="name"
class="song-badge"
>{{ name }}</span>
</div>
</div>
<select
v-if="pdfOutline.length"
class="page-select"
:value="getChapterPage(chapter.slug) ?? ''"
@change="setChapterPage(chapter.slug, ($event.target as HTMLSelectElement).value)"
>
<option value=""> Page PDF </option>
<option
v-for="entry in pdfOutline"
:key="`${entry.page}-${entry.title}`"
:value="entry.page"
>{{ '\u00A0\u00A0'.repeat(entry.level) }}{{ entry.title }} (p.{{ entry.page }})</option>
</select>
<span v-else class="text-xs text-white/30"></span>
<button
class="delete-btn"
@click="removeChapter(chapter.slug)"
aria-label="Supprimer"
>
<div class="i-lucide-trash-2 h-4 w-4" />
</button>
<NuxtLink :to="`/admin/book/${chapter.slug}`">
<div class="i-lucide-chevron-right h-4 w-4 text-white/20" /> <div class="i-lucide-chevron-right h-4 w-4 text-white/20" />
</NuxtLink> </NuxtLink>
</div> </div>
</div> </div>
<!-- Add chapter -->
<div class="mt-6 flex items-end gap-3">
<div class="flex-1">
<label class="block text-xs text-white/40 mb-1">Titre</label>
<input v-model="newTitle" class="admin-input w-full" placeholder="Nouveau chapitre" />
</div>
<div>
<label class="block text-xs text-white/40 mb-1">Slug</label>
<input v-model="newSlug" class="admin-input w-full font-mono text-xs" placeholder="12-slug" />
</div>
<button class="add-btn" @click="addChapter" :disabled="!newTitle || !newSlug">
<div class="i-lucide-plus h-4 w-4" />
Ajouter
</button>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -23,37 +110,317 @@ definePageMeta({
middleware: 'admin', middleware: 'admin',
}) })
const { data: chapters } = await useFetch('/api/admin/chapters') const { data: chapters, refresh } = await useFetch<any[]>('/api/admin/chapters')
const { data: bookConfig } = await useFetch<any>('/api/content/config')
function getChapterSongNames(chapterSlug: string): string[] {
if (!bookConfig.value) return []
const links = (bookConfig.value.chapterSongs ?? []).filter(
(cs: any) => cs.chapterSlug === chapterSlug,
)
return links.map((link: any) => {
const song = bookConfig.value.songs.find((s: any) => s.id === link.songId)
return song?.title ?? link.songId
})
}
const saving = ref(false)
const saved = ref(false)
const newTitle = ref('')
const newSlug = ref('')
// PDF path + outline
const pdfPath = ref(bookConfig.value?.book?.pdfFile ?? '/pdf/une-economie-du-don.pdf')
const savingPdf = ref(false)
const savedPdf = ref(false)
const pdfOutline = ref<Array<{ title: string; page: number; level: number }>>([])
const chapterPageMap = ref<Record<string, number | undefined>>({})
if (bookConfig.value?.chapterPages) {
for (const cp of bookConfig.value.chapterPages) {
chapterPageMap.value[cp.chapterSlug] = cp.page
}
}
function getChapterPage(slug: string): number | undefined {
return chapterPageMap.value[slug]
}
function setChapterPage(slug: string, val: string) {
const num = parseInt(val, 10)
if (num > 0) chapterPageMap.value[slug] = num
else delete chapterPageMap.value[slug]
}
// Charger le sommaire PDF côté client via l'API serveur
onMounted(() => {
$fetch<Array<{ title: string; page: number; level: number }>>('/api/admin/pdf-outline')
.then((data) => { pdfOutline.value = data })
.catch((err) => { console.warn('PDF outline load failed:', err) })
})
async function savePdfConfig() {
if (!bookConfig.value) return
savingPdf.value = true
savedPdf.value = false
try {
bookConfig.value.book.pdfFile = pdfPath.value
bookConfig.value.chapterPages = Object.entries(chapterPageMap.value)
.filter(([, page]) => page != null)
.map(([chapterSlug, page]) => ({ chapterSlug, page }))
await $fetch('/api/admin/content/config', {
method: 'PUT',
body: bookConfig.value,
})
savedPdf.value = true
setTimeout(() => { savedPdf.value = false }, 2000)
} finally {
savingPdf.value = false
}
}
// Drag & drop state
const dragIdx = ref<number | null>(null)
const dropIdx = ref<number | null>(null)
function onDragStart(i: number, e: DragEvent) {
dragIdx.value = i
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'
}
}
function onDragOver(i: number) {
dropIdx.value = i
}
function onDragEnd() {
if (dragIdx.value !== null && dropIdx.value !== null && dragIdx.value !== dropIdx.value && chapters.value) {
const [moved] = chapters.value.splice(dragIdx.value, 1)
chapters.value.splice(dropIdx.value, 0, moved)
}
dragIdx.value = null
dropIdx.value = null
}
async function saveOrder() {
if (!chapters.value) return
saving.value = true
saved.value = false
try {
const orderedChapters = chapters.value.map((ch: any, i: number) => ({
slug: ch.slug,
order: i + 1,
}))
await $fetch('/api/admin/chapters', {
method: 'PUT',
body: { chapters: orderedChapters },
})
saved.value = true
setTimeout(() => { saved.value = false }, 2000)
}
finally {
saving.value = false
}
}
async function addChapter() {
if (!newTitle.value || !newSlug.value) return
const order = (chapters.value?.length ?? 0) + 1
await $fetch('/api/admin/chapters', {
method: 'POST',
body: { slug: newSlug.value, title: newTitle.value, order },
})
newTitle.value = ''
newSlug.value = ''
await refresh()
}
async function removeChapter(slug: string) {
if (!confirm(`Supprimer le chapitre "${slug}" ?`)) return
await $fetch(`/api/admin/chapters/${slug}`, { method: 'DELETE' })
await refresh()
}
</script> </script>
<style scoped> <style scoped>
.pdf-section {
margin-bottom: 1.5rem;
padding: 1rem;
border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem;
background: hsl(var(--color-bg));
}
.save-pdf-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-surface-light));
background: none;
color: hsl(var(--color-text-muted));
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.save-pdf-btn:hover:not(:disabled) {
border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-primary));
}
.save-pdf-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.chapter-item { .chapter-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
text-decoration: none;
transition: all 0.2s; transition: all 0.2s;
} }
.chapter-item:hover { .chapter-item:hover {
border-color: hsl(12 76% 48% / 0.3); border-color: hsl(var(--color-primary) / 0.3);
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
}
.chapter-item--dragging {
opacity: 0.4;
}
.chapter-item--over {
border-top: 2px solid hsl(var(--color-primary));
}
.drag-handle {
cursor: grab;
padding: 0.25rem;
color: hsl(var(--color-text-muted));
flex-shrink: 0;
}
.drag-handle:active {
cursor: grabbing;
} }
.chapter-order { .chapter-order {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.85rem; font-size: 0.85rem;
color: hsl(12 76% 48% / 0.5); color: hsl(var(--color-primary) / 0.5);
font-weight: 600; font-weight: 600;
width: 1.75rem; width: 1.75rem;
} }
.chapter-title { .chapter-info {
flex: 1; flex: 1;
color: white; min-width: 0;
}
.chapter-title {
display: block;
color: hsl(var(--color-text));
font-weight: 500; font-weight: 500;
text-decoration: none;
}
.chapter-title:hover {
color: hsl(var(--color-primary));
}
.chapter-songs {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: 0.25rem;
}
.song-badge {
font-size: 0.65rem;
padding: 0.1rem 0.5rem;
border-radius: 9999px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
border: 1px solid hsl(var(--color-primary) / 0.2);
}
.page-select {
flex-shrink: 0;
max-width: 14rem;
padding: 0.25rem 0.4rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--color-surface-light));
background: hsl(var(--color-bg));
color: hsl(var(--color-text-muted));
font-size: 0.7rem;
cursor: pointer;
}
.page-select:focus {
outline: none;
border-color: hsl(var(--color-primary) / 0.5);
}
.delete-btn {
flex-shrink: 0;
padding: 0.375rem;
border-radius: 0.375rem;
color: hsl(0 60% 50%);
background: none;
border: none;
cursor: pointer;
transition: background 0.15s;
}
.delete-btn:hover {
background: hsl(0 60% 50% / 0.1);
}
.admin-input {
padding: 0.375rem 0.5rem;
border-radius: 0.375rem;
border: 1px solid hsl(var(--color-surface-light));
background: hsl(var(--color-bg));
color: hsl(var(--color-text));
font-size: 0.8rem;
}
.admin-input:focus {
outline: none;
border-color: hsl(var(--color-primary) / 0.5);
}
.add-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-surface-light));
background: none;
color: hsl(var(--color-text-muted));
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.add-btn:hover:not(:disabled) {
border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-primary));
}
.add-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
} }
</style> </style>

View File

@@ -9,8 +9,8 @@
<p class="text-sm text-white/50">Identité, navigation, footer</p> <p class="text-sm text-white/50">Identité, navigation, footer</p>
</NuxtLink> </NuxtLink>
<NuxtLink to="/admin/pages/home" class="dash-card"> <NuxtLink to="/admin/pages" class="dash-card">
<div class="i-lucide-home h-8 w-8 text-primary mb-2" /> <div class="i-lucide-file-text h-8 w-8 text-primary mb-2" />
<h2 class="text-lg font-semibold text-white">Pages</h2> <h2 class="text-lg font-semibold text-white">Pages</h2>
<p class="text-sm text-white/50">Contenus des pages publiques</p> <p class="text-sm text-white/50">Contenus des pages publiques</p>
</NuxtLink> </NuxtLink>
@@ -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>
@@ -47,14 +53,14 @@ definePageMeta({
.dash-card { .dash-card {
padding: 1.5rem; padding: 1.5rem;
border-radius: 0.75rem; border-radius: 0.75rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
text-decoration: none; text-decoration: none;
transition: all 0.2s; transition: all 0.2s;
} }
.dash-card:hover { .dash-card:hover {
border-color: hsl(12 76% 48% / 0.3); border-color: hsl(var(--color-primary) / 0.3);
background: hsl(20 8% 8%); background: hsl(var(--color-surface));
} }
</style> </style>

View File

@@ -22,6 +22,10 @@
<div v-if="loading" class="i-lucide-loader-2 h-4 w-4 animate-spin" /> <div v-if="loading" class="i-lucide-loader-2 h-4 w-4 animate-spin" />
Se connecter Se connecter
</button> </button>
<p v-if="devHint" class="dev-hint">
Dev : <code>{{ devHint }}</code>
</p>
</form> </form>
</div> </div>
</template> </template>
@@ -34,6 +38,13 @@ definePageMeta({
const password = ref('') const password = ref('')
const error = ref('') const error = ref('')
const loading = ref(false) const loading = ref(false)
const devHint = ref('')
if (import.meta.dev) {
$fetch('/api/admin/auth/hint').then((res: any) => {
devHint.value = res.password
}).catch(() => {})
}
async function login() { async function login() {
error.value = '' error.value = ''
@@ -61,16 +72,16 @@ async function login() {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: hsl(20 8% 3.5%); background: hsl(var(--color-bg));
} }
.login-form { .login-form {
width: 100%; width: 100%;
max-width: 24rem; max-width: 24rem;
padding: 2.5rem; padding: 2.5rem;
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 1rem; border-radius: 1rem;
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
} }
.login-error { .login-error {
@@ -86,7 +97,7 @@ async function login() {
display: block; display: block;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
margin-bottom: 0.375rem; margin-bottom: 0.375rem;
} }
@@ -94,16 +105,16 @@ async function login() {
width: 100%; width: 100%;
padding: 0.625rem 0.75rem; padding: 0.625rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.login-input:focus { .login-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
.login-btn { .login-btn {
@@ -115,7 +126,7 @@ async function login() {
padding: 0.625rem; padding: 0.625rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
background: hsl(12 76% 48%); background: hsl(var(--color-primary));
color: white; color: white;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
@@ -124,11 +135,25 @@ async function login() {
} }
.login-btn:hover:not(:disabled) { .login-btn:hover:not(:disabled) {
background: hsl(12 76% 42%); background: hsl(var(--color-primary));
} }
.login-btn:disabled { .login-btn:disabled {
opacity: 0.7; opacity: 0.7;
cursor: wait; cursor: wait;
} }
.dev-hint {
margin-top: 1rem;
text-align: center;
font-size: 0.75rem;
color: hsl(var(--color-text-muted));
}
.dev-hint code {
color: hsl(var(--color-primary));
background: hsl(var(--color-surface));
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
}
</style> </style>

View File

@@ -5,7 +5,7 @@
<span class="text-sm text-white/40">{{ messages?.length || 0 }} message(s)</span> <span class="text-sm text-white/40">{{ messages?.length || 0 }} message(s)</span>
</div> </div>
<div v-if="messages?.length" class="space-y-3"> <div v-if="messages?.length" class="space-y-4">
<div <div
v-for="msg in messages" v-for="msg in messages"
:key="msg.id" :key="msg.id"
@@ -17,6 +17,9 @@
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<span class="font-semibold text-white text-sm truncate">{{ msg.author }}</span> <span class="font-semibold text-white text-sm truncate">{{ msg.author }}</span>
<span v-if="msg.email" class="text-white/30 text-xs truncate">{{ msg.email }}</span> <span v-if="msg.email" class="text-white/30 text-xs truncate">{{ msg.email }}</span>
<span class="type-badge" :class="`type-badge--${msg.type || 'reaction'}`">
{{ typeLabel(msg.type) }}
</span>
</div> </div>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<span class="text-xs text-white/30">{{ formatDate(msg.createdAt) }}</span> <span class="text-xs text-white/30">{{ formatDate(msg.createdAt) }}</span>
@@ -29,23 +32,54 @@
</div> </div>
</div> </div>
<!-- Texte éditable --> <!-- Texte du message -->
<div v-if="editing === msg.id" class="mb-3"> <div v-if="editing === msg.id" class="mb-3 space-y-2">
<input <input v-model="editForm.author" class="admin-input w-full" placeholder="Auteur" />
v-model="editForm.author" <select v-model="editForm.type" class="admin-input w-full">
class="admin-input mb-2 w-full" <option value="question">Question</option>
placeholder="Auteur" <option value="suggestion">Suggestion</option>
/> <option value="retour">Retour d'expérience</option>
<textarea </select>
v-model="editForm.text" <textarea v-model="editForm.text" class="admin-input w-full" rows="3" />
class="admin-input w-full" </div>
rows="3" <p v-else class="msg-text text-sm leading-relaxed mb-3">{{ msg.text }}</p>
/>
<!-- Réponse existante -->
<div v-if="msg.reply && replyEditing !== msg.id" class="reply-block mb-3">
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-semibold text-primary/80">Réponse publiée</span>
<span class="text-xs text-white/30">{{ formatDate(msg.reply.publishedAt) }}</span>
</div>
<p class="text-white/60 text-sm leading-relaxed">{{ msg.reply.text }}</p>
<button class="action-btn mt-2 text-xs" @click="startReply(msg)">
<div class="i-lucide-pencil h-3 w-3" />
Modifier la réponse
</button>
</div>
<!-- Formulaire réponse -->
<div v-if="replyEditing === msg.id" class="reply-form mb-3">
<textarea
v-model="replyText"
class="admin-input w-full text-sm"
rows="3"
placeholder="Votre réponse..."
/>
<div class="flex gap-2 mt-2">
<button class="action-btn action-btn--save" @click="saveReply(msg)">
<div class="i-lucide-check h-3.5 w-3.5" />
Publier la réponse
</button>
<button v-if="msg.reply" class="action-btn action-btn--danger" @click="removeReply(msg)">
<div class="i-lucide-x h-3.5 w-3.5" />
Supprimer la réponse
</button>
<button class="action-btn" @click="replyEditing = null">Annuler</button>
</div>
</div> </div>
<p v-else class="text-white/70 text-sm leading-relaxed mb-3">{{ msg.text }}</p>
<!-- Actions --> <!-- Actions -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 flex-wrap">
<button class="action-btn" @click="togglePublished(msg)"> <button class="action-btn" @click="togglePublished(msg)">
<div :class="msg.published ? 'i-lucide-eye-off' : 'i-lucide-eye'" class="h-3.5 w-3.5" /> <div :class="msg.published ? 'i-lucide-eye-off' : 'i-lucide-eye'" class="h-3.5 w-3.5" />
{{ msg.published ? 'Dépublier' : 'Publier' }} {{ msg.published ? 'Dépublier' : 'Publier' }}
@@ -56,15 +90,18 @@
<div class="i-lucide-check h-3.5 w-3.5" /> <div class="i-lucide-check h-3.5 w-3.5" />
Valider Valider
</button> </button>
<button class="action-btn" @click="editing = null"> <button class="action-btn" @click="editing = null">Annuler</button>
Annuler
</button>
</template> </template>
<button v-else class="action-btn" @click="startEdit(msg)"> <button v-else class="action-btn" @click="startEdit(msg)">
<div class="i-lucide-pencil h-3.5 w-3.5" /> <div class="i-lucide-pencil h-3.5 w-3.5" />
Modifier Modifier
</button> </button>
<button v-if="replyEditing !== msg.id && !msg.reply" class="action-btn action-btn--reply" @click="startReply(msg)">
<div class="i-lucide-reply h-3.5 w-3.5" />
Répondre
</button>
<button class="action-btn action-btn--danger ml-auto" @click="remove(msg)"> <button class="action-btn action-btn--danger ml-auto" @click="remove(msg)">
<div class="i-lucide-trash-2 h-3.5 w-3.5" /> <div class="i-lucide-trash-2 h-3.5 w-3.5" />
Supprimer Supprimer
@@ -86,18 +123,33 @@ definePageMeta({
const { data: messages, refresh } = await useFetch<any[]>('/api/admin/messages') const { data: messages, refresh } = await useFetch<any[]>('/api/admin/messages')
const editing = ref<number | null>(null) const editing = ref<number | null>(null)
const editForm = reactive({ author: '', text: '' }) const editForm = reactive({ author: '', text: '', type: 'question' })
const replyEditing = ref<number | null>(null)
const replyText = ref('')
const TYPE_LABELS: Record<string, string> = {
reaction: 'Réaction',
question: 'Question',
suggestion: 'Suggestion',
retour: 'Retour',
}
function typeLabel(type: string) {
return TYPE_LABELS[type] ?? type
}
function startEdit(msg: any) { function startEdit(msg: any) {
editing.value = msg.id editing.value = msg.id
editForm.author = msg.author editForm.author = msg.author
editForm.text = msg.text editForm.text = msg.text
editForm.type = TYPE_LABELS[msg.type] ? msg.type : 'question'
} }
async function saveEdit(msg: any) { async function saveEdit(msg: any) {
await $fetch(`/api/admin/messages/${msg.id}`, { await $fetch(`/api/admin/messages/${msg.id}`, {
method: 'PUT', method: 'PUT',
body: { author: editForm.author, text: editForm.text }, body: { author: editForm.author, text: editForm.text, type: editForm.type },
}) })
editing.value = null editing.value = null
await refresh() await refresh()
@@ -111,6 +163,29 @@ async function togglePublished(msg: any) {
await refresh() await refresh()
} }
function startReply(msg: any) {
replyEditing.value = msg.id
replyText.value = msg.reply?.text ?? ''
}
async function saveReply(msg: any) {
await $fetch(`/api/admin/messages/${msg.id}`, {
method: 'PUT',
body: { reply: replyText.value.trim() || null },
})
replyEditing.value = null
await refresh()
}
async function removeReply(msg: any) {
await $fetch(`/api/admin/messages/${msg.id}`, {
method: 'PUT',
body: { reply: null },
})
replyEditing.value = null
await refresh()
}
async function remove(msg: any) { async function remove(msg: any) {
if (!confirm(`Supprimer le message de "${msg.author}" ?`)) return if (!confirm(`Supprimer le message de "${msg.author}" ?`)) return
await $fetch(`/api/admin/messages/${msg.id}`, { method: 'DELETE' }) await $fetch(`/api/admin/messages/${msg.id}`, { method: 'DELETE' })
@@ -130,14 +205,14 @@ function formatDate(iso: string) {
<style scoped> <style scoped>
.msg-row { .msg-row {
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
} }
.msg-row--draft { .msg-row--draft {
border-left: 3px solid hsl(36 80% 52%); border-left: 3px solid hsl(var(--color-accent));
} }
.status-badge { .status-badge {
@@ -155,22 +230,61 @@ function formatDate(iso: string) {
} }
.status-badge--draft { .status-badge--draft {
background: hsl(36 80% 52% / 0.15); background: hsl(var(--color-accent) / 0.15);
color: hsl(36 80% 66%); color: hsl(var(--color-accent));
}
.type-badge {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.45rem;
border-radius: 9999px;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary) / 0.7);
}
.type-badge--question {
background: hsl(220 70% 50% / 0.12);
color: hsl(220 70% 65%);
}
.type-badge--suggestion {
background: hsl(280 60% 55% / 0.12);
color: hsl(280 60% 70%);
}
.type-badge--retour {
background: hsl(35 70% 50% / 0.12);
color: hsl(35 70% 65%);
}
.reply-block {
background: hsl(var(--color-primary) / 0.06);
border-left: 2px solid hsl(var(--color-primary) / 0.3);
border-radius: 0 0.5rem 0.5rem 0;
padding: 0.75rem 1rem;
}
.reply-form {
background: hsl(var(--color-surface) / 0.5);
border-radius: 0.5rem;
padding: 0.75rem;
} }
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
.action-btn { .action-btn {
@@ -180,16 +294,16 @@ function formatDate(iso: string) {
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
border-radius: 0.375rem; border-radius: 0.375rem;
font-size: 0.75rem; font-size: 0.75rem;
color: hsl(20 8% 60%); color: hsl(var(--color-text-muted));
background: none; background: none;
border: 1px solid hsl(20 8% 16%); border: 1px solid hsl(var(--color-surface-light));
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.action-btn:hover { .action-btn:hover {
background: hsl(20 8% 10%); background: hsl(var(--color-surface));
color: white; color: hsl(var(--color-text));
} }
.action-btn--save { .action-btn--save {
@@ -201,6 +315,20 @@ function formatDate(iso: string) {
background: hsl(142 70% 40% / 0.1); background: hsl(142 70% 40% / 0.1);
} }
.action-btn--reply {
border-color: hsl(var(--color-primary) / 0.3);
color: hsl(var(--color-primary) / 0.8);
}
.action-btn--reply:hover {
background: hsl(var(--color-primary) / 0.08);
}
.msg-text {
color: hsl(var(--color-text) / 0.72);
white-space: pre-line;
}
.action-btn--danger:hover { .action-btn--danger:hover {
background: hsl(0 70% 40% / 0.1); background: hsl(0 70% 40% / 0.1);
border-color: hsl(0 70% 40% / 0.3); border-color: hsl(0 70% 40% / 0.3);

View File

@@ -2,11 +2,11 @@
<div> <div>
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<div> <div>
<NuxtLink to="/admin" class="text-sm text-white/40 hover:text-white/60 transition-colors"> <NuxtLink to="/admin/pages" class="text-sm text-white/40 hover:text-white/60 transition-colors">
Dashboard Pages
</NuxtLink> </NuxtLink>
<h1 class="font-display text-2xl font-bold text-white mt-1"> <h1 class="font-display text-2xl font-bold text-white mt-1">
Page : {{ pageName }} Page : {{ pagePath }}
</h1> </h1>
</div> </div>
<AdminSaveButton :saving="saving" :saved="saved" @save="save" /> <AdminSaveButton :saving="saving" :saved="saved" @save="save" />
@@ -40,9 +40,12 @@ definePageMeta({
}) })
const route = useRoute() const route = useRoute()
const pageName = computed(() => route.params.name as string) const pagePath = computed(() => {
const p = route.params.path
return Array.isArray(p) ? p.join('/') : p
})
const { data, error } = await useFetch(() => `/api/content/pages/${pageName.value}`) const { data, error } = await useFetch(() => `/api/content/pages/${pagePath.value}`)
const yamlContent = ref('') const yamlContent = ref('')
@@ -60,7 +63,7 @@ async function save() {
saved.value = false saved.value = false
try { try {
const parsed = yaml.parse(yamlContent.value) const parsed = yaml.parse(yamlContent.value)
await $fetch(`/api/admin/content/pages/${pageName.value}`, { await $fetch(`/api/admin/content/pages/${pagePath.value}`, {
method: 'PUT', method: 'PUT',
body: parsed, body: parsed,
}) })
@@ -80,10 +83,10 @@ async function save() {
.yaml-textarea { .yaml-textarea {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
border-radius: 0.5rem; border-radius: 0.5rem;
background: hsl(20 8% 4%); background: hsl(var(--color-bg));
color: hsl(36 80% 76%); color: hsl(var(--color-accent));
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.7; line-height: 1.7;
@@ -93,6 +96,6 @@ async function save() {
.yaml-textarea:focus { .yaml-textarea:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -0,0 +1,97 @@
<template>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<NuxtLink to="/admin" class="text-sm text-white/40 hover:text-white/60 transition-colors">
Dashboard
</NuxtLink>
<h1 class="font-display text-2xl font-bold text-white mt-1">Pages</h1>
</div>
</div>
<div v-if="pages" class="flex flex-col gap-6">
<!-- Root pages -->
<div v-if="rootPages.length">
<h2 class="font-display text-lg font-semibold text-white/80 mb-3">Pages principales</h2>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="page in rootPages"
:key="page.path"
:to="`/admin/pages/${page.path}`"
class="page-card"
>
<div class="i-lucide-file-text h-5 w-5 text-primary mb-1" />
<span class="font-mono text-sm text-white">{{ page.label }}</span>
</NuxtLink>
</div>
</div>
<!-- Section pages -->
<div v-for="[section, sectionPages] in sections" :key="section">
<h2 class="font-display text-lg font-semibold text-white/80 mb-3">
<span class="i-lucide-folder h-4 w-4 inline-block mr-1 text-accent" />
{{ section }}
</h2>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="page in sectionPages"
:key="page.path"
:to="`/admin/pages/${page.path}`"
class="page-card"
>
<div class="i-lucide-file-text h-5 w-5 text-primary/60 mb-1" />
<span class="font-mono text-sm text-white">{{ page.label }}</span>
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'admin',
})
interface PageEntry {
path: string
label: string
section?: string
}
const { data: pages } = await useFetch<PageEntry[]>('/api/admin/content/pages')
const rootPages = computed(() =>
(pages.value ?? []).filter(p => !p.section),
)
const sections = computed(() => {
const map = new Map<string, PageEntry[]>()
for (const p of pages.value ?? []) {
if (p.section) {
if (!map.has(p.section)) map.set(p.section, [])
map.get(p.section)!.push(p)
}
}
return Array.from(map.entries())
})
</script>
<style scoped>
.page-card {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-surface-light));
background: hsl(var(--color-bg));
text-decoration: none;
transition: all 0.2s;
}
.page-card:hover {
border-color: hsl(var(--color-primary) / 0.3);
background: hsl(var(--color-surface));
}
</style>

View File

@@ -65,7 +65,7 @@
</AdminFieldList> </AdminFieldList>
</AdminFormSection> </AdminFormSection>
<AdminFormSection title="GrateWizard"> <AdminFormSection title="grateWizard">
<AdminFieldText v-model="data.gratewizard.url" label="URL de l'application" /> <AdminFieldText v-model="data.gratewizard.url" label="URL de l'application" />
</AdminFormSection> </AdminFormSection>
</template> </template>
@@ -103,14 +103,14 @@ async function save() {
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
} }
</style> </style>

View File

@@ -6,14 +6,22 @@
</div> </div>
<template v-if="config"> <template v-if="config">
<AdminFormSection title="Métadonnées des chansons" open> <AdminFormSection title="Morceaux" open>
<div <div
v-for="(song, i) in config.songs" v-for="(song, i) in config.songs"
:key="i" :key="song.id"
class="song-row" class="song-row"
draggable="true"
@dragstart="onDragStart(i, $event)"
@dragover.prevent="onDragOver(i)"
@dragend="onDragEnd"
:class="{ 'song-row--dragging': dragIdx === i, 'song-row--over': dropIdx === i && dropIdx !== dragIdx }"
> >
<div class="drag-handle" aria-label="Réordonner">
<div class="i-lucide-grip-vertical h-4 w-4" />
</div>
<span class="song-num">{{ i + 1 }}</span> <span class="song-num">{{ i + 1 }}</span>
<div class="flex-1 grid gap-2 sm:grid-cols-2"> <div class="flex-1 grid gap-2 sm:grid-cols-3">
<input <input
v-model="song.title" v-model="song.title"
class="admin-input" class="admin-input"
@@ -24,8 +32,33 @@
class="admin-input" class="admin-input"
placeholder="/audio/fichier.mp3" placeholder="/audio/fichier.mp3"
/> />
<input
v-model="song.id"
class="admin-input font-mono text-xs"
placeholder="identifiant-slug"
/>
</div> </div>
<div class="flex-1">
<textarea
v-model="song.lyrics"
class="admin-input lyrics-textarea"
placeholder="Paroles..."
rows="2"
/>
</div> </div>
<button
class="delete-btn"
@click="removeSong(i)"
aria-label="Supprimer"
>
<div class="i-lucide-trash-2 h-4 w-4" />
</button>
</div>
<button class="add-btn" @click="addSong">
<div class="i-lucide-plus h-4 w-4" />
Ajouter un morceau
</button>
</AdminFormSection> </AdminFormSection>
</template> </template>
</div> </div>
@@ -37,14 +70,80 @@ definePageMeta({
middleware: 'admin', middleware: 'admin',
}) })
const { data: config } = await useFetch('/api/content/config') const { data: config } = await useFetch<any>('/api/content/config')
const saving = ref(false) const saving = ref(false)
const saved = ref(false) const saved = ref(false)
// Drag & drop state
const dragIdx = ref<number | null>(null)
const dropIdx = ref<number | null>(null)
function onDragStart(i: number, e: DragEvent) {
dragIdx.value = i
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'
}
}
function onDragOver(i: number) {
dropIdx.value = i
}
function onDragEnd() {
if (dragIdx.value !== null && dropIdx.value !== null && dragIdx.value !== dropIdx.value && config.value) {
const songs = config.value.songs
const [moved] = songs.splice(dragIdx.value, 1)
songs.splice(dropIdx.value, 0, moved)
// Sync defaultPlaylistOrder
config.value.defaultPlaylistOrder = songs.map((s: any) => s.id)
}
dragIdx.value = null
dropIdx.value = null
}
function addSong() {
if (!config.value) return
const newSong = {
id: `nouveau-morceau-${Date.now()}`,
title: '',
artist: 'Yvv',
file: '/audio/',
duration: 0,
lyrics: '',
tags: [],
}
config.value.songs.push(newSong)
config.value.defaultPlaylistOrder.push(newSong.id)
}
function removeSong(i: number) {
if (!config.value) return
const songId = config.value.songs[i].id
config.value.songs.splice(i, 1)
// Remove from defaultPlaylistOrder
const orderIdx = config.value.defaultPlaylistOrder.indexOf(songId)
if (orderIdx !== -1) config.value.defaultPlaylistOrder.splice(orderIdx, 1)
// Clean chapterSongs
config.value.chapterSongs = config.value.chapterSongs.filter((cs: any) => cs.songId !== songId)
}
async function save() { async function save() {
saving.value = true saving.value = true
saved.value = false saved.value = false
try { try {
// Regenerate IDs from titles for new songs
for (const song of config.value!.songs) {
if (song.id.startsWith('nouveau-morceau-')) {
song.id = song.title
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
}
// Sync playlist order
config.value!.defaultPlaylistOrder = config.value!.songs.map((s: any) => s.id)
await $fetch('/api/admin/content/config', { await $fetch('/api/admin/content/config', {
method: 'PUT', method: 'PUT',
body: config.value, body: config.value,
@@ -61,32 +160,96 @@ async function save() {
<style scoped> <style scoped>
.song-row { .song-row {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 0.75rem; gap: 0.75rem;
padding: 0.5rem; padding: 0.75rem 0.5rem;
border-bottom: 1px solid hsl(20 8% 10%); border-bottom: 1px solid hsl(var(--color-surface));
transition: background 0.15s;
}
.song-row--dragging {
opacity: 0.4;
}
.song-row--over {
border-top: 2px solid hsl(var(--color-primary));
}
.drag-handle {
cursor: grab;
padding: 0.25rem;
color: hsl(var(--color-text-muted));
flex-shrink: 0;
margin-top: 0.25rem;
}
.drag-handle:active {
cursor: grabbing;
} }
.song-num { .song-num {
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
font-size: 0.8rem; font-size: 0.8rem;
color: hsl(20 8% 40%); color: hsl(var(--color-text-muted));
width: 1.25rem; width: 1.25rem;
text-align: right; text-align: right;
flex-shrink: 0;
margin-top: 0.375rem;
} }
.admin-input { .admin-input {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-surface-light));
background: hsl(20 8% 6%); background: hsl(var(--color-bg));
color: white; color: hsl(var(--color-text));
font-size: 0.8rem; font-size: 0.8rem;
width: 100%; width: 100%;
} }
.admin-input:focus { .admin-input:focus {
outline: none; outline: none;
border-color: hsl(12 76% 48% / 0.5); border-color: hsl(var(--color-primary) / 0.5);
}
.lyrics-textarea {
resize: vertical;
min-height: 2.5rem;
}
.delete-btn {
flex-shrink: 0;
padding: 0.375rem;
border-radius: 0.375rem;
color: hsl(0 60% 50%);
background: none;
border: none;
cursor: pointer;
transition: background 0.15s;
margin-top: 0.25rem;
}
.delete-btn:hover {
background: hsl(0 60% 50% / 0.1);
}
.add-btn {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px dashed hsl(var(--color-surface-light));
background: none;
color: hsl(var(--color-text-muted));
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
}
.add-btn:hover {
border-color: hsl(var(--color-primary) / 0.5);
color: hsl(var(--color-primary));
} }
</style> </style>

View File

@@ -0,0 +1,159 @@
<template>
<div class="section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<div :class="`i-lucide-${content?.icon ?? 'landmark'}`" class="h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title ?? slug }}
</h1>
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<!-- Features grid (for decision page) -->
<div v-if="content?.features" class="grid gap-4 sm:grid-cols-2 mb-12">
<div v-for="feature in content.features" :key="feature.title" class="feature-card">
<div class="feature-icon">
<div :class="`i-lucide-${feature.icon} h-5 w-5`" />
</div>
<h3 class="font-display font-semibold mb-1" style="color: hsl(var(--color-text))">{{ feature.title }}</h3>
<p class="text-sm leading-relaxed" style="color: hsl(var(--color-text-muted))">{{ feature.text }}</p>
</div>
</div>
<!-- Project card -->
<div v-if="content?.project" class="project-card mb-8">
<div class="project-icon">
<div class="i-lucide-rocket h-5 w-5" />
</div>
<h2 class="font-display text-xl font-semibold mb-2" style="color: hsl(var(--color-text))">
{{ content.project.name }}
</h2>
<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>
</div>
<!-- Extended content -->
<div v-if="content?.content" class="prose-block mb-10">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
<div class="text-center flex flex-col items-center gap-3 sm:flex-row sm:justify-center">
<UiBaseButton v-if="slug === 'decision'" :href="decisionUrl" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
Ouvrir Glibredecision
</UiBaseButton>
<UiBaseButton v-if="slug === 'tarifs-eau'" :href="sejeteral0Url" target="_blank">
<div class="i-lucide-external-link mr-2 h-4 w-4" />
Lancer SejeteralO
</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>
</template>
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug as string
const { data: content } = await usePageContent(`citoyenne/${slug}`)
const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({
title: content.value?.meta?.title ?? slug,
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.feature-card {
padding: 1.25rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
}
.feature-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.1);
color: hsl(var(--color-primary));
margin-bottom: 0.75rem;
}
.project-card {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-primary) / 0.15);
background: hsl(var(--color-surface));
}
.project-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
margin-bottom: 1rem;
}
.gestation-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
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);
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
</style>

View File

@@ -0,0 +1,631 @@
<template>
<div class="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>
<!-- ship's wheel -->
<circle cx="50" cy="130" r="28" stroke="currentColor" stroke-width="3" fill="none" opacity="0.35"/>
<line x1="50" y1="102" x2="50" y2="158" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="22" y1="130" x2="78" y2="130" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="30" y1="110" x2="70" y2="150" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="70" y1="110" x2="30" y2="150" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<!-- wheel spokes knobs -->
<circle cx="50" cy="100" r="3" fill="currentColor" opacity="0.4"/>
<circle cx="50" cy="160" r="3" fill="currentColor" opacity="0.4"/>
<circle cx="20" cy="130" r="3" fill="currentColor" opacity="0.4"/>
<circle cx="80" cy="130" r="3" fill="currentColor" opacity="0.4"/>
<!-- body (small oval, profile facing right) -->
<ellipse cx="85" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="92" cy="68" r="16" fill="currentColor" opacity="0.3"/>
<!-- captain hat -->
<rect x="76" y="52" width="32" height="8" rx="4" fill="currentColor" opacity="0.4"/>
<rect x="80" y="46" width="24" height="8" rx="3" fill="currentColor" opacity="0.3"/>
<!-- hat brim detail -->
<ellipse cx="92" cy="60" rx="18" ry="3" fill="currentColor" opacity="0.2"/>
<!-- eyes (looking right through telescope) -->
<circle cx="98" cy="65" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="90" cy="66" r="2" fill="currentColor" opacity="0.5"/>
<!-- beak (pointy, profile right) -->
<polygon points="108,68 120,65 108,72" fill="currentColor" opacity="0.35"/>
<!-- left arm on wheel -->
<line x1="65" y1="100" x2="55" y2="115" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- right arm holding telescope -->
<line x1="105" y1="95" x2="130" y2="72" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- telescope -->
<rect x="128" y="66" width="30" height="7" rx="3.5" fill="currentColor" opacity="0.4"/>
<rect x="155" y="64" width="10" height="11" rx="3" fill="currentColor" opacity="0.3"/>
<!-- left leg (long, forward) -->
<line x1="75" y1="135" x2="60" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- left foot -->
<ellipse cx="55" cy="193" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (long, back) -->
<line x1="92" y1="136" x2="100" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- right foot -->
<ellipse cx="103" cy="195" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 2. Shadok Avocate (top-right robe, scroll, arm up) -->
<svg class="shadok shadok-avocate" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- body (small oval) -->
<ellipse cx="80" cy="115" rx="22" ry="30" fill="currentColor" opacity="0.25"/>
<!-- robe/gown flowing -->
<path d="M58 100 Q55 145 48 175 L112 175 Q105 145 102 100 Z" fill="currentColor" opacity="0.18"/>
<!-- robe collar -->
<path d="M68 88 Q80 96 92 88" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.35"/>
<!-- head -->
<circle cx="80" cy="70" r="16" fill="currentColor" opacity="0.3"/>
<!-- eyes (looking up-left) -->
<circle cx="74" cy="67" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="83" cy="68" r="2" fill="currentColor" opacity="0.5"/>
<!-- beak (3/4 view, slight left) -->
<polygon points="68,72 56,69 66,76" fill="currentColor" opacity="0.35"/>
<!-- left arm up dramatically -->
<line x1="60" y1="100" x2="30" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- open hand at top -->
<line x1="30" y1="55" x2="24" y2="46" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<line x1="30" y1="55" x2="28" y2="44" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<line x1="30" y1="55" x2="34" y2="46" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- right arm holding scroll -->
<line x1="100" y1="102" x2="125" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- scroll document (big) -->
<rect x="120" y="72" width="28" height="40" rx="3" fill="currentColor" opacity="0.25"/>
<!-- scroll curl top -->
<ellipse cx="134" cy="72" rx="14" ry="5" fill="currentColor" opacity="0.2"/>
<!-- scroll curl bottom -->
<ellipse cx="134" cy="112" rx="12" ry="4" fill="currentColor" opacity="0.2"/>
<!-- scroll text lines -->
<line x1="126" y1="82" x2="142" y2="82" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="126" y1="88" x2="140" y2="88" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="126" y1="94" x2="143" y2="94" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="126" y1="100" x2="138" y2="100" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<!-- left leg (long) -->
<line x1="72" y1="142" x2="62" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="58" cy="198" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg -->
<line x1="88" y1="142" x2="95" y2="196" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="99" cy="199" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 3. Shadok Vigie (left crow's nest, looking far) -->
<svg class="shadok shadok-vigie" viewBox="0 0 140 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- mast pole -->
<line x1="70" y1="90" x2="70" y2="218" stroke="currentColor" stroke-width="4" opacity="0.3"/>
<!-- crow's nest platform -->
<path d="M38 88 L42 100 L98 100 L102 88 Z" fill="currentColor" opacity="0.25"/>
<line x1="36" y1="88" x2="104" y2="88" stroke="currentColor" stroke-width="3" opacity="0.35"/>
<!-- nest railing -->
<line x1="40" y1="78" x2="40" y2="100" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="100" y1="78" x2="100" y2="100" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="40" y1="78" x2="100" y2="78" stroke="currentColor" stroke-width="2" opacity="0.25"/>
<!-- body (small, inside nest) -->
<ellipse cx="70" cy="72" rx="20" ry="22" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="70" cy="38" r="15" fill="currentColor" opacity="0.3"/>
<!-- eyes (squinting, looking right far) -->
<circle cx="78" cy="35" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="70" cy="36" r="2" fill="currentColor" opacity="0.4"/>
<!-- beak (profile right) -->
<polygon points="83,38 96,35 84,42" fill="currentColor" opacity="0.35"/>
<!-- right hand shielding eyes -->
<line x1="88" y1="55" x2="98" y2="35" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="96" y1="33" x2="108" y2="30" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<!-- left arm resting on railing -->
<line x1="52" y1="60" x2="42" y2="78" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- legs dangling below nest (long!) -->
<line x1="60" y1="98" x2="52" y2="160" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="48" cy="163" rx="8" ry="4" fill="currentColor" opacity="0.3"/>
<line x1="80" y1="98" x2="86" y2="158" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="90" cy="161" rx="8" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 4. Shadok Comedien (right theater masks, cape, leg lifted) -->
<svg class="shadok shadok-comedien" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- dramatic cape -->
<path d="M65 85 Q40 120 35 180 L115 180 Q110 120 95 85 Z" fill="currentColor" opacity="0.15"/>
<path d="M95 85 Q120 100 135 160 Q130 165 115 180" fill="currentColor" opacity="0.1"/>
<!-- body (small oval) -->
<ellipse cx="80" cy="105" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="80" cy="62" r="16" fill="currentColor" opacity="0.3"/>
<!-- eyes (different directions dramatic!) -->
<circle cx="74" cy="59" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="86" cy="61" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- beak -->
<polygon points="72,66 60,63 70,70" fill="currentColor" opacity="0.35"/>
<!-- left arm up holding comedy mask -->
<line x1="62" y1="92" x2="32" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- comedy mask (big, smiling) -->
<circle cx="25" cy="42" r="16" fill="currentColor" opacity="0.2"/>
<path d="M17 46 Q25 56 33 46" stroke="currentColor" stroke-width="2" fill="none" opacity="0.4"/>
<circle cx="20" cy="38" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="30" cy="38" r="2" fill="currentColor" opacity="0.5"/>
<!-- right arm out holding tragedy mask -->
<line x1="98" y1="92" x2="135" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- tragedy mask (big, frowning) -->
<circle cx="145" cy="55" r="16" fill="currentColor" opacity="0.2"/>
<path d="M137 62 Q145 54 153 62" stroke="currentColor" stroke-width="2" fill="none" opacity="0.4"/>
<circle cx="140" cy="50" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="150" cy="50" r="2" fill="currentColor" opacity="0.5"/>
<!-- left leg (long, planted) -->
<line x1="72" y1="128" x2="62" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="58" cy="193" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (lifted dramatically!) -->
<line x1="88" y1="126" x2="115" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="120" cy="167" rx="8" ry="4" transform="rotate(-25 120 167)" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 5. Shadok Cartographe (bottom-left sitting, map, compass) -->
<svg class="shadok shadok-cartographe" viewBox="0 0 180 190" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- table -->
<rect x="20" y="125" width="140" height="6" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="35" y1="131" x2="30" y2="185" stroke="currentColor" stroke-width="3" opacity="0.25"/>
<line x1="145" y1="131" x2="150" y2="185" stroke="currentColor" stroke-width="3" opacity="0.25"/>
<!-- big map spread on table -->
<rect x="30" y="108" width="110" height="18" rx="2" fill="currentColor" opacity="0.2"/>
<!-- map details -->
<path d="M45 112 Q65 108 85 115 Q105 110 125 114" stroke="currentColor" stroke-width="1" fill="none" opacity="0.25"/>
<circle cx="75" cy="115" r="3" stroke="currentColor" stroke-width="1" fill="none" opacity="0.2"/>
<line x1="95" y1="110" x2="95" y2="122" stroke="currentColor" stroke-width="0.8" opacity="0.2"/>
<!-- body (small, hunched over map) -->
<ellipse cx="90" cy="88" rx="22" ry="25" fill="currentColor" opacity="0.25" transform="rotate(10 90 88)"/>
<!-- head (bent down looking at map) -->
<circle cx="78" cy="60" r="15" fill="currentColor" opacity="0.3"/>
<!-- eyes (looking down at map) -->
<circle cx="74" cy="64" r="2" fill="currentColor" opacity="0.6"/>
<circle cx="83" cy="65" r="2" fill="currentColor" opacity="0.5"/>
<!-- beak (pointing down) -->
<polygon points="72,70 65,78 78,72" fill="currentColor" opacity="0.35"/>
<!-- right arm holding compass tool -->
<line x1="110" y1="92" x2="130" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- compass/divider tool (big V shape) -->
<line x1="130" y1="105" x2="125" y2="125" stroke="currentColor" stroke-width="2.5" opacity="0.4"/>
<line x1="130" y1="105" x2="140" y2="125" stroke="currentColor" stroke-width="2.5" opacity="0.4"/>
<circle cx="130" cy="103" r="3" fill="currentColor" opacity="0.3"/>
<!-- left arm holding magnifying glass -->
<line x1="70" y1="90" x2="48" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- magnifying glass (big) -->
<circle cx="38" cy="96" r="12" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.35"/>
<line x1="48" y1="103" x2="55" y2="112" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- legs (sitting, bent under table) -->
<line x1="78" y1="110" x2="65" y2="145" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="65" y1="145" x2="50" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<ellipse cx="45" cy="150" rx="8" ry="3.5" fill="currentColor" opacity="0.3"/>
<line x1="100" y1="110" x2="110" y2="145" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="110" y1="145" x2="122" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<ellipse cx="127" cy="150" rx="8" ry="3.5" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 6. Shadok Juge (bottom-right on bench, gavel, wig) -->
<svg class="shadok shadok-juge" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- elevated bench/podium -->
<rect x="30" y="145" width="100" height="55" rx="4" fill="currentColor" opacity="0.2"/>
<rect x="25" y="138" width="110" height="10" rx="3" fill="currentColor" opacity="0.28"/>
<!-- bench front panel detail -->
<rect x="55" y="155" width="50" height="35" rx="3" fill="currentColor" opacity="0.1"/>
<!-- body (small, seated) -->
<ellipse cx="80" cy="112" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- head -->
<circle cx="80" cy="68" r="16" fill="currentColor" opacity="0.3"/>
<!-- judge wig (curly puffs) -->
<circle cx="66" cy="60" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="94" cy="60" r="6" fill="currentColor" opacity="0.2"/>
<circle cx="72" cy="54" r="5" fill="currentColor" opacity="0.18"/>
<circle cx="88" cy="54" r="5" fill="currentColor" opacity="0.18"/>
<circle cx="80" cy="52" r="5.5" fill="currentColor" opacity="0.2"/>
<!-- wig curls hanging -->
<circle cx="63" cy="70" r="4.5" fill="currentColor" opacity="0.15"/>
<circle cx="97" cy="70" r="4.5" fill="currentColor" opacity="0.15"/>
<!-- eyes (stern, both forward) -->
<circle cx="74" cy="66" r="2.5" fill="currentColor" opacity="0.65"/>
<circle cx="86" cy="66" r="2.5" fill="currentColor" opacity="0.65"/>
<!-- stern eyebrows -->
<line x1="71" y1="62" x2="77" y2="63" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<line x1="89" y1="63" x2="83" y2="62" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- beak (small, stern) -->
<polygon points="80,74 72,78 80,80" fill="currentColor" opacity="0.35"/>
<!-- right arm raising gavel -->
<line x1="100" y1="98" x2="130" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- gavel (big!) -->
<rect x="120" y="44" width="26" height="12" rx="4" fill="currentColor" opacity="0.4"/>
<line x1="133" y1="56" x2="133" y2="38" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- left arm on bench -->
<line x1="60" y1="100" x2="40" y2="130" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- legs (dangling from bench, long) -->
<line x1="72" y1="138" x2="62" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="58" cy="203" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<line x1="88" y1="138" x2="96" y2="198" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="100" cy="201" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<!-- 7. Shadok Matelot (center-bottom pulling rope, leaning back, anchor) -->
<svg class="shadok shadok-matelot" viewBox="0 0 160 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- rope going up and off -->
<path d="M30 20 Q35 50 55 70 Q65 78 68 85" stroke="currentColor" stroke-width="3" fill="none" opacity="0.3"/>
<!-- body (leaning back, tilted) -->
<ellipse cx="85" cy="108" rx="20" ry="27" fill="currentColor" opacity="0.25" transform="rotate(-15 85 108)"/>
<!-- head -->
<circle cx="95" cy="68" r="15" fill="currentColor" opacity="0.3"/>
<!-- bandana -->
<path d="M80 62 Q95 55 110 62" stroke="currentColor" stroke-width="3" fill="currentColor" opacity="0.2"/>
<line x1="108" y1="60" x2="118" y2="55" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="108" y1="62" x2="116" y2="60" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.2"/>
<!-- eyes (straining, squinting) -->
<circle cx="90" cy="66" r="2" fill="currentColor" opacity="0.6"/>
<circle cx="100" cy="65" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- beak -->
<polygon points="104,70 115,67 105,74" fill="currentColor" opacity="0.35"/>
<!-- both arms pulling rope -->
<line x1="68" y1="95" x2="55" y2="78" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="75" y1="98" x2="62" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- left leg (long, bracing forward) -->
<line x1="72" y1="130" x2="48" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="43" cy="188" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (long, back) -->
<line x1="92" y1="132" x2="110" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="114" cy="191" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<!-- anchor nearby -->
<line x1="135" y1="140" x2="135" y2="185" stroke="currentColor" stroke-width="3" opacity="0.3"/>
<circle cx="135" cy="138" r="5" stroke="currentColor" stroke-width="2" fill="none" opacity="0.25"/>
<path d="M120 180 Q135 195 150 180" stroke="currentColor" stroke-width="3" fill="none" opacity="0.3"/>
<line x1="120" y1="180" x2="117" y2="174" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
<line x1="150" y1="180" x2="153" y2="174" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
</g>
</svg>
<!-- 8. Shadok Mime (right pushing invisible wall, striped shirt, beret) -->
<svg class="shadok shadok-mime" viewBox="0 0 150 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<!-- body (small oval) -->
<ellipse cx="70" cy="112" rx="22" ry="28" fill="currentColor" opacity="0.2"/>
<!-- striped shirt lines -->
<line x1="52" y1="96" x2="88" y2="96" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="50" y1="102" x2="90" y2="102" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="50" y1="108" x2="90" y2="108" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="50" y1="114" x2="90" y2="114" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="52" y1="120" x2="88" y2="120" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="54" y1="126" x2="86" y2="126" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<!-- head (white face = lighter) -->
<circle cx="75" cy="65" r="16" fill="currentColor" opacity="0.2"/>
<!-- white face highlight -->
<circle cx="75" cy="65" r="13" fill="currentColor" opacity="0.08"/>
<!-- beret -->
<ellipse cx="75" cy="50" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
<circle cx="75" cy="46" r="3" fill="currentColor" opacity="0.25"/>
<!-- eyes (expressive, wide) -->
<circle cx="69" cy="62" r="3" fill="currentColor" opacity="0.55"/>
<circle cx="81" cy="62" r="3" fill="currentColor" opacity="0.55"/>
<!-- raised eyebrows (surprised) -->
<path d="M66 57 Q69 54 72 57" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<path d="M78 57 Q81 54 84 57" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<!-- beak (small) -->
<polygon points="75,72 68,76 75,78" fill="currentColor" opacity="0.3"/>
<!-- both arms pushing against invisible wall (to the right) -->
<line x1="90" y1="100" x2="125" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="115" x2="125" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- flat hands pressed against wall -->
<rect x="124" y="78" width="12" height="15" rx="3" fill="currentColor" opacity="0.2"/>
<rect x="124" y="103" width="12" height="15" rx="3" fill="currentColor" opacity="0.2"/>
<!-- invisible wall hint (faint line) -->
<line x1="138" y1="50" x2="138" y2="180" stroke="currentColor" stroke-width="1" stroke-dasharray="4 6" opacity="0.12"/>
<!-- left leg (long, leaning forward) -->
<line x1="60" y1="138" x2="45" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="40" cy="198" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
<!-- right leg (long, back) -->
<line x1="80" y1="138" x2="95" y2="194" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="99" cy="197" rx="9" ry="4" fill="currentColor" opacity="0.3"/>
</g>
</svg>
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-6">
<!-- Decision collective -->
<div class="item-card">
<NuxtLink to="/citoyenne/decision" class="item-body group">
<div class="item-header">
<div class="item-icon">
<div class="i-lucide-gavel h-5 w-5" />
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Décision collective
</h2>
</div>
<p class="leading-relaxed mt-3" style="color: hsl(var(--color-text-muted))">
Se donner les moyens de la décision collective.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
<div class="item-actions">
<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" />
Ouvrir Glibredecision
</a>
</div>
</div>
<!-- Tarifs de l'eau -->
<div class="item-card">
<NuxtLink to="/citoyenne/tarifs-eau" class="item-body group">
<div class="item-header">
<div class="item-icon">
<div class="i-lucide-droplets h-5 w-5" />
</div>
<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>
</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.
Permet de confier la décision à la population des communes.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
<div class="item-actions">
<a :href="sejeteral0Url" target="_blank" rel="noopener" class="action-btn action-btn--primary">
<div class="i-lucide-external-link h-3.5 w-3.5" />
Lancer SejeteralO
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('citoyenne')
const appConfig = useAppConfig()
const decisionUrl = (appConfig.libredecision as { url: string })?.url ?? '#'
const sejeteral0Url = (appConfig.sejeteral0 as { url: string })?.url ?? '#'
useHead({
title: content.value?.meta?.title ?? 'Autonomie citoyenne',
})
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.item-card {
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
transition: border-color 0.2s;
overflow: hidden;
}
.item-card:hover {
border-color: hsl(var(--color-primary) / 0.2);
}
.item-body {
display: block;
padding: 1.5rem;
text-decoration: none;
color: inherit;
}
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.item-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);
}
.item-actions {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.75rem 1.5rem;
border-top: 1px solid hsl(var(--color-text) / 0.06);
background: hsl(var(--color-bg) / 0.4);
}
.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;
text-decoration: none;
transition: all 0.2s;
cursor: pointer;
}
.action-btn--primary {
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.12);
border: 1px solid hsl(var(--color-primary) / 0.25);
}
.action-btn--primary:hover {
background: hsl(var(--color-primary) / 0.2);
border-color: hsl(var(--color-primary) / 0.4);
}
/* Shadok illustrations */
.shadok {
position: absolute;
pointer-events: none;
width: clamp(70px, 10vw, 140px);
z-index: 0;
}
.shadok-capitaine {
top: 2%;
left: 2%;
color: hsl(var(--color-primary));
opacity: 0.22;
animation: shadok-float-1 9s ease-in-out infinite;
}
.shadok-avocate {
top: 1%;
right: 2%;
color: hsl(var(--color-accent));
opacity: 0.2;
animation: shadok-float-2 11s ease-in-out infinite;
}
.shadok-vigie {
left: 2%;
top: 38%;
color: hsl(var(--color-primary));
opacity: 0.24;
animation: shadok-float-3 10s ease-in-out infinite;
}
.shadok-comedien {
right: 3%;
top: 35%;
color: hsl(var(--color-accent));
opacity: 0.2;
animation: shadok-float-4 8s ease-in-out infinite;
}
.shadok-cartographe {
bottom: 10%;
left: 1%;
color: hsl(var(--color-accent));
opacity: 0.22;
animation: shadok-float-5 12s ease-in-out infinite;
}
.shadok-juge {
bottom: 6%;
right: 1%;
color: hsl(var(--color-primary));
opacity: 0.24;
animation: shadok-float-6 9.5s ease-in-out infinite;
}
.shadok-matelot {
bottom: 2%;
left: 50%;
transform: translateX(-50%);
color: hsl(var(--color-primary));
opacity: 0.18;
animation: shadok-float-7 7s ease-in-out infinite;
}
.shadok-mime {
right: 2%;
top: 55%;
color: hsl(var(--color-accent));
opacity: 0.28;
animation: shadok-float-8 10.5s ease-in-out infinite;
}
@keyframes shadok-float-1 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-2 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-3 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@keyframes shadok-float-4 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-5 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes shadok-float-6 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-7 {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-7px); }
}
@keyframes shadok-float-8 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok {
display: none;
}
}
</style>

View File

@@ -0,0 +1,453 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok 1 : Libraire behind counter, 3/4 view, glasses, recommending a book -->
<svg class="shadok-libraire" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body (small oval) -->
<ellipse cx="75" cy="95" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="75" cy="58" r="16" fill="currentColor" opacity="0.3"/>
<!-- Eyes looking right (recommending) -->
<circle cx="80" cy="55" r="2" fill="currentColor" opacity="0.6"/>
<circle cx="86" cy="54" r="2" fill="currentColor" opacity="0.6"/>
<!-- Glasses on beak -->
<circle cx="80" cy="55" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<circle cx="86" cy="54" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<line x1="85" y1="55" x2="81" y2="54" stroke="currentColor" stroke-width="1" opacity="0.3"/>
<!-- Beak (pointy, triangular) -->
<polygon points="90,58 102,55 90,52" fill="currentColor" opacity="0.35"/>
<!-- Arm holding book out to customer -->
<line x1="97" y1="88" x2="125" y2="78" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Book in hand (big, detailed) -->
<rect x="120" y="70" width="18" height="24" rx="2" fill="currentColor" opacity="0.35"/>
<rect x="122" y="73" width="14" height="3" rx="1" fill="currentColor" opacity="0.2"/>
<line x1="129" y1="70" x2="129" y2="94" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- Other arm resting on counter -->
<line x1="53" y1="90" x2="35" y2="108" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Counter (big) -->
<rect x="10" y="120" width="140" height="10" rx="2" fill="currentColor" opacity="0.3"/>
<rect x="15" y="130" width="130" height="6" rx="1" fill="currentColor" opacity="0.15"/>
<!-- Stack of books on counter -->
<rect x="25" y="105" width="22" height="15" rx="2" fill="currentColor" opacity="0.3"/>
<rect x="28" y="98" width="18" height="7" rx="1" fill="currentColor" opacity="0.25"/>
<rect x="30" y="93" width="14" height="5" rx="1" fill="currentColor" opacity="0.2"/>
<!-- Legs (long!) -->
<line x1="65" y1="123" x2="55" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="85" y1="123" x2="95" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="50" cy="188" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="100" cy="188" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 2 : Factrice running profile, letter carrier bag, letters flying, cap -->
<svg class="shadok-factrice" viewBox="0 0 180 200" fill="none" aria-hidden="true">
<!-- Body (small oval, leaning forward) -->
<ellipse cx="80" cy="80" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-15 80 80)"/>
<!-- Head -->
<circle cx="95" cy="48" r="15" fill="currentColor" opacity="0.3"/>
<!-- Cap -->
<rect x="82" y="34" width="28" height="7" rx="3" fill="currentColor" opacity="0.4"/>
<rect x="106" y="36" width="10" height="5" rx="2" fill="currentColor" opacity="0.3"/>
<!-- Eyes (determined, looking forward) -->
<circle cx="101" cy="46" r="1.8" fill="currentColor" opacity="0.6"/>
<circle cx="106" cy="45" r="1.8" fill="currentColor" opacity="0.6"/>
<!-- Beak (pointy, profile) -->
<polygon points="110,48 125,44 110,42" fill="currentColor" opacity="0.35"/>
<!-- Carrier bag (big, on shoulder) -->
<rect x="55" y="62" width="28" height="35" rx="4" fill="currentColor" opacity="0.3"/>
<line x1="60" y1="62" x2="75" y2="55" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- Letters flying out of bag -->
<rect x="42" y="55" width="12" height="9" rx="1" fill="currentColor" opacity="0.2" transform="rotate(-20 48 59)"/>
<rect x="35" y="45" width="10" height="7" rx="1" fill="currentColor" opacity="0.15" transform="rotate(-35 40 48)"/>
<rect x="48" y="42" width="11" height="8" rx="1" fill="currentColor" opacity="0.18" transform="rotate(10 53 46)"/>
<!-- Arm swinging back -->
<line x1="65" y1="72" x2="45" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Running legs (long strides!) -->
<line x1="72" y1="106" x2="40" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="88" y1="106" x2="130" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="35" cy="174" rx="11" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="135" cy="168" rx="11" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 3 : Cartonnier assembling cardboard box, tape gun, flat boxes nearby -->
<svg class="shadok-cartonnier" viewBox="0 0 170 220" fill="none" aria-hidden="true">
<!-- Body (small, leaning over work) -->
<ellipse cx="70" cy="100" rx="22" ry="27" fill="currentColor" opacity="0.25" transform="rotate(8 70 100)"/>
<!-- Head (looking down at box) -->
<circle cx="78" cy="65" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes (focused, looking down) -->
<circle cx="83" cy="67" r="1.8" fill="currentColor" opacity="0.6"/>
<circle cx="88" cy="68" r="1.8" fill="currentColor" opacity="0.6"/>
<!-- Beak pointing down -->
<polygon points="88,72 98,78 86,76" fill="currentColor" opacity="0.35"/>
<!-- Arm holding tape gun -->
<line x1="90" y1="95" x2="130" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Tape gun (big, detailed) -->
<circle cx="138" cy="102" r="8" fill="currentColor" opacity="0.25"/>
<rect x="132" y="99" width="18" height="6" rx="1" fill="currentColor" opacity="0.35"/>
<polygon points="150,100 158,102 150,104" fill="currentColor" opacity="0.3"/>
<!-- Other arm holding box flap -->
<line x1="50" y1="92" x2="35" y2="118" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Box being assembled (big, 3D perspective) -->
<rect x="25" y="118" width="45" height="35" rx="2" fill="currentColor" opacity="0.2"/>
<polygon points="25,118 15,108 60,108 70,118" fill="currentColor" opacity="0.15"/>
<polygon points="70,118 60,108 60,143 70,153" fill="currentColor" opacity="0.12"/>
<!-- Flap open -->
<polygon points="25,118 15,108 15,98 25,108" fill="currentColor" opacity="0.18"/>
<!-- Flat boxes nearby -->
<rect x="90" y="145" width="35" height="4" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="88" y="150" width="38" height="4" rx="1" fill="currentColor" opacity="0.15"/>
<rect x="92" y="155" width="32" height="4" rx="1" fill="currentColor" opacity="0.12"/>
<!-- Legs (long!) -->
<line x1="60" y1="127" x2="48" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="80" y1="127" x2="92" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="43" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="97" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 4 : Cycliste livreur on bicycle, package on rack, profile leaning forward -->
<svg class="shadok-cycliste" viewBox="0 0 180 200" fill="none" aria-hidden="true">
<!-- Back wheel -->
<circle cx="35" cy="160" r="22" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.25"/>
<circle cx="35" cy="160" r="3" fill="currentColor" opacity="0.3"/>
<!-- Front wheel -->
<circle cx="145" cy="160" r="22" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.25"/>
<circle cx="145" cy="160" r="3" fill="currentColor" opacity="0.3"/>
<!-- Frame -->
<line x1="35" y1="160" x2="80" y2="120" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="80" y1="120" x2="120" y2="120" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="120" y1="120" x2="145" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="35" y1="160" x2="80" y2="140" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.2"/>
<line x1="80" y1="140" x2="120" y2="120" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.2"/>
<!-- Handlebars -->
<line x1="120" y1="120" x2="130" y2="105" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<line x1="125" y1="105" x2="135" y2="105" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<!-- Saddle -->
<rect x="72" y="116" width="16" height="5" rx="2" fill="currentColor" opacity="0.3"/>
<!-- Package on rear rack (big!) -->
<rect x="15" y="125" width="30" height="22" rx="3" fill="currentColor" opacity="0.3"/>
<line x1="30" y1="125" x2="30" y2="147" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<line x1="15" y1="136" x2="45" y2="136" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- Rear rack -->
<line x1="35" y1="147" x2="35" y2="155" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="15" y1="147" x2="55" y2="147" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Body (small, leaning forward on bike) -->
<ellipse cx="90" cy="100" rx="18" ry="24" fill="currentColor" opacity="0.25" transform="rotate(-20 90 100)"/>
<!-- Head -->
<circle cx="108" cy="68" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes (focused ahead) -->
<circle cx="115" cy="65" r="1.8" fill="currentColor" opacity="0.6"/>
<circle cx="120" cy="64" r="1.8" fill="currentColor" opacity="0.6"/>
<!-- Beak -->
<polygon points="122,68 134,64 122,62" fill="currentColor" opacity="0.35"/>
<!-- Arms to handlebars -->
<line x1="100" y1="90" x2="130" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs pedaling -->
<line x1="82" y1="118" x2="72" y2="145" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="88" y1="120" x2="95" y2="148" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Feet on pedals -->
<ellipse cx="70" cy="148" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="97" cy="150" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok 5 : Lectrice satisfaite sitting in armchair, open book, wrapping paper on floor -->
<svg class="shadok-lectrice" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Armchair (big!) -->
<path d="M20 100 Q20 80 35 80 L115 80 Q130 80 130 100 L130 150 L20 150 Z" fill="currentColor" opacity="0.15"/>
<!-- Armrests -->
<rect x="10" y="90" width="16" height="55" rx="6" fill="currentColor" opacity="0.2"/>
<rect x="124" y="90" width="16" height="55" rx="6" fill="currentColor" opacity="0.2"/>
<!-- Chair legs -->
<line x1="25" y1="150" x2="22" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.2"/>
<line x1="125" y1="150" x2="128" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.2"/>
<!-- Body (sitting, small) -->
<ellipse cx="75" cy="120" rx="20" ry="25" fill="currentColor" opacity="0.25"/>
<!-- Head (tilted, reading happily) -->
<circle cx="75" cy="82" r="16" fill="currentColor" opacity="0.3"/>
<!-- Happy eyes (curved) -->
<path d="M66 79 Q69 76 72 79" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
<path d="M80 78 Q83 75 86 78" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5"/>
<!-- Small smile -->
<path d="M70 88 Q75 93 80 88" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Beak (small, happy) -->
<polygon points="88,83 98,80 88,78" fill="currentColor" opacity="0.3"/>
<!-- Arms holding open book -->
<line x1="55" y1="112" x2="40" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="95" y1="112" x2="110" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Open book (big, in lap) -->
<rect x="38" y="125" width="28" height="20" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="66" y="125" width="28" height="20" rx="1" fill="currentColor" opacity="0.25"/>
<line x1="66" y1="125" x2="66" y2="145" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<!-- Text lines on book pages -->
<line x1="42" y1="131" x2="62" y2="131" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="42" y1="135" x2="60" y2="135" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="70" y1="131" x2="90" y2="131" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="70" y1="135" x2="88" y2="135" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<!-- Legs (long, dangling from chair) -->
<line x1="62" y1="145" x2="50" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="88" y1="145" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="45" cy="198" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="105" cy="198" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<!-- Wrapping paper on floor -->
<path d="M30 175 Q45 170 55 178 Q60 182 50 185" stroke="currentColor" stroke-width="1.5" fill="currentColor" opacity="0.12"/>
<path d="M100 180 Q115 172 125 180 Q120 188 108 185" stroke="currentColor" stroke-width="1.5" fill="currentColor" opacity="0.1"/>
</svg>
<!-- Shadok 6 : Empileur balancing tower of books on head, arms out, worried eyes -->
<svg class="shadok-empileur" viewBox="0 0 140 220" fill="none" aria-hidden="true">
<!-- Precarious tower of books on head (big, detailed!) -->
<rect x="48" y="8" width="24" height="7" rx="1" fill="currentColor" opacity="0.2" transform="rotate(3 60 11)"/>
<rect x="46" y="16" width="28" height="7" rx="1" fill="currentColor" opacity="0.25" transform="rotate(-2 60 19)"/>
<rect x="44" y="24" width="32" height="7" rx="1" fill="currentColor" opacity="0.2" transform="rotate(4 60 27)"/>
<rect x="47" y="32" width="26" height="7" rx="1" fill="currentColor" opacity="0.28" transform="rotate(-3 60 35)"/>
<rect x="50" y="40" width="22" height="6" rx="1" fill="currentColor" opacity="0.22" transform="rotate(2 61 43)"/>
<!-- Head -->
<circle cx="65" cy="58" r="14" fill="currentColor" opacity="0.3"/>
<!-- Worried eyes (looking up at books) -->
<circle cx="60" cy="54" r="2.2" fill="currentColor" opacity="0.6"/>
<circle cx="70" cy="54" r="2.2" fill="currentColor" opacity="0.6"/>
<!-- Tiny worried pupils (looking up) -->
<circle cx="60" cy="53" r="1" fill="currentColor" opacity="0.35"/>
<circle cx="70" cy="53" r="1" fill="currentColor" opacity="0.35"/>
<!-- Worried eyebrows -->
<line x1="56" y1="50" x2="63" y2="51" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<line x1="74" y1="51" x2="67" y2="50" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<!-- Beak (slightly open, worried) -->
<polygon points="74,60 86,57 74,55" fill="currentColor" opacity="0.3"/>
<line x1="76" y1="58" x2="83" y2="58" stroke="currentColor" stroke-width="0.8" opacity="0.2"/>
<!-- Body (small oval, upright, tense) -->
<ellipse cx="65" cy="95" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Arms out wide for balance -->
<line x1="45" y1="88" x2="10" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="85" y1="88" x2="120" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Hands (small circles at arm ends) -->
<circle cx="8" cy="82" r="3" fill="currentColor" opacity="0.25"/>
<circle cx="122" cy="82" r="3" fill="currentColor" opacity="0.25"/>
<!-- Legs (long!) -->
<line x1="55" y1="121" x2="45" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="75" y1="121" x2="85" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Big flat feet -->
<ellipse cx="40" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
<ellipse cx="90" cy="196" rx="10" ry="4" fill="currentColor" opacity="0.3"/>
</svg>
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<div class="i-lucide-shopping-bag h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="text-lg leading-relaxed mb-10 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<!-- Bookelis CTA -->
<div class="cta-block mb-8">
<div class="cta-icon">
<div class="i-lucide-globe h-5 w-5" />
</div>
<div class="flex-1">
<h2 class="font-display text-lg font-semibold mb-1" style="color: hsl(var(--color-text))">
Commander en ligne
</h2>
<p class="text-sm mb-3" style="color: hsl(var(--color-text-muted))">
Impression à la demande, livraison chez vous.
</p>
<a
:href="content?.bookelis?.url"
target="_blank"
rel="noopener"
class="order-btn"
>
<div class="i-lucide-external-link h-4 w-4" />
{{ content?.bookelis?.label }}
</a>
</div>
</div>
<!-- Librairie -->
<div class="cta-block mb-10">
<div class="cta-icon cta-icon--accent">
<div class="i-lucide-store h-5 w-5" />
</div>
<div class="flex-1">
<h2 class="font-display text-lg font-semibold mb-2" style="color: hsl(var(--color-text))">
{{ content?.librairie?.title }}
</h2>
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content?.librairie?.text }}
</p>
</div>
</div>
<div class="text-center">
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('economique/commande')
useHead({
title: content.value?.meta?.title ?? 'Commander le livre',
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.cta-block {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
}
.cta-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: hsl(var(--color-primary) / 0.12);
color: hsl(var(--color-primary));
flex-shrink: 0;
margin-top: 0.125rem;
}
.cta-icon--accent {
background: hsl(var(--color-accent) / 0.12);
color: hsl(var(--color-accent));
}
.order-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-decoration: none;
background: hsl(var(--color-primary));
color: white;
transition: all 0.2s;
}
.order-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px hsl(var(--color-primary) / 0.3);
}
.shadok-libraire {
position: absolute;
left: 2%;
top: 5%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-1 9s ease-in-out infinite;
}
.shadok-factrice {
position: absolute;
right: 2%;
top: 4%;
width: clamp(70px, 10vw, 140px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-2 11s ease-in-out infinite;
}
.shadok-cartonnier {
position: absolute;
left: 3%;
top: 45%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-3 10s ease-in-out infinite;
}
.shadok-cycliste {
position: absolute;
left: 3%;
bottom: 8%;
width: clamp(75px, 10vw, 140px);
opacity: 0.18;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-4 8s ease-in-out infinite;
}
.shadok-lectrice {
position: absolute;
right: 2%;
bottom: 10%;
width: clamp(70px, 10vw, 135px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-5 12s ease-in-out infinite;
}
.shadok-empileur {
position: absolute;
left: 50%;
bottom: 2%;
transform: translateX(-50%);
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-6 7s ease-in-out infinite;
}
@keyframes shadok-float-1 { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes shadok-float-2 { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-10px) rotate(2deg); } }
@keyframes shadok-float-3 { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-12px) rotate(-1deg); } }
@keyframes shadok-float-4 { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-7px) rotate(1deg); } }
@keyframes shadok-float-5 { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
@keyframes shadok-float-6 { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(-9px); } }
@media (max-width: 768px) {
.shadok-libraire,
.shadok-factrice,
.shadok-cartonnier,
.shadok-cycliste,
.shadok-lectrice,
.shadok-empileur { display: none; }
}
</style>

View File

@@ -0,0 +1,647 @@
<template>
<div class="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">
<!-- body tilted forward walking -->
<ellipse cx="85" cy="105" rx="22" ry="28" fill="currentColor" opacity="0.25" transform="rotate(-8 85 105)" />
<!-- head -->
<circle cx="80" cy="65" r="16" fill="currentColor" opacity="0.3" />
<!-- eyes looking forward -->
<circle cx="88" cy="62" r="2" fill="currentColor" />
<circle cx="88" cy="68" r="1.5" fill="currentColor" />
<!-- beak pointing right -->
<path d="M94 64 L108 62 L94 68" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms up holding tray -->
<line x1="75" y1="88" x2="60" y2="42" stroke-width="3" />
<line x1="95" y1="88" x2="110" y2="42" stroke-width="3" />
<!-- big bread tray -->
<rect x="48" y="32" width="74" height="12" rx="3" fill="currentColor" opacity="0.25" />
<!-- bread loaves on tray -->
<ellipse cx="62" cy="30" rx="10" ry="6" fill="currentColor" opacity="0.3" />
<ellipse cx="85" cy="28" rx="11" ry="7" fill="currentColor" opacity="0.3" />
<ellipse cx="108" cy="30" rx="9" ry="6" fill="currentColor" opacity="0.3" />
<!-- score marks on loaves -->
<line x1="58" y1="28" x2="60" y2="32" opacity="0.4" />
<line x1="64" y1="27" x2="66" y2="31" opacity="0.4" />
<line x1="82" y1="26" x2="84" y2="30" opacity="0.4" />
<line x1="88" y1="25" x2="90" y2="29" opacity="0.4" />
<!-- flour apron -->
<path d="M70 95 Q85 92 100 95 L97 125 Q85 128 73 125 Z" fill="currentColor" opacity="0.1" />
<!-- flour dots -->
<circle cx="78" cy="100" r="1" fill="currentColor" opacity="0.3" />
<circle cx="92" cy="108" r="1.2" fill="currentColor" opacity="0.25" />
<circle cx="83" cy="115" r="0.8" fill="currentColor" opacity="0.3" />
<!-- long legs walking stride -->
<line x1="78" y1="132" x2="58" y2="185" stroke-width="3" />
<line x1="92" y1="132" x2="112" y2="180" stroke-width="3" />
<!-- big flat feet -->
<ellipse cx="52" cy="188" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="118" cy="183" rx="12" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok potier (top-right, sitting at wheel) -->
<svg class="shadok shadok-potier" viewBox="0 0 170 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body sitting, slightly hunched -->
<ellipse cx="70" cy="100" rx="23" ry="26" fill="currentColor" opacity="0.25" transform="rotate(5 70 100)" />
<!-- head looking down at wheel -->
<circle cx="65" cy="65" r="15" fill="currentColor" opacity="0.3" />
<!-- eyes looking down -->
<circle cx="60" cy="68" r="2" fill="currentColor" />
<circle cx="72" cy="70" r="1.8" fill="currentColor" />
<!-- beak pointing down-right -->
<path d="M68 76 L80 82 L66 82" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms reaching to vase -->
<line x1="50" y1="92" x2="110" y2="115" stroke-width="3" />
<line x1="90" y1="90" x2="120" y2="110" stroke-width="3" />
<!-- hands on vase -->
<circle cx="112" cy="113" r="4" fill="currentColor" opacity="0.2" />
<circle cx="122" cy="108" r="4" fill="currentColor" opacity="0.2" />
<!-- vase being shaped -->
<path d="M108 95 Q102 108 106 125 Q115 135 124 125 Q128 108 122 95" fill="currentColor" opacity="0.2" stroke="currentColor" stroke-width="2" />
<!-- vase opening -->
<ellipse cx="115" cy="95" rx="8" ry="3" fill="currentColor" opacity="0.15" />
<!-- pottery wheel -->
<ellipse cx="115" cy="140" rx="28" ry="8" fill="currentColor" opacity="0.2" />
<line x1="115" y1="148" x2="115" y2="170" stroke-width="3" />
<line x1="100" y1="170" x2="130" y2="170" stroke-width="3" />
<!-- spinning motion lines -->
<path d="M88 138 Q85 135 88 132" fill="none" opacity="0.35" />
<path d="M142 138 Q145 135 142 132" fill="none" opacity="0.35" />
<path d="M90 145 Q86 143 90 140" fill="none" opacity="0.25" />
<!-- legs folded sitting -->
<line x1="60" y1="124" x2="40" y2="165" stroke-width="3" />
<line x1="80" y1="124" x2="75" y2="170" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="34" cy="168" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="70" cy="173" rx="11" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok apicultrice (left, 40% down, with smoker and bees) -->
<svg class="shadok shadok-apicultrice" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body upright -->
<ellipse cx="75" cy="110" rx="20" ry="27" fill="currentColor" opacity="0.25" />
<!-- head -->
<circle cx="75" cy="70" r="15" fill="currentColor" opacity="0.3" />
<!-- beekeeping hat brim -->
<ellipse cx="75" cy="56" rx="22" ry="5" fill="currentColor" opacity="0.2" />
<!-- hat top -->
<path d="M58 56 Q58 42 75 40 Q92 42 92 56" fill="currentColor" opacity="0.15" />
<!-- veil hanging down -->
<path d="M53 56 L53 82 Q75 88 97 82 L97 56" fill="none" stroke-dasharray="3 2" opacity="0.3" />
<!-- eyes behind veil -->
<circle cx="70" cy="68" r="2" fill="currentColor" />
<circle cx="82" cy="66" r="1.8" fill="currentColor" />
<!-- beak -->
<path d="M80 74 L94 72 L82 78" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arm left holding smoker -->
<line x1="56" y1="100" x2="28" y2="85" stroke-width="3" />
<!-- smoker device (big) -->
<rect x="10" y="72" width="22" height="20" rx="4" fill="currentColor" opacity="0.25" />
<path d="M14 72 L14 62 Q21 58 28 62 L28 72" fill="currentColor" opacity="0.15" />
<!-- smoke puffs -->
<circle cx="18" cy="55" r="5" fill="currentColor" opacity="0.12" />
<circle cx="24" cy="48" r="4" fill="currentColor" opacity="0.08" />
<circle cx="16" cy="42" r="3" fill="currentColor" opacity="0.06" />
<!-- arm right -->
<line x1="94" y1="100" x2="110" y2="90" stroke-width="3" />
<!-- buzzing bees (small detailed dots) -->
<circle cx="120" cy="60" r="2.5" fill="currentColor" opacity="0.35" />
<line x1="118" y1="58" x2="115" y2="56" opacity="0.3" />
<line x1="122" y1="58" x2="125" y2="55" opacity="0.3" />
<circle cx="135" cy="75" r="2" fill="currentColor" opacity="0.3" />
<line x1="133" y1="73" x2="131" y2="71" opacity="0.25" />
<line x1="137" y1="73" x2="139" y2="71" opacity="0.25" />
<circle cx="110" cy="48" r="2.2" fill="currentColor" opacity="0.25" />
<line x1="108" y1="46" x2="106" y2="44" opacity="0.2" />
<line x1="112" y1="46" x2="114" y2="43" opacity="0.2" />
<circle cx="140" cy="55" r="1.8" fill="currentColor" opacity="0.2" />
<circle cx="128" cy="42" r="2" fill="currentColor" opacity="0.22" />
<!-- long legs -->
<line x1="66" y1="136" x2="55" y2="192" stroke-width="3" />
<line x1="84" y1="136" x2="95" y2="192" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="49" cy="195" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="101" cy="195" rx="11" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok forgeron (right, 35%, swinging hammer on anvil) -->
<svg class="shadok shadok-forgeron" viewBox="0 0 180 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body leaning forward, muscular feel -->
<ellipse cx="90" cy="105" rx="24" ry="30" fill="currentColor" opacity="0.3" transform="rotate(-12 90 105)" />
<!-- head in profile -->
<circle cx="100" cy="65" r="16" fill="currentColor" opacity="0.3" />
<!-- eyes focused, intense -->
<circle cx="108" cy="62" r="2.5" fill="currentColor" />
<circle cx="108" cy="70" r="2" fill="currentColor" />
<!-- beak pointing right -->
<path d="M114 65 L130 62 L114 70" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- big arm swinging hammer UP -->
<line x1="72" y1="90" x2="40" y2="40" stroke-width="3.5" />
<!-- hammer head (big!) -->
<rect x="22" y="22" width="38" height="18" rx="3" fill="currentColor" opacity="0.3" />
<!-- hammer handle end -->
<line x1="40" y1="40" x2="42" y2="30" stroke-width="4" />
<!-- other arm resting on anvil -->
<line x1="110" y1="92" x2="140" y2="130" stroke-width="3" />
<!-- anvil (big detailed) -->
<path d="M120 135 L160 135 L168 145 L112 145 Z" fill="currentColor" opacity="0.25" />
<rect x="128" y="145" width="24" height="20" fill="currentColor" opacity="0.2" />
<rect x="122" y="165" width="36" height="6" rx="2" fill="currentColor" opacity="0.25" />
<!-- hot metal on anvil -->
<rect x="132" y="130" width="20" height="5" rx="1" fill="currentColor" opacity="0.35" />
<!-- sparks flying -->
<line x1="138" y1="128" x2="132" y2="118" opacity="0.4" />
<line x1="145" y1="126" x2="150" y2="116" opacity="0.35" />
<line x1="152" y1="128" x2="160" y2="120" opacity="0.3" />
<circle cx="130" cy="115" r="1.5" fill="currentColor" opacity="0.35" />
<circle cx="155" cy="112" r="1.2" fill="currentColor" opacity="0.3" />
<circle cx="162" cy="118" r="1" fill="currentColor" opacity="0.25" />
<!-- long legs wide stance -->
<line x1="78" y1="133" x2="55" y2="190" stroke-width="3" />
<line x1="100" y1="133" x2="120" y2="192" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="48" cy="193" rx="13" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="127" cy="195" rx="13" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok maraîchère (bottom-left, pushing wheelbarrow) -->
<svg class="shadok shadok-maraichere" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body leaning forward pushing -->
<ellipse cx="120" cy="95" rx="21" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-20 120 95)" />
<!-- head tilted forward -->
<circle cx="130" cy="58" r="14" fill="currentColor" opacity="0.3" />
<!-- eyes looking down at path -->
<circle cx="136" cy="56" r="2" fill="currentColor" />
<circle cx="136" cy="63" r="1.5" fill="currentColor" />
<!-- beak pointing right-down -->
<path d="M140 60 L154 64 L140 66" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms pushing wheelbarrow handles -->
<line x1="104" y1="86" x2="72" y2="110" stroke-width="3" />
<line x1="108" y1="80" x2="72" y2="100" stroke-width="3" />
<!-- wheelbarrow body (big!) -->
<path d="M10 80 L70 80 L75 120 L5 120 Z" fill="currentColor" opacity="0.2" />
<path d="M10 80 L70 80 L75 120 L5 120 Z" />
<!-- wheelbarrow handles -->
<line x1="70" y1="90" x2="72" y2="110" stroke-width="3" />
<line x1="70" y1="100" x2="72" y2="100" stroke-width="2" />
<!-- wheel -->
<circle cx="12" cy="128" r="12" fill="currentColor" opacity="0.15" />
<circle cx="12" cy="128" r="12" />
<circle cx="12" cy="128" r="3" fill="currentColor" opacity="0.3" />
<!-- vegetables in wheelbarrow -->
<circle cx="22" cy="72" r="7" fill="currentColor" opacity="0.3" />
<circle cx="38" cy="70" r="8" fill="currentColor" opacity="0.25" />
<circle cx="55" cy="73" r="6" fill="currentColor" opacity="0.3" />
<ellipse cx="30" cy="76" rx="5" ry="8" fill="currentColor" opacity="0.2" transform="rotate(15 30 76)" />
<!-- carrot tops -->
<line x1="22" y1="66" x2="18" y2="58" opacity="0.35" />
<line x1="22" y1="66" x2="25" y2="57" opacity="0.35" />
<line x1="55" y1="67" x2="52" y2="60" opacity="0.3" />
<line x1="55" y1="67" x2="58" y2="59" opacity="0.3" />
<!-- long legs striding -->
<line x1="112" y1="118" x2="100" y2="180" stroke-width="3" />
<line x1="128" y1="118" x2="150" y2="178" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="94" cy="183" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="156" cy="181" rx="12" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<!-- Shadok tisserand (bottom-right, sitting at loom) -->
<svg class="shadok shadok-tisserand" viewBox="0 0 170 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body sitting upright -->
<ellipse cx="55" cy="100" rx="20" ry="25" fill="currentColor" opacity="0.25" />
<!-- head looking down at loom -->
<circle cx="55" cy="65" r="15" fill="currentColor" opacity="0.3" />
<!-- eyes focused downward -->
<circle cx="62" cy="66" r="2" fill="currentColor" />
<circle cx="60" cy="72" r="1.5" fill="currentColor" />
<!-- beak pointing right-down -->
<path d="M66 70 L78 74 L64 76" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arms reaching to loom -->
<line x1="72" y1="92" x2="100" y2="80" stroke-width="3" />
<line x1="74" y1="100" x2="105" y2="100" stroke-width="3" />
<!-- loom frame (big detailed) -->
<rect x="95" y="55" width="60" height="80" rx="3" fill="none" />
<!-- vertical loom posts -->
<line x1="95" y1="55" x2="95" y2="135" stroke-width="3" />
<line x1="155" y1="55" x2="155" y2="135" stroke-width="3" />
<!-- top beam -->
<line x1="95" y1="55" x2="155" y2="55" stroke-width="3" />
<!-- warp threads (vertical) -->
<line x1="105" y1="55" x2="105" y2="135" opacity="0.3" />
<line x1="115" y1="55" x2="115" y2="135" opacity="0.3" />
<line x1="125" y1="55" x2="125" y2="135" opacity="0.3" />
<line x1="135" y1="55" x2="135" y2="135" opacity="0.3" />
<line x1="145" y1="55" x2="145" y2="135" opacity="0.3" />
<!-- weft threads (horizontal, partial = work in progress) -->
<line x1="95" y1="70" x2="155" y2="70" opacity="0.25" />
<line x1="95" y1="80" x2="155" y2="80" opacity="0.25" />
<line x1="95" y1="90" x2="145" y2="90" opacity="0.25" />
<line x1="95" y1="100" x2="135" y2="100" opacity="0.2" />
<!-- shuttle in hand -->
<ellipse cx="105" cy="100" rx="8" ry="3" fill="currentColor" opacity="0.3" transform="rotate(-5 105 100)" />
<!-- woven fabric area -->
<rect x="97" y="62" width="56" height="28" fill="currentColor" opacity="0.08" />
<!-- legs folded sitting on stool -->
<line x1="45" y1="123" x2="30" y2="178" stroke-width="3" />
<line x1="65" y1="123" x2="70" y2="180" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="24" cy="181" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="76" cy="183" rx="11" ry="4" fill="currentColor" opacity="0.3" />
<!-- stool -->
<rect x="38" y="124" width="30" height="6" rx="2" fill="currentColor" opacity="0.15" />
<line x1="42" y1="130" x2="40" y2="148" stroke-width="2" opacity="0.3" />
<line x1="64" y1="130" x2="66" y2="148" stroke-width="2" opacity="0.3" />
</g>
</svg>
<!-- Shadok berger (center-bottom, walking with sheep) -->
<svg class="shadok shadok-berger" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body walking, slight lean -->
<ellipse cx="85" cy="90" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-5 85 90)" />
<!-- head 3/4 view -->
<circle cx="88" cy="52" r="16" fill="currentColor" opacity="0.3" />
<!-- eyes looking ahead, slightly different directions -->
<circle cx="94" cy="48" r="2" fill="currentColor" />
<circle cx="96" cy="56" r="1.8" fill="currentColor" />
<!-- beak -->
<path d="M100 52 L114 50 L100 56" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arm holding staff -->
<line x1="68" y1="82" x2="50" y2="60" stroke-width="3" />
<!-- shepherd's crook (big, detailed) -->
<line x1="50" y1="60" x2="44" y2="10" stroke-width="3" />
<path d="M44 10 Q44 0 54 0 Q62 0 62 10 Q62 18 54 18" fill="none" stroke-width="3" />
<!-- other arm relaxed -->
<line x1="104" y1="84" x2="118" y2="95" stroke-width="3" />
<!-- long legs walking -->
<line x1="76" y1="114" x2="60" y2="175" stroke-width="3" />
<line x1="94" y1="114" x2="110" y2="172" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="54" cy="178" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="116" cy="175" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<!-- sheep 1 (following) -->
<ellipse cx="140" cy="160" rx="14" ry="10" fill="currentColor" opacity="0.18" />
<circle cx="150" cy="152" r="6" fill="currentColor" opacity="0.15" />
<circle cx="153" cy="150" r="1" fill="currentColor" />
<line x1="132" y1="170" x2="132" y2="180" stroke-width="2" opacity="0.3" />
<line x1="148" y1="170" x2="148" y2="180" stroke-width="2" opacity="0.3" />
<!-- sheep 2 -->
<ellipse cx="158" cy="172" rx="12" ry="8" fill="currentColor" opacity="0.14" />
<circle cx="166" cy="166" r="5" fill="currentColor" opacity="0.12" />
<circle cx="168" cy="164" r="0.8" fill="currentColor" />
<line x1="152" y1="180" x2="152" y2="188" stroke-width="1.5" opacity="0.25" />
<line x1="164" y1="180" x2="164" y2="188" stroke-width="1.5" opacity="0.25" />
<!-- sheep 3 (smaller, behind) -->
<ellipse cx="170" cy="178" rx="9" ry="6" fill="currentColor" opacity="0.1" />
<circle cx="176" cy="174" r="4" fill="currentColor" opacity="0.08" />
<!-- dog at side -->
<ellipse cx="125" cy="170" rx="8" ry="5" fill="currentColor" opacity="0.2" />
<circle cx="130" cy="164" r="4" fill="currentColor" opacity="0.18" />
<circle cx="132" cy="163" r="1" fill="currentColor" />
<line x1="118" y1="168" x2="112" y2="165" stroke-width="1.5" opacity="0.3" />
<line x1="120" y1="175" x2="120" y2="182" stroke-width="1.5" opacity="0.25" />
<line x1="130" y1="175" x2="130" y2="182" stroke-width="1.5" opacity="0.25" />
</g>
</svg>
<!-- Shadok vigneronne (right, 55%, carrying grape basket) -->
<svg class="shadok shadok-vigneronne" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<g stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- body seen from 3/4 back, leaning under weight -->
<ellipse cx="80" cy="105" rx="22" ry="28" fill="currentColor" opacity="0.25" transform="rotate(10 80 105)" />
<!-- head turned slightly -->
<circle cx="72" cy="65" r="15" fill="currentColor" opacity="0.3" />
<!-- eyes looking to the side -->
<circle cx="64" cy="62" r="2" fill="currentColor" />
<circle cx="66" cy="69" r="1.5" fill="currentColor" />
<!-- beak pointing left -->
<path d="M58 65 L44 62 L58 69" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.2" />
<!-- arm back holding basket strap -->
<line x1="98" y1="92" x2="115" y2="75" stroke-width="3" />
<!-- arm front with pruning shears -->
<line x1="62" y1="95" x2="38" y2="80" stroke-width="3" />
<!-- pruning shears (big) -->
<line x1="38" y1="80" x2="26" y2="68" stroke-width="3" />
<line x1="38" y1="80" x2="28" y2="78" stroke-width="3" />
<circle cx="36" cy="78" r="3" fill="currentColor" opacity="0.2" />
<!-- blade shapes -->
<path d="M26 68 Q22 72 28 74" fill="none" stroke-width="2" />
<path d="M28 78 Q22 76 24 72" fill="none" stroke-width="2" />
<!-- big basket on back with grapes -->
<path d="M100 60 L130 60 L135 120 L95 120 Z" fill="currentColor" opacity="0.2" />
<path d="M100 60 L130 60 L135 120 L95 120 Z" />
<!-- basket weave texture -->
<line x1="100" y1="75" x2="133" y2="75" opacity="0.2" />
<line x1="98" y1="90" x2="134" y2="90" opacity="0.2" />
<line x1="97" y1="105" x2="135" y2="105" opacity="0.2" />
<!-- grapes overflowing -->
<circle cx="108" cy="55" r="4" fill="currentColor" opacity="0.3" />
<circle cx="116" cy="53" r="4.5" fill="currentColor" opacity="0.3" />
<circle cx="124" cy="55" r="4" fill="currentColor" opacity="0.25" />
<circle cx="112" cy="48" r="3.5" fill="currentColor" opacity="0.25" />
<circle cx="120" cy="47" r="3.5" fill="currentColor" opacity="0.2" />
<circle cx="105" cy="52" r="3" fill="currentColor" opacity="0.2" />
<!-- grape stems -->
<line x1="115" y1="43" x2="115" y2="36" opacity="0.3" />
<line x1="113" y1="36" x2="117" y2="36" opacity="0.3" />
<!-- grapevine nearby -->
<line x1="148" y1="30" x2="148" y2="130" stroke-width="2" opacity="0.2" />
<path d="M148 50 Q158 45 162 55" fill="none" opacity="0.2" />
<path d="M148 75 Q160 70 165 80" fill="none" opacity="0.2" />
<circle cx="160" cy="58" r="3" fill="currentColor" opacity="0.12" />
<circle cx="163" cy="83" r="3" fill="currentColor" opacity="0.1" />
<!-- grape leaf -->
<path d="M148 90 Q155 85 158 90 Q160 95 155 98 Q150 95 148 90" fill="currentColor" opacity="0.12" />
<!-- long legs -->
<line x1="70" y1="132" x2="55" y2="192" stroke-width="3" />
<line x1="90" y1="132" x2="105" y2="190" stroke-width="3" />
<!-- flat feet -->
<ellipse cx="49" cy="195" rx="12" ry="4" fill="currentColor" opacity="0.3" />
<ellipse cx="111" cy="193" rx="12" ry="4" fill="currentColor" opacity="0.3" />
</g>
</svg>
<div class="container-content">
<!-- Header -->
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-8">
<!-- Monnaie libre -->
<NuxtLink to="/economique/monnaie-libre" class="item-card group">
<div class="item-header">
<div class="item-icon">
<span class="g1-icon">Ğ1</span>
</div>
<h2 class="font-display text-xl font-bold" style="color: hsl(var(--color-text))">
Monnaie libre
</h2>
</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.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
<!-- Modèle économique — bloc livre -->
<div class="book-block">
<HomeBookSection
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
</div>
<!-- Productions collectives -->
<NuxtLink to="/economique/productions-collectives" class="item-card group">
<div class="item-header">
<div class="item-icon">
<div class="i-lucide-users h-5 w-5" />
</div>
<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>
</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.
</p>
<div class="mt-3 inline-flex items-center gap-1 text-sm text-primary group-hover:text-primary/80 transition-colors">
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</div>
</NuxtLink>
</div>
</div>
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('economique')
useHead({
title: content.value?.meta?.title ?? 'Autonomie économique',
})
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.item-card {
display: block;
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, transform 0.12s ease;
text-decoration: none;
}
.item-card:hover {
border-color: hsl(var(--color-primary) / 0.2);
transform: translateY(-3px);
box-shadow: 0 8px 24px hsl(var(--color-primary) / 0.08);
}
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.g1-icon {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.1rem;
line-height: 1;
}
.item-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);
}
.book-block {
padding: 1.5rem;
border-radius: 0.75rem;
border: 1px solid hsl(var(--color-text) / 0.08);
background: hsl(var(--color-surface));
}
/* Shadok illustrations */
.shadok {
position: absolute;
pointer-events: none;
width: clamp(70px, 10vw, 140px);
z-index: 0;
}
.shadok-boulangere {
top: 1%;
left: 2%;
opacity: 0.22;
color: hsl(var(--color-primary));
animation: shadok-float-1 9s ease-in-out infinite;
}
.shadok-potier {
top: 1%;
right: 2%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-2 11s ease-in-out infinite;
}
.shadok-apicultrice {
left: 2%;
top: 40%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-3 10s ease-in-out infinite;
}
.shadok-forgeron {
right: 3%;
top: 35%;
opacity: 0.24;
color: hsl(var(--color-primary));
animation: shadok-float-4 8s ease-in-out infinite;
}
.shadok-maraichere {
bottom: 14%;
left: 1%;
opacity: 0.18;
color: hsl(var(--color-primary));
animation: shadok-float-5 12s ease-in-out infinite;
}
.shadok-tisserand {
bottom: 12%;
right: 1%;
opacity: 0.22;
color: hsl(var(--color-accent));
animation: shadok-float-6 9.5s ease-in-out infinite;
}
.shadok-berger {
bottom: 2%;
left: 50%;
transform: translateX(-50%);
opacity: 0.28;
color: hsl(var(--color-primary));
animation: shadok-float-7 11s ease-in-out infinite;
}
.shadok-vigneronne {
right: 2%;
top: 55%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-8 7.5s ease-in-out infinite;
}
@keyframes shadok-float-1 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-2 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-3 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes shadok-float-4 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-5 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-11px); }
}
@keyframes shadok-float-6 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-7 {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-10px); }
}
@keyframes shadok-float-8 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@media (max-width: 768px) {
.shadok {
display: none;
}
}
</style>

View File

@@ -14,7 +14,7 @@
<nav class="mt-16 flex items-center justify-between border-t border-white/8 pt-8"> <nav class="mt-16 flex items-center justify-between border-t border-white/8 pt-8">
<NuxtLink <NuxtLink
v-if="prevChapter" v-if="prevChapter"
:to="`/lire/${prevChapter.stem}`" :to="`/economique/modele-eco/${prevChapter.stem?.split('/').pop()}`"
class="btn-ghost gap-2" class="btn-ghost gap-2"
> >
<div class="i-lucide-arrow-left h-4 w-4" /> <div class="i-lucide-arrow-left h-4 w-4" />
@@ -24,7 +24,7 @@
<NuxtLink <NuxtLink
v-if="nextChapter" v-if="nextChapter"
:to="`/lire/${nextChapter.stem}`" :to="`/economique/modele-eco/${nextChapter.stem?.split('/').pop()}`"
class="btn-ghost gap-2" class="btn-ghost gap-2"
> >
<span class="text-sm">{{ nextChapter.title }}</span> <span class="text-sm">{{ nextChapter.title }}</span>
@@ -64,7 +64,7 @@ const { data: allChapters } = await useAsyncData('book-nav', () =>
) )
const currentIndex = computed(() => const currentIndex = computed(() =>
allChapters.value?.findIndex(c => c.stem === slug) ?? -1, allChapters.value?.findIndex(c => c.stem?.split('/').pop() === slug) ?? -1,
) )
const prevChapter = computed(() => { const prevChapter = computed(() => {

View File

@@ -0,0 +1,583 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok illustrations -->
<!-- 1. Typographe placing movable type in composing stick, profile view -->
<svg class="shadok shadok-typographe" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (small oval, profile facing right) -->
<ellipse cx="70" cy="72" rx="22" ry="28" fill="currentColor" opacity="0.28"/>
<!-- head -->
<circle cx="78" cy="36" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak (pointy, profile right) -->
<polygon points="94,34 110,38 94,42" fill="currentColor" opacity="0.35"/>
<!-- eyes (profile one visible) -->
<circle cx="84" cy="33" r="2.5" fill="currentColor" opacity="0.6"/>
<!-- apron -->
<path d="M52 60 L88 60 L85 100 L55 100 Z" fill="currentColor" opacity="0.15"/>
<line x1="70" y1="60" x2="70" y2="100" stroke="currentColor" stroke-width="1" opacity="0.2"/>
<!-- arm reaching to composing stick -->
<line x1="88" y1="65" x2="120" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- other arm holding type block -->
<line x1="52" y1="68" x2="35" y2="58" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- composing stick (big, detailed) -->
<rect x="110" y="40" width="50" height="14" rx="2" fill="currentColor" opacity="0.2"/>
<rect x="110" y="42" width="50" height="10" rx="1" stroke="currentColor" stroke-width="1" fill="none" opacity="0.25"/>
<!-- type blocks in stick -->
<rect x="114" y="44" width="6" height="7" rx="0.5" fill="currentColor" opacity="0.3"/>
<rect x="122" y="44" width="5" height="7" rx="0.5" fill="currentColor" opacity="0.25"/>
<rect x="129" y="44" width="7" height="7" rx="0.5" fill="currentColor" opacity="0.3"/>
<rect x="138" y="44" width="5" height="7" rx="0.5" fill="currentColor" opacity="0.22"/>
<rect x="145" y="44" width="6" height="7" rx="0.5" fill="currentColor" opacity="0.28"/>
<!-- tiny letter on held block -->
<rect x="30" y="52" width="8" height="10" rx="1" fill="currentColor" opacity="0.25"/>
<text x="32" y="60" font-size="6" fill="currentColor" opacity="0.5" font-family="serif">A</text>
<!-- long legs -->
<line x1="62" y1="98" x2="50" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="78" y1="98" x2="90" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="45" cy="168" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="95" cy="168" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 2. Lectrice sitting in armchair, legs crossed, book on lap, reading glasses -->
<svg class="shadok shadok-lectrice" viewBox="0 0 180 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- armchair back -->
<path d="M30 55 Q25 50 28 30 L85 28 Q88 50 83 55" fill="currentColor" opacity="0.12"/>
<!-- armchair seat -->
<path d="M25 55 L88 55 L92 90 L20 90 Z" fill="currentColor" opacity="0.15"/>
<!-- armchair arms -->
<rect x="15" y="45" width="12" height="45" rx="5" fill="currentColor" opacity="0.15"/>
<rect x="85" y="45" width="12" height="45" rx="5" fill="currentColor" opacity="0.15"/>
<!-- body (seated, small) -->
<ellipse cx="58" cy="68" rx="20" ry="25" fill="currentColor" opacity="0.28"/>
<!-- head -->
<circle cx="58" cy="32" r="15" fill="currentColor" opacity="0.3"/>
<!-- beak (small, facing right-down toward book) -->
<polygon points="70,35 80,40 70,42" fill="currentColor" opacity="0.3"/>
<!-- reading glasses -->
<circle cx="52" cy="30" r="6" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<circle cx="65" cy="30" r="6" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.4"/>
<line x1="58" y1="30" x2="59" y2="30" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<!-- eyes behind glasses (looking down) -->
<circle cx="53" cy="31" r="2" fill="currentColor" opacity="0.55"/>
<circle cx="64" cy="31" r="2" fill="currentColor" opacity="0.55"/>
<!-- arms holding open book on lap -->
<line x1="40" y1="60" x2="35" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="76" y1="60" x2="80" y2="82" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- open book on lap -->
<path d="M32 80 L58 88 L84 80 L84 95 L58 103 L32 95 Z" fill="currentColor" opacity="0.18"/>
<line x1="58" y1="88" x2="58" y2="103" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<!-- text lines on pages -->
<line x1="37" y1="86" x2="54" y2="91" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<line x1="37" y1="89" x2="54" y2="94" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<line x1="62" y1="91" x2="79" y2="86" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<line x1="62" y1="94" x2="79" y2="89" stroke="currentColor" stroke-width="0.7" opacity="0.2"/>
<!-- crossed legs (long!) -->
<line x1="48" y1="90" x2="30" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="68" y1="90" x2="55" y2="150" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- the crossed leg goes over -->
<line x1="55" y1="150" x2="75" y2="140" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.22"/>
<!-- big flat feet -->
<ellipse cx="25" cy="168" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="79" cy="142" rx="9" ry="3.5" fill="currentColor" opacity="0.22"/>
<!-- cup of tea nearby -->
<rect x="130" y="78" width="14" height="16" rx="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="137" cy="78" rx="7" ry="2.5" fill="currentColor" opacity="0.25"/>
<!-- tea handle -->
<path d="M144 83 Q152 86 144 92" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<!-- steam -->
<path d="M134 73 Q132 68 135 64" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.15"/>
<path d="M139 74 Q141 69 138 65" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.15"/>
</svg>
<!-- 3. Calligraphe standing at tilted drafting table, large quill, ink flourishes -->
<svg class="shadok shadok-calligraphe" viewBox="0 0 180 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- drafting table (tilted, big) -->
<rect x="75" y="50" width="65" height="85" rx="3" fill="currentColor" opacity="0.12" transform="rotate(-15 108 92)"/>
<!-- table legs -->
<line x1="85" y1="130" x2="78" y2="195" stroke="currentColor" stroke-width="2.5" opacity="0.18"/>
<line x1="140" y1="115" x2="150" y2="195" stroke="currentColor" stroke-width="2.5" opacity="0.18"/>
<!-- paper on table -->
<rect x="85" y="58" width="48" height="65" rx="1" fill="currentColor" opacity="0.08" transform="rotate(-15 109 90)"/>
<!-- ink flourishes on paper -->
<path d="M95 80 Q105 70 115 82 Q120 90 110 95" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.25" transform="rotate(-15 105 85)"/>
<path d="M100 95 Q108 88 118 98" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.2" transform="rotate(-15 109 93)"/>
<!-- body (3/4 view, facing table) -->
<ellipse cx="55" cy="95" rx="21" ry="27" fill="currentColor" opacity="0.28"/>
<!-- head (turned toward table) -->
<circle cx="62" cy="58" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak pointing at paper -->
<polygon points="75,55 90,52 78,60" fill="currentColor" opacity="0.32"/>
<!-- eyes (looking at paper, slightly different directions) -->
<circle cx="66" cy="55" r="2.5" fill="currentColor" opacity="0.6"/>
<circle cx="58" cy="54" r="2" fill="currentColor" opacity="0.5"/>
<!-- arm holding large quill -->
<line x1="72" y1="85" x2="105" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- large quill -->
<line x1="105" y1="65" x2="100" y2="80" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<path d="M105 65 L115 30 Q108 45 100 40 Q105 55 105 65" fill="currentColor" opacity="0.2"/>
<!-- other arm resting on table edge -->
<line x1="40" y1="88" x2="80" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- ink trailing from quill tip -->
<path d="M100 80 Q95 90 98 100 Q102 108 96 115" stroke="currentColor" stroke-width="0.8" fill="none" opacity="0.2"/>
<!-- long legs -->
<line x1="45" y1="120" x2="35" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="65" y1="120" x2="72" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="30" cy="193" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="77" cy="193" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 4. Relieur sewing book spine with needle and thread, stack of signatures -->
<svg class="shadok shadok-relieur" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (leaning forward over work) -->
<ellipse cx="85" cy="80" rx="22" ry="26" fill="currentColor" opacity="0.28" transform="rotate(10 85 80)"/>
<!-- head (tilted down, focused) -->
<circle cx="95" cy="46" r="15" fill="currentColor" opacity="0.3"/>
<!-- beak (pointing down at work) -->
<polygon points="105,52 115,62 103,58" fill="currentColor" opacity="0.32"/>
<!-- eyes (both looking down at different angles) -->
<circle cx="92" cy="44" r="2" fill="currentColor" opacity="0.55"/>
<circle cx="100" cy="46" r="2.5" fill="currentColor" opacity="0.6"/>
<!-- arm holding needle high -->
<line x1="100" y1="70" x2="130" y2="40" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- needle -->
<line x1="130" y1="40" x2="135" y2="32" stroke="currentColor" stroke-width="2" opacity="0.45"/>
<!-- thread from needle down to book -->
<path d="M132 38 Q140 55 125 75 Q115 90 120 100" stroke="currentColor" stroke-width="1" fill="none" stroke-dasharray="4 3" opacity="0.3"/>
<!-- other arm holding book spine -->
<line x1="68" y1="75" x2="50" y2="95" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- book being bound (open spine view, big) -->
<path d="M40 90 L60 85 L60 125 L40 130 Z" fill="currentColor" opacity="0.18"/>
<path d="M60 85 L80 90 L80 130 L60 125 Z" fill="currentColor" opacity="0.14"/>
<!-- stitching holes along spine -->
<circle cx="60" cy="92" r="1.2" fill="currentColor" opacity="0.35"/>
<circle cx="60" cy="100" r="1.2" fill="currentColor" opacity="0.35"/>
<circle cx="60" cy="108" r="1.2" fill="currentColor" opacity="0.35"/>
<circle cx="60" cy="116" r="1.2" fill="currentColor" opacity="0.35"/>
<!-- stack of folded signatures nearby -->
<rect x="10" y="125" width="30" height="5" rx="1" fill="currentColor" opacity="0.18"/>
<rect x="12" y="119" width="28" height="5" rx="1" fill="currentColor" opacity="0.15"/>
<rect x="11" y="113" width="29" height="5" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="13" y="107" width="27" height="5" rx="1" fill="currentColor" opacity="0.16"/>
<!-- long legs -->
<line x1="75" y1="104" x2="65" y2="172" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="95" y1="104" x2="105" y2="172" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="60" cy="175" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="110" cy="175" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 5. Conteuse on small stage, arms dramatically wide, 3 tiny shadoks below -->
<svg class="shadok shadok-conteuse" viewBox="0 0 180 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- small stage/box -->
<rect x="55" y="110" width="60" height="20" rx="3" fill="currentColor" opacity="0.18"/>
<rect x="58" y="108" width="54" height="4" rx="1.5" fill="currentColor" opacity="0.22"/>
<!-- body (on stage, upright, dramatic) -->
<ellipse cx="85" cy="85" rx="20" ry="26" fill="currentColor" opacity="0.28"/>
<!-- head (thrown back slightly) -->
<circle cx="85" cy="50" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak (open, telling story) -->
<polygon points="98,46 112,42 100,52" fill="currentColor" opacity="0.3"/>
<polygon points="100,52 112,55 98,54" fill="currentColor" opacity="0.22"/>
<!-- wide expressive eyes -->
<circle cx="82" cy="46" r="3" fill="currentColor" opacity="0.6"/>
<circle cx="92" cy="45" r="2.5" fill="currentColor" opacity="0.55"/>
<!-- arms dramatically wide -->
<line x1="65" y1="75" x2="15" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<line x1="105" y1="75" x2="160" y2="50" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- gesture sparkles -->
<circle cx="10" cy="45" r="2" fill="currentColor" opacity="0.15"/>
<circle cx="165" cy="45" r="2" fill="currentColor" opacity="0.15"/>
<circle cx="18" cy="38" r="1.5" fill="currentColor" opacity="0.12"/>
<circle cx="157" cy="38" r="1.5" fill="currentColor" opacity="0.12"/>
<!-- long legs (standing on stage) -->
<line x1="77" y1="108" x2="70" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="93" y1="108" x2="100" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet on stage -->
<ellipse cx="67" cy="112" rx="8" ry="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="103" cy="112" rx="8" ry="3" fill="currentColor" opacity="0.2"/>
<!-- audience: 3 tiny shadoks below stage -->
<!-- tiny shadok 1 (left) -->
<ellipse cx="40" cy="165" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="40" cy="152" r="6" fill="currentColor" opacity="0.22"/>
<circle cx="39" cy="151" r="1" fill="currentColor" opacity="0.45"/>
<circle cx="42" cy="151" r="1" fill="currentColor" opacity="0.45"/>
<polygon points="45,151 50,153 45,155" fill="currentColor" opacity="0.2"/>
<line x1="37" y1="175" x2="35" y2="195" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="43" y1="175" x2="45" y2="195" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<!-- tiny shadok 2 (center) -->
<ellipse cx="85" cy="168" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="85" cy="155" r="6" fill="currentColor" opacity="0.22"/>
<circle cx="83" cy="154" r="1" fill="currentColor" opacity="0.45"/>
<circle cx="87" cy="154" r="1" fill="currentColor" opacity="0.45"/>
<polygon points="90,154 95,156 90,158" fill="currentColor" opacity="0.2"/>
<line x1="82" y1="178" x2="80" y2="198" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="88" y1="178" x2="90" y2="198" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<!-- tiny shadok 3 (right) -->
<ellipse cx="130" cy="163" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="130" cy="150" r="6" fill="currentColor" opacity="0.22"/>
<circle cx="128" cy="149" r="1" fill="currentColor" opacity="0.45"/>
<circle cx="132" cy="149" r="1" fill="currentColor" opacity="0.45"/>
<polygon points="135,149 140,151 135,153" fill="currentColor" opacity="0.2"/>
<line x1="127" y1="173" x2="125" y2="193" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="133" y1="173" x2="135" y2="193" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
</svg>
<!-- 6. Correcteur leaning forward, magnifying glass over manuscript, red pen -->
<svg class="shadok shadok-correcteur" viewBox="0 0 170 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (leaning forward heavily) -->
<ellipse cx="75" cy="82" rx="21" ry="28" fill="currentColor" opacity="0.28" transform="rotate(20 75 82)"/>
<!-- head (craned forward) -->
<circle cx="95" cy="48" r="15" fill="currentColor" opacity="0.3"/>
<!-- beak (pursed, critical) -->
<polygon points="108,45 118,48 108,52" fill="currentColor" opacity="0.3"/>
<!-- eyes (squinting, one bigger peering through glass) -->
<circle cx="92" cy="45" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="101" cy="46" r="3" fill="currentColor" opacity="0.6"/>
<!-- arm holding magnifying glass -->
<line x1="90" y1="72" x2="120" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- magnifying glass (big) -->
<circle cx="128" cy="98" r="18" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.3"/>
<circle cx="128" cy="98" r="16" fill="currentColor" opacity="0.06"/>
<!-- handle -->
<line x1="140" y1="112" x2="155" y2="135" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- manuscript under magnifying glass -->
<rect x="95" y="115" width="55" height="70" rx="2" fill="currentColor" opacity="0.1"/>
<!-- text lines on manuscript -->
<line x1="100" y1="125" x2="140" y2="125" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="132" x2="145" y2="132" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="139" x2="138" y2="139" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="146" x2="142" y2="146" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<line x1="100" y1="153" x2="135" y2="153" stroke="currentColor" stroke-width="0.8" opacity="0.18"/>
<!-- crossed-out text (red corrections) -->
<line x1="100" y1="132" x2="130" y2="132" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<line x1="105" y1="146" x2="125" y2="146" stroke="currentColor" stroke-width="1.5" opacity="0.35"/>
<!-- other arm holding red pen -->
<line x1="58" y1="78" x2="35" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- red pen -->
<line x1="35" y1="100" x2="25" y2="115" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<circle cx="24" cy="117" r="1.5" fill="currentColor" opacity="0.4"/>
<!-- long legs -->
<line x1="65" y1="108" x2="50" y2="178" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="82" y1="105" x2="95" y2="178" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="45" cy="181" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="100" cy="181" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 7. Colporteur walking profile, books in wooden crate on back, walking stick, hat -->
<svg class="shadok shadok-colporteur" viewBox="0 0 160 210" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body (profile, walking right, leaning forward under weight) -->
<ellipse cx="70" cy="72" rx="20" ry="25" fill="currentColor" opacity="0.28" transform="rotate(15 70 72)"/>
<!-- head -->
<circle cx="82" cy="38" r="14" fill="currentColor" opacity="0.3"/>
<!-- hat (brimmed) -->
<ellipse cx="82" cy="26" rx="18" ry="5" fill="currentColor" opacity="0.25"/>
<rect x="72" y="16" width="20" height="12" rx="3" fill="currentColor" opacity="0.2"/>
<!-- beak (profile right) -->
<polygon points="94,36 106,40 94,43" fill="currentColor" opacity="0.32"/>
<!-- eye (profile one visible, determined) -->
<circle cx="88" cy="35" r="2.5" fill="currentColor" opacity="0.6"/>
<!-- wooden crate on back (big, loaded with books) -->
<rect x="25" y="35" width="40" height="50" rx="3" stroke="currentColor" stroke-width="2" fill="currentColor" opacity="0.12"/>
<!-- strap over shoulder -->
<line x1="45" y1="35" x2="78" y2="55" stroke="currentColor" stroke-width="2" opacity="0.25"/>
<line x1="65" y1="35" x2="85" y2="60" stroke="currentColor" stroke-width="2" opacity="0.25"/>
<!-- books in crate (visible spines) -->
<rect x="28" y="38" width="6" height="44" rx="1" fill="currentColor" opacity="0.22"/>
<rect x="36" y="40" width="5" height="42" rx="1" fill="currentColor" opacity="0.18"/>
<rect x="43" y="37" width="7" height="45" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="52" y="39" width="5" height="43" rx="1" fill="currentColor" opacity="0.16"/>
<rect x="58" y="38" width="4" height="44" rx="1" fill="currentColor" opacity="0.2"/>
<!-- arm forward with walking stick -->
<line x1="88" y1="65" x2="115" y2="80" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- walking stick (long) -->
<line x1="115" y1="80" x2="125" y2="195" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
<!-- other arm back holding strap -->
<line x1="55" y1="68" x2="45" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- long legs (walking stride) front leg forward, back leg behind -->
<line x1="78" y1="95" x2="100" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="62" y1="95" x2="40" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet (walking) -->
<ellipse cx="105" cy="173" rx="11" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="35" cy="173" rx="10" ry="4" fill="currentColor" opacity="0.22"/>
</svg>
<!-- 8. Illustratrice at easel, brush in one hand, palette in other, canvas showing a shadok -->
<svg class="shadok shadok-illustratrice" viewBox="0 0 180 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- easel legs -->
<line x1="105" y1="30" x2="85" y2="210" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="145" y1="30" x2="165" y2="210" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<line x1="125" y1="60" x2="125" y2="210" stroke="currentColor" stroke-width="2" opacity="0.18"/>
<!-- canvas on easel -->
<rect x="95" y="25" width="60" height="75" rx="2" fill="currentColor" opacity="0.08"/>
<rect x="95" y="25" width="60" height="75" rx="2" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<!-- tiny shadok drawing on canvas! -->
<ellipse cx="125" cy="65" rx="8" ry="10" fill="currentColor" opacity="0.18"/>
<circle cx="125" cy="52" r="6" fill="currentColor" opacity="0.2"/>
<polygon points="130,51 136,53 130,55" fill="currentColor" opacity="0.18"/>
<circle cx="123" cy="51" r="1" fill="currentColor" opacity="0.3"/>
<circle cx="127" cy="51" r="1" fill="currentColor" opacity="0.3"/>
<line x1="121" y1="75" x2="118" y2="88" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="129" y1="75" x2="132" y2="88" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<!-- body (3/4 view, standing back from easel) -->
<ellipse cx="55" cy="90" rx="22" ry="28" fill="currentColor" opacity="0.28"/>
<!-- head (looking at canvas) -->
<circle cx="62" cy="52" r="16" fill="currentColor" opacity="0.3"/>
<!-- beak (profile right, toward canvas) -->
<polygon points="75,50 88,54 76,56" fill="currentColor" opacity="0.3"/>
<!-- eyes (artistic scrutiny, different sizes) -->
<circle cx="60" cy="49" r="2" fill="currentColor" opacity="0.55"/>
<circle cx="69" cy="50" r="2.8" fill="currentColor" opacity="0.6"/>
<!-- arm holding brush toward canvas -->
<line x1="73" y1="82" x2="100" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<!-- paintbrush -->
<line x1="100" y1="60" x2="108" y2="52" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<circle cx="110" cy="50" r="2" fill="currentColor" opacity="0.3"/>
<!-- other arm holding palette -->
<line x1="38" y1="85" x2="18" y2="95" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- palette (big, with color dots) -->
<ellipse cx="12" cy="102" rx="16" ry="12" fill="currentColor" opacity="0.15"/>
<!-- thumb hole -->
<circle cx="12" cy="108" r="3" fill="currentColor" opacity="0.05"/>
<!-- paint dabs on palette -->
<circle cx="6" cy="97" r="2.5" fill="currentColor" opacity="0.3"/>
<circle cx="14" cy="94" r="2" fill="currentColor" opacity="0.25"/>
<circle cx="21" cy="98" r="2.5" fill="currentColor" opacity="0.28"/>
<circle cx="8" cy="104" r="2" fill="currentColor" opacity="0.22"/>
<circle cx="18" cy="103" r="2" fill="currentColor" opacity="0.3"/>
<!-- long legs -->
<line x1="45" y1="116" x2="35" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<line x1="65" y1="116" x2="75" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.25"/>
<!-- big flat feet -->
<ellipse cx="30" cy="191" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
<ellipse cx="80" cy="191" rx="10" ry="4" fill="currentColor" opacity="0.25"/>
</svg>
<div class="container-content">
<!-- Page de couverture du livre -->
<HomeBookSection
class="mb-10 hero-compact"
:show-chapters="false"
@open-player="showBookPlayer = true"
@open-pdf="showPdfReader = true"
/>
<header class="mb-10 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-3 mx-auto max-w-2xl text-sm" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl">
<ul class="flex flex-col gap-3">
<li
v-for="chapter in chapters"
:key="chapter.path"
>
<NuxtLink
:to="`/economique/modele-eco/${chapter.stem?.split('/').pop()}`"
class="card-surface flex items-start gap-4 group"
>
<span class="font-mono text-2xl font-bold text-primary/30 leading-none mt-1 w-10 text-right flex-shrink-0">
{{ String(chapter.order).padStart(2, '0') }}
</span>
<div class="min-w-0 flex-1">
<div class="flex items-baseline gap-2 flex-wrap">
<h2 class="font-display text-lg font-semibold text-white group-hover:text-primary transition-colors">
{{ chapter.title }}
</h2>
<span v-if="chapter.page" class="text-xs font-mono text-white/30 flex-shrink-0">p.&nbsp;{{ chapter.page }}</span>
</div>
<p v-if="chapter.description" class="mt-1 text-sm text-white/50">
{{ chapter.description }}
</p>
<div class="mt-2 flex items-center gap-3">
<span v-if="chapter.readingTime" class="text-xs text-white/30">
<span class="i-lucide-clock inline-block h-3 w-3 mr-1 align-middle" />
{{ chapter.readingTime }}
</span>
<SongBadges :chapter-slug="chapter.stem?.split('/').pop() ?? ''" />
</div>
</div>
<div class="i-lucide-chevron-right h-5 w-5 text-white/20 group-hover:text-primary/60 transition-colors flex-shrink-0 mt-2" />
</NuxtLink>
</li>
</ul>
</div>
</div>
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('economique/modele-eco')
useHead({
title: content.value?.meta?.title ?? 'Table des matières',
})
const { data: chapters } = await useAsyncData('book-toc', () =>
queryCollection('book').order('order', 'ASC').all(),
)
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
</script>
<style scoped>
.page-title {
font-size: clamp(1.5rem, 3.5vw, 2rem);
}
/* Compact the HomeBookSection hero */
:deep(.hero-compact .book-hero),
:deep(.hero-compact .section-hero) {
padding-top: 2rem;
padding-bottom: 2rem;
}
:deep(.hero-compact img),
:deep(.hero-compact .book-cover) {
max-height: 260px;
}
:deep(.hero-compact h1),
:deep(.hero-compact .hero-title) {
font-size: clamp(1.4rem, 3vw, 2rem);
}
:deep(.hero-compact .hero-description) {
font-size: 0.9rem;
}
/* Shadok illustrations — shared */
.shadok {
position: absolute;
pointer-events: none;
width: clamp(70px, 10vw, 140px);
z-index: 0;
}
/* 1. Typographe — top left */
.shadok-typographe {
top: 1%;
left: 1%;
opacity: 0.24;
color: hsl(var(--color-primary));
animation: shadok-float-1 9s ease-in-out infinite;
}
/* 2. Lectrice — top right, sitting */
.shadok-lectrice {
top: 2%;
right: 1%;
opacity: 0.22;
color: hsl(var(--color-accent));
animation: shadok-float-2 11s ease-in-out infinite;
}
/* 3. Calligraphe — left, 40% */
.shadok-calligraphe {
top: 40%;
left: 1%;
opacity: 0.2;
color: hsl(var(--color-primary));
animation: shadok-float-3 10s ease-in-out infinite;
}
/* 4. Relieur — right, 35% */
.shadok-relieur {
top: 35%;
right: 2%;
opacity: 0.24;
color: hsl(var(--color-accent));
animation: shadok-float-4 8s ease-in-out infinite;
}
/* 5. Conteuse — bottom left */
.shadok-conteuse {
bottom: 6%;
left: 1%;
opacity: 0.22;
color: hsl(var(--color-primary));
animation: shadok-float-5 12s ease-in-out infinite;
}
/* 6. Correcteur — bottom right, leaning */
.shadok-correcteur {
bottom: 5%;
right: 1%;
opacity: 0.2;
color: hsl(var(--color-accent));
animation: shadok-float-6 9.5s ease-in-out infinite;
}
/* 7. Colporteur — center bottom, walking */
.shadok-colporteur {
bottom: 2%;
left: 50%;
transform: translateX(-50%);
opacity: 0.18;
color: hsl(var(--color-primary));
animation: shadok-float-7 7.5s ease-in-out infinite;
}
/* 8. Illustratrice — right, 58% */
.shadok-illustratrice {
top: 58%;
right: 1%;
opacity: 0.26;
color: hsl(var(--color-accent));
animation: shadok-float-8 10.5s ease-in-out infinite;
}
/* Float animations — each unique duration and offset */
@keyframes shadok-float-1 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadok-float-2 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-3 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes shadok-float-4 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-5 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-11px); }
}
@keyframes shadok-float-6 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@keyframes shadok-float-7 {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-10px); }
}
@keyframes shadok-float-8 {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
/* Hidden on mobile */
@media (max-width: 768px) {
.shadok {
display: none;
}
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<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>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
<!-- Content -->
<div v-if="content?.content" class="prose-block mb-8">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
<!-- External links -->
<div v-if="content?.links" class="flex flex-col gap-3 mb-10">
<a
v-for="link in content.links"
:key="link.href"
:href="link.href"
target="_blank"
rel="noopener"
class="link-card group"
>
<div class="link-icon">
<div :class="`i-lucide-${link.icon ?? 'external-link'} h-4 w-4`" />
</div>
<span class="text-sm font-medium" style="color: hsl(var(--color-text))">{{ link.label }}</span>
<div class="i-lucide-arrow-up-right h-3.5 w-3.5 ml-auto text-primary/40 group-hover:text-primary transition-colors" />
</a>
</div>
<div class="text-center">
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('economique/monnaie-libre')
useHead({
title: content.value?.meta?.title ?? 'Monnaie libre',
})
</script>
<style scoped>
.g1-icon {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.75rem;
line-height: 1;
}
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
.link-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--color-primary) / 0.1);
background: hsl(var(--color-surface));
text-decoration: none;
transition: border-color 0.2s;
}
.link-card:hover {
border-color: hsl(var(--color-primary) / 0.25);
}
.link-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;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="section-padding">
<div class="container-content">
<div class="mx-auto max-w-2xl">
<div class="section-icon mx-auto mb-6">
<div :class="`i-lucide-${content?.icon ?? 'users'}`" class="h-12 w-12" />
</div>
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase text-center">
{{ content?.kicker }}
</p>
<h1 class="font-display text-3xl font-bold mb-4 text-center" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="text-lg leading-relaxed mb-8 text-center" style="color: hsl(var(--color-text-muted))">
{{ 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>
<!-- Content -->
<div v-if="content?.content" class="prose-block mb-10">
<p class="leading-relaxed whitespace-pre-line" style="color: hsl(var(--color-text-muted))">
{{ content.content }}
</p>
</div>
<div class="text-center">
<UiBaseButton variant="ghost" to="/economique">
<div class="i-lucide-arrow-left mr-2 h-4 w-4" />
Autonomie économique
</UiBaseButton>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { data: content } = await usePageContent('economique/productions-collectives')
useHead({
title: content.value?.meta?.title ?? 'Productions collectives',
})
</script>
<style scoped>
.section-icon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 1rem;
background: hsl(var(--color-primary) / 0.1);
border: 1px solid hsl(var(--color-primary) / 0.2);
color: hsl(var(--color-primary));
}
.gestation-badge {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
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);
width: fit-content;
}
.prose-block {
padding: 1.5rem;
border-radius: 0.75rem;
background: hsl(var(--color-surface));
}
</style>

View File

@@ -1,110 +0,0 @@
<template>
<div class="section-padding">
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight text-white">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl text-white/60">
{{ content?.description }}
</p>
</header>
<!-- Search + view toggle -->
<div class="mb-6 flex items-center justify-between gap-4">
<div class="relative flex-1 max-w-md">
<div class="i-lucide-search absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/30" />
<input
v-model="search"
type="text"
:placeholder="content?.searchPlaceholder"
class="w-full rounded-lg bg-surface border border-white/8 py-2 pl-10 pr-4 text-sm text-white placeholder:text-white/30 focus:border-primary/50 focus:outline-none"
>
</div>
<div class="flex items-center gap-1 rounded-lg bg-surface p-1">
<button
class="rounded p-1.5 transition-colors"
:class="viewMode === 'list' ? 'bg-white/10 text-white' : 'text-white/40'"
@click="viewMode = 'list'"
>
<div class="i-lucide-list h-4 w-4" />
</button>
<button
class="rounded p-1.5 transition-colors"
:class="viewMode === 'grid' ? 'bg-white/10 text-white' : 'text-white/40'"
@click="viewMode = 'grid'"
>
<div class="i-lucide-grid-3x3 h-4 w-4" />
</button>
</div>
</div>
<!-- Song list -->
<div v-if="viewMode === 'list'" class="flex flex-col gap-2">
<SongItem
v-for="song in filteredSongs"
:key="song.id"
:song="song"
/>
</div>
<!-- Song grid -->
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<SongItem
v-for="song in filteredSongs"
:key="song.id"
:song="song"
/>
</div>
<p v-if="filteredSongs.length === 0" class="text-center text-white/40 py-12">
{{ content?.noResults }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('ecouter')
useHead({
title: content.value?.meta?.title ?? 'Écouter',
})
const store = usePlayerStore()
const bookData = useBookData()
const { loadFullPlaylist } = usePlaylist()
await bookData.init()
// Switch to free mode
store.setMode('free')
await loadFullPlaylist()
const search = ref('')
const viewMode = ref<'list' | 'grid'>('list')
const filteredSongs = computed(() => {
const songs = bookData.getSongs()
if (!search.value.trim()) return songs
const q = search.value.toLowerCase()
return songs.filter(
s => s.title.toLowerCase().includes(q)
|| s.artist.toLowerCase().includes(q)
|| s.tags.some(t => t.toLowerCase().includes(q)),
)
})
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div class="relative overflow-hidden section-padding">
<!-- Shadok danseur: character dancing with music notes -->
<svg class="shadok-danseur" viewBox="0 0 240 300" fill="none" aria-hidden="true">
<!-- Body (dynamic pose, leaning) -->
<ellipse cx="120" cy="155" rx="38" ry="46" fill="currentColor" opacity="0.85"/>
<!-- Head (tilted with joy) -->
<ellipse cx="125" cy="92" rx="24" ry="23" fill="currentColor" opacity="0.8"/>
<!-- Eyes (happy, squinted) -->
<path d="M114 88 Q118 84 122 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M130 88 Q134 84 138 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Big smile -->
<path d="M116 100 Q125 108 134 100" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<!-- Arms thrown up (dancing) -->
<line x1="85" y1="140" x2="50" y2="100" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="155" y1="140" x2="190" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Hands -->
<circle cx="50" cy="98" r="4" fill="currentColor" opacity="0.4"/>
<circle cx="190" cy="103" r="4" fill="currentColor" opacity="0.4"/>
<!-- Legs (one kicked up) -->
<line x1="105" y1="198" x2="80" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="135" y1="198" x2="170" y2="240" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Feet -->
<path d="M80 255 L68 258 M80 255 L75 261" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<path d="M170 240 L180 238 M170 240 L175 246" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
<!-- Music notes floating around -->
<text x="42" y="82" fill="currentColor" opacity="0.3" font-size="18">&#9834;</text>
<text x="195" y="88" fill="currentColor" opacity="0.25" font-size="16">&#9835;</text>
<text x="60" y="65" fill="currentColor" opacity="0.2" font-size="14">&#9833;</text>
<text x="180" y="72" fill="currentColor" opacity="0.2" font-size="20">&#9834;</text>
</svg>
<!-- Shadok DJ: character with headphones behind a turntable -->
<svg class="shadok-dj" viewBox="0 0 260 300" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="130" cy="155" rx="42" ry="50" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="130" cy="88" r="26" fill="currentColor" opacity="0.8"/>
<!-- Headphones band -->
<path d="M104 78 Q130 55 156 78" stroke="currentColor" stroke-width="4" stroke-linecap="round" fill="none" opacity="0.6"/>
<!-- Headphone ear pads -->
<ellipse cx="102" cy="85" rx="8" ry="12" fill="currentColor" opacity="0.5"/>
<ellipse cx="158" cy="85" rx="8" ry="12" fill="currentColor" opacity="0.5"/>
<!-- Eyes (cool, half-lidded) -->
<ellipse cx="120" cy="85" rx="5" ry="3" fill="currentColor" opacity="0.25"/>
<ellipse cx="140" cy="85" rx="5" ry="3" fill="currentColor" opacity="0.25"/>
<circle cx="121" cy="86" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="141" cy="86" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Mouth (grin) -->
<path d="M122 98 Q130 104 138 98" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<!-- Arms reaching to turntable -->
<line x1="90" y1="150" x2="55" y2="195" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="170" y1="150" x2="205" y2="195" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Turntable body -->
<rect x="30" y="200" width="200" height="18" rx="4" fill="currentColor" opacity="0.4"/>
<!-- Turntable platter -->
<ellipse cx="130" cy="200" rx="55" ry="15" fill="currentColor" opacity="0.25"/>
<ellipse cx="130" cy="200" rx="55" ry="15" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.35"/>
<!-- Record center -->
<circle cx="130" cy="200" r="5" fill="currentColor" opacity="0.4"/>
<!-- Tone arm -->
<line x1="195" y1="188" x2="150" y2="195" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<circle cx="195" cy="188" r="3" fill="currentColor" opacity="0.35"/>
<!-- Legs -->
<line x1="115" y1="202" x2="105" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<line x1="145" y1="202" x2="155" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
</svg>
<div class="container-content">
<!-- Hero section with book cover -->
<div class="mb-12 grid items-center gap-8 md:grid-cols-2">
<div class="book-cover-wrapper">
<div class="book-cover-3d">
<img
:src="homeContent?.book.coverImage"
:alt="homeContent?.book.coverAlt"
class="book-cover-img"
/>
</div>
</div>
<div class="text-center md:text-left">
<p class="mb-2 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight text-white">
{{ content?.title }}
</h1>
<p class="mt-4 text-white/60">
{{ content?.description }}
</p>
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:gap-4 justify-center md:justify-start">
<UiBaseButton @click="showBookPlayer = true">
<div class="i-lucide-play mr-2 h-5 w-5" />
Présentation musicale
</UiBaseButton>
<UiBaseButton variant="accent" @click="showPdfReader = true">
<div class="i-lucide-book-open mr-2 h-5 w-5" />
Lire le livre
</UiBaseButton>
</div>
</div>
</div>
<!-- Search + view toggle -->
<div class="mb-6 flex items-center justify-between gap-4">
<div class="relative flex-1 max-w-md">
<div class="i-lucide-search absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/30" />
<input
v-model="search"
type="text"
:placeholder="content?.searchPlaceholder"
class="w-full rounded-lg bg-surface border border-white/8 py-2 pl-10 pr-4 text-sm text-white placeholder:text-white/30 focus:border-primary/50 focus:outline-none"
>
</div>
<div class="flex items-center gap-1 rounded-lg bg-surface p-1">
<button
class="rounded p-1.5 transition-colors"
:class="viewMode === 'list' ? 'bg-white/10 text-white' : 'text-white/40'"
@click="viewMode = 'list'"
>
<div class="i-lucide-list h-4 w-4" />
</button>
<button
class="rounded p-1.5 transition-colors"
:class="viewMode === 'grid' ? 'bg-white/10 text-white' : 'text-white/40'"
@click="viewMode = 'grid'"
>
<div class="i-lucide-grid-3x3 h-4 w-4" />
</button>
</div>
</div>
<!-- Song list -->
<div v-if="viewMode === 'list'" class="flex flex-col gap-2">
<SongItem
v-for="song in filteredSongs"
:key="song.id"
:song="song"
/>
</div>
<!-- Song grid -->
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<SongItem
v-for="song in filteredSongs"
:key="song.id"
:song="song"
/>
</div>
<p v-if="filteredSongs.length === 0" class="text-center text-white/40 py-12">
{{ content?.noResults }}
</p>
</div>
<BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" />
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('en-musique')
const { data: homeContent } = await usePageContent('home')
useHead({
title: content.value?.meta?.title ?? 'En musique',
})
const store = usePlayerStore()
const bookData = useBookData()
const { loadFullPlaylist } = usePlaylist()
await bookData.init()
// Switch to free mode
store.setMode('free')
await loadFullPlaylist()
const search = ref('')
const viewMode = ref<'list' | 'grid'>('list')
const showBookPlayer = ref(false)
const showPdfReader = ref(false)
const filteredSongs = computed(() => {
const songs = bookData.getSongs()
if (!search.value.trim()) return songs
const q = search.value.toLowerCase()
return songs.filter(
s => s.title.toLowerCase().includes(q)
|| s.artist.toLowerCase().includes(q)
|| s.tags.some(t => t.toLowerCase().includes(q)),
)
})
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
.book-cover-wrapper {
perspective: 800px;
display: flex;
justify-content: center;
}
.book-cover-3d {
aspect-ratio: 3 / 4;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid hsl(var(--color-text) / 0.1);
box-shadow:
0 12px 40px hsl(var(--color-text) / 0.15),
0 0 0 1px hsl(var(--color-text) / 0.08);
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
box-shadow 0.5s ease;
max-width: 280px;
}
.book-cover-3d:hover {
transform: rotateY(-8deg) rotateX(3deg) scale(1.02);
box-shadow:
12px 16px 48px hsl(var(--color-text) / 0.2),
0 0 0 1px hsl(var(--color-primary) / 0.2);
}
.book-cover-img {
width: 200%;
height: 100%;
object-fit: cover;
transform: translateX(-50%);
}
.shadok-danseur {
position: absolute;
left: 2%;
top: 3%;
width: clamp(100px, 14vw, 200px);
opacity: 0.28;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-danseur 7s ease-in-out infinite;
}
.shadok-dj {
position: absolute;
right: 2%;
top: 3%;
width: clamp(120px, 16vw, 230px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-dj 8s ease-in-out infinite;
}
@keyframes shadok-float-danseur {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-8px) rotate(2deg); }
75% { transform: translateY(-4px) rotate(-2deg); }
}
@keyframes shadok-float-dj {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@media (max-width: 768px) {
.shadok-danseur { display: none; }
.shadok-dj { display: none; }
}
</style>

558
app/pages/evenement.vue Normal file
View File

@@ -0,0 +1,558 @@
<template>
<div class="relative overflow-hidden section-padding min-h-[70vh] flex items-center justify-center">
<!-- 1. Shadok funambule: walking on tightrope (top-left) -->
<svg class="shadok-funambule" viewBox="0 0 170 200" fill="none" aria-hidden="true">
<!-- Tightrope -->
<line x1="5" y1="170" x2="165" y2="170" stroke="currentColor" stroke-width="2" opacity="0.3"/>
<!-- Body (small oval, leaning forward) -->
<ellipse cx="85" cy="110" rx="20" ry="28" fill="currentColor" opacity="0.25" transform="rotate(-5 85 110)"/>
<!-- Head -->
<circle cx="88" cy="72" r="16" fill="currentColor" opacity="0.3"/>
<!-- Eyes (focused, looking down-right) -->
<circle cx="93" cy="70" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="99" cy="71" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (pointy, right side) -->
<polygon points="103,74 116,72 103,78" fill="currentColor" opacity="0.3"/>
<!-- Balancing pole (big, horizontal) -->
<line x1="10" y1="92" x2="160" y2="88" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Arms holding pole -->
<line x1="67" y1="100" x2="45" y2="92" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="103" y1="98" x2="125" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Left leg (forward, on rope) -->
<line x1="78" y1="136" x2="70" y2="170" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="70" cy="172" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<!-- Right leg (back, lifted) -->
<line x1="92" y1="136" x2="108" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="108" cy="167" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 2. Shadok accordeoniste: playing accordion (top-right, profile) -->
<svg class="shadok-accordeoniste" viewBox="0 0 180 210" fill="none" aria-hidden="true">
<!-- Body (profile, small) -->
<ellipse cx="70" cy="115" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head (profile) -->
<circle cx="72" cy="75" r="16" fill="currentColor" opacity="0.3"/>
<!-- Beret -->
<ellipse cx="72" cy="60" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
<circle cx="72" cy="56" r="4" fill="currentColor" opacity="0.25"/>
<!-- Eye (profile, one visible) -->
<circle cx="80" cy="73" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (profile right) -->
<polygon points="86,76 98,74 86,80" fill="currentColor" opacity="0.3"/>
<!-- Accordion bellows (big, extended right) -->
<rect x="92" y="95" width="12" height="45" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="98" y1="98" x2="98" y2="137" stroke="currentColor" stroke-width="0.8" opacity="0.2"/>
<rect x="107" y="93" width="8" height="49" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="118" y="91" width="8" height="53" rx="2" fill="currentColor" opacity="0.2"/>
<rect x="129" y="89" width="8" height="57" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="140" y="87" width="12" height="61" rx="2" fill="currentColor" opacity="0.3"/>
<!-- Keyboard dots on right panel -->
<circle cx="146" cy="100" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="146" cy="110" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="146" cy="120" r="1.5" fill="currentColor" opacity="0.2"/>
<circle cx="146" cy="130" r="1.5" fill="currentColor" opacity="0.2"/>
<!-- Left arm on bellows -->
<line x1="52" y1="105" x2="92" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Right arm stretched to far end -->
<line x1="90" y1="108" x2="140" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Foot tapping (right lifted) -->
<line x1="60" y1="141" x2="52" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="52" cy="197" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="80" y1="141" x2="90" y2="188" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="90" cy="190" rx="8" ry="3" fill="currentColor" opacity="0.3" transform="rotate(-15 90 190)"/>
<!-- Music notes -->
<text x="155" y="80" fill="currentColor" opacity="0.25" font-size="14">&#9834;</text>
<text x="145" y="68" fill="currentColor" opacity="0.2" font-size="11">&#9835;</text>
</svg>
<!-- 3. Shadok jongleur: 4 balls in the air (top-center) -->
<svg class="shadok-jongleur" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="80" cy="120" rx="21" ry="27" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="80" cy="82" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes (looking up, wide) -->
<circle cx="74" cy="78" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="86" cy="78" r="2" fill="currentColor" opacity="0.5"/>
<!-- Mouth open (concentration) -->
<ellipse cx="80" cy="92" rx="4" ry="3" fill="currentColor" opacity="0.2"/>
<!-- Beak (small, front view) -->
<polygon points="80,86 87,89 80,92" fill="currentColor" opacity="0.3"/>
<!-- Arms up wide -->
<line x1="60" y1="110" x2="30" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="110" x2="130" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- 4 juggling balls in arc -->
<circle cx="40" cy="35" r="8" fill="currentColor" opacity="0.3"/>
<circle cx="70" cy="18" r="8" fill="currentColor" opacity="0.25"/>
<circle cx="100" cy="15" r="8" fill="currentColor" opacity="0.3"/>
<circle cx="128" cy="30" r="8" fill="currentColor" opacity="0.25"/>
<!-- Long legs -->
<line x1="72" y1="145" x2="60" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="60" cy="202" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="88" y1="145" x2="100" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="100" cy="202" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 4. Shadok cracheur de feu: head tilted back, flame from mouth -->
<svg class="shadok-cracheur" viewBox="0 0 170 220" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="75" cy="125" rx="22" ry="30" fill="currentColor" opacity="0.25"/>
<!-- Head (tilted back) -->
<circle cx="78" cy="82" r="16" fill="currentColor" opacity="0.3" />
<!-- Eyes (looking up) -->
<circle cx="73" cy="76" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="83" cy="76" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (pointing up, open) -->
<polygon points="78,66 85,52 72,66" fill="currentColor" opacity="0.3"/>
<!-- Flame stream from beak (big, upward-right) -->
<path d="M82 54 Q100 25 95 10 Q110 30 120 8 Q115 35 135 15 Q120 42 140 30 Q118 50 130 45" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="currentColor" opacity="0.2"/>
<!-- Torch in left hand -->
<line x1="55" y1="115" x2="30" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<rect x="24" y="78" width="8" height="16" rx="2" fill="currentColor" opacity="0.35"/>
<path d="M28 78 Q28 68 32 72 Q28 65 28 78" fill="currentColor" opacity="0.25"/>
<!-- Right arm out for balance -->
<line x1="95" y1="112" x2="125" y2="100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Long legs -->
<line x1="66" y1="153" x2="55" y2="210" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="55" cy="212" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="84" y1="153" x2="95" y2="210" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="95" cy="212" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 5. Shadok trapeziste: hanging from trapeze, body in arc -->
<svg class="shadok-trapeziste" viewBox="0 0 150 220" fill="none" aria-hidden="true">
<!-- Trapeze ropes -->
<line x1="40" y1="0" x2="50" y2="40" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="110" y1="0" x2="100" y2="40" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- Trapeze bar -->
<line x1="48" y1="40" x2="102" y2="40" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.35"/>
<!-- Arms (hanging from bar) -->
<line x1="60" y1="42" x2="65" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="42" x2="85" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Body (arched, graceful) -->
<ellipse cx="75" cy="95" rx="20" ry="25" fill="currentColor" opacity="0.25" transform="rotate(10 75 95)"/>
<!-- Head (below body, looking down) -->
<circle cx="78" cy="72" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes (excited, looking down) -->
<circle cx="74" cy="74" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="84" cy="75" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak (pointing down-right) -->
<polygon points="86,78 96,84 86,82" fill="currentColor" opacity="0.3"/>
<!-- Legs (pointed, graceful, extending down-right) -->
<line x1="80" y1="118" x2="95" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="70" y1="118" x2="82" y2="180" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Pointed feet -->
<polygon points="95,175 105,180 95,182" fill="currentColor" opacity="0.3"/>
<polygon points="82,180 92,185 82,187" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 6. Shadok batteur: behind drum kit, sticks raised -->
<svg class="shadok-batteur" viewBox="0 0 180 210" fill="none" aria-hidden="true">
<!-- Snare drum (center, big) -->
<ellipse cx="90" cy="165" rx="35" ry="12" fill="currentColor" opacity="0.2"/>
<rect x="55" y="155" width="70" height="20" rx="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="90" cy="155" rx="35" ry="12" fill="currentColor" opacity="0.25"/>
<!-- Hi-hat (left) -->
<ellipse cx="30" cy="140" rx="18" ry="5" fill="currentColor" opacity="0.2"/>
<line x1="30" y1="140" x2="30" y2="185" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<!-- Cymbal (right) -->
<ellipse cx="155" cy="120" rx="20" ry="5" fill="currentColor" opacity="0.2"/>
<line x1="155" y1="120" x2="155" y2="185" stroke="currentColor" stroke-width="2" opacity="0.2"/>
<!-- Body (behind kit) -->
<ellipse cx="90" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="90" cy="70" r="16" fill="currentColor" opacity="0.3"/>
<!-- Eyes (intense, looking at drums) -->
<circle cx="84" cy="68" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="96" cy="69" r="2" fill="currentColor" opacity="0.5"/>
<!-- Beak (front, small) -->
<polygon points="90,76 97,80 90,82" fill="currentColor" opacity="0.3"/>
<!-- Arms raised with drumsticks -->
<line x1="70" y1="100" x2="40" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="40" y1="65" x2="25" y2="50" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<line x1="110" y1="100" x2="140" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="140" y1="60" x2="158" y2="48" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- Motion lines on sticks -->
<path d="M22 52 Q18 48 20 44" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<path d="M160 50 Q164 46 162 42" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<!-- Legs (tucked behind kit) -->
<line x1="80" y1="136" x2="70" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="70" cy="197" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
<line x1="100" y1="136" x2="110" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<ellipse cx="110" cy="197" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 7. Shadok marionnettiste: holding puppet strings -->
<svg class="shadok-marionnettiste" viewBox="0 0 160 220" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="80" cy="75" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="80" cy="40" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes (looking down at puppet) -->
<circle cx="75" cy="42" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="86" cy="43" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak -->
<polygon points="89,44 100,42 89,48" fill="currentColor" opacity="0.3"/>
<!-- Arms down holding control bar -->
<line x1="62" y1="68" x2="50" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="98" y1="68" x2="110" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Control bar (cross) -->
<line x1="40" y1="108" x2="120" y2="108" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<line x1="80" y1="98" x2="80" y2="118" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.35"/>
<!-- Puppet strings -->
<line x1="45" y1="108" x2="55" y2="155" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<line x1="80" y1="118" x2="70" y2="158" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<line x1="115" y1="108" x2="85" y2="155" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<line x1="80" y1="98" x2="70" y2="148" stroke="currentColor" stroke-width="1" opacity="0.25"/>
<!-- Mini puppet shadok -->
<circle cx="70" cy="155" r="8" fill="currentColor" opacity="0.2"/>
<ellipse cx="70" cy="172" rx="10" ry="13" fill="currentColor" opacity="0.15"/>
<!-- Puppet eyes -->
<circle cx="67" cy="154" r="1" fill="currentColor" opacity="0.4"/>
<circle cx="74" cy="154" r="1" fill="currentColor" opacity="0.4"/>
<!-- Puppet beak -->
<polygon points="76,156 82,155 76,158" fill="currentColor" opacity="0.2"/>
<!-- Puppet legs -->
<line x1="64" y1="184" x2="58" y2="208" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="76" y1="184" x2="82" y2="208" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Master shadok legs -->
<line x1="72" y1="99" x2="62" y2="155" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<line x1="88" y1="99" x2="98" y2="155" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3"/>
<ellipse cx="62" cy="157" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
<ellipse cx="98" cy="157" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
</svg>
<!-- 8. Shadok clown: oversized shoes, red nose, squirting flower -->
<svg class="shadok-clown" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="80" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head -->
<circle cx="80" cy="70" r="16" fill="currentColor" opacity="0.3"/>
<!-- Party hat (tall cone) -->
<polygon points="80,38 68,62 92,62" fill="currentColor" opacity="0.25"/>
<circle cx="80" cy="38" r="3" fill="currentColor" opacity="0.35"/>
<!-- Big red nose (circle on beak) -->
<circle cx="92" cy="74" r="5" fill="currentColor" opacity="0.4"/>
<!-- Beak behind nose -->
<polygon points="88,72 100,74 88,78" fill="currentColor" opacity="0.2"/>
<!-- Eyes (different directions goofy) -->
<circle cx="74" cy="66" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="85" cy="64" r="2" fill="currentColor" opacity="0.5"/>
<!-- Big goofy grin -->
<path d="M72 82 Q80 90 88 82" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Squirting flower on chest -->
<circle cx="80" cy="95" r="6" fill="currentColor" opacity="0.25"/>
<circle cx="80" cy="95" r="2.5" fill="currentColor" opacity="0.35"/>
<!-- Water squirt from flower -->
<path d="M86 93 Q100 85 105 90" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
<circle cx="107" cy="90" r="2" fill="currentColor" opacity="0.2"/>
<circle cx="112" cy="88" r="1.5" fill="currentColor" opacity="0.15"/>
<!-- Arms -->
<line x1="60" y1="100" x2="35" y2="90" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="100" x2="125" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Long legs -->
<line x1="72" y1="136" x2="50" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="88" y1="136" x2="110" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- OVERSIZED shoes -->
<ellipse cx="42" cy="190" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
<ellipse cx="118" cy="190" rx="18" ry="6" fill="currentColor" opacity="0.3"/>
</svg>
<!-- 9. Shadok acrobate: mid-cartwheel, body rotated 90deg -->
<svg class="shadok-acrobate" viewBox="0 0 160 200" fill="none" aria-hidden="true">
<!-- Body (rotated 90 degrees) -->
<ellipse cx="80" cy="100" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(90 80 100)"/>
<!-- Head (at bottom-right, inverted) -->
<circle cx="112" cy="100" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes (dizzy, spiral-ish) -->
<path d="M108 97 Q110 94 113 97" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<path d="M117 97 Q119 94 122 97" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
<!-- Beak (pointing right) -->
<polygon points="124,102 136,100 124,106" fill="currentColor" opacity="0.3"/>
<!-- Limbs in pinwheel pattern -->
<!-- Arm up-right -->
<line x1="85" y1="80" x2="110" y2="35" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Arm down-left -->
<line x1="75" y1="120" x2="50" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Leg up-left (hand on ground) -->
<line x1="65" y1="88" x2="25" y2="55" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<circle cx="23" cy="53" r="4" fill="currentColor" opacity="0.25"/>
<!-- Leg down-right -->
<line x1="95" y1="112" x2="135" y2="150" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="110" cy="33" rx="6" ry="3" fill="currentColor" opacity="0.25" transform="rotate(-60 110 33)"/>
<ellipse cx="135" cy="152" rx="6" ry="3" fill="currentColor" opacity="0.25" transform="rotate(40 135 152)"/>
<!-- Motion arc -->
<path d="M30 40 Q20 50 25 60" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
<path d="M140 140 Q148 148 145 158" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
</svg>
<!-- 10. Shadok regisseur: headset, clipboard, running -->
<svg class="shadok-regisseur" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body (leaning forward, running) -->
<ellipse cx="75" cy="105" rx="20" ry="27" fill="currentColor" opacity="0.25" transform="rotate(-12 75 105)"/>
<!-- Head -->
<circle cx="80" cy="68" r="15" fill="currentColor" opacity="0.3"/>
<!-- Headset arc -->
<path d="M66 60 Q80 48 94 60" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" fill="none" opacity="0.35"/>
<!-- Headset earpiece -->
<ellipse cx="66" cy="64" rx="4" ry="6" fill="currentColor" opacity="0.3"/>
<!-- Headset mic -->
<path d="M66 70 Q62 78 70 80" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
<!-- Eyes (stressed, one looking forward one sideways) -->
<circle cx="76" cy="65" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="87" cy="64" r="2" fill="currentColor" opacity="0.5"/>
<!-- Beak -->
<polygon points="91,68 104,66 91,72" fill="currentColor" opacity="0.3"/>
<!-- Lanyard -->
<line x1="80" y1="82" x2="80" y2="100" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<!-- Badge -->
<rect x="74" y="100" width="12" height="15" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="76" y="103" width="8" height="3" rx="1" fill="currentColor" opacity="0.15"/>
<!-- Right arm pointing forward (with authority) -->
<line x1="92" y1="95" x2="135" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Pointing finger -->
<line x1="135" y1="75" x2="145" y2="70" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
<!-- Left arm holding clipboard -->
<line x1="58" y1="98" x2="38" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Clipboard (big) -->
<rect x="22" y="105" width="22" height="30" rx="2" fill="currentColor" opacity="0.25"/>
<rect x="28" y="102" width="10" height="5" rx="1" fill="currentColor" opacity="0.3"/>
<!-- Clipboard lines -->
<line x1="26" y1="114" x2="40" y2="114" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="26" y1="120" x2="38" y2="120" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<line x1="26" y1="126" x2="40" y2="126" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<!-- Legs (running stride, long) -->
<line x1="68" y1="130" x2="40" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="38" cy="197" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<line x1="82" y1="130" x2="115" y2="190" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<ellipse cx="117" cy="192" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<div class="container-content relative z-10 text-center">
<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 class="mt-4 text-lg" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('evenement')
useHead({
title: content.value?.meta?.title ?? 'Évènement',
})
</script>
<style scoped>
.page-title {
font-size: clamp(2.5rem, 6vw, 3.5rem);
}
/* 1. Funambule — top-left */
.shadok-funambule {
position: absolute;
left: 4%;
top: 5%;
width: clamp(70px, 10vw, 140px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-sway-funambule 9s ease-in-out infinite;
}
/* 2. Accordeoniste — top-right */
.shadok-accordeoniste {
position: absolute;
right: 3%;
top: 4%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-bounce-accordeon 8s ease-in-out infinite;
}
/* 3. Jongleur — top-center */
.shadok-jongleur {
position: absolute;
left: 50%;
top: 2%;
transform: translateX(-50%);
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-jongleur 7s ease-in-out infinite;
}
/* 4. Cracheur de feu — left 5%, 40% */
.shadok-cracheur {
position: absolute;
left: 5%;
top: 40%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-flicker-cracheur 8s ease-in-out infinite;
}
/* 5. Trapeziste — right 3%, 30% */
.shadok-trapeziste {
position: absolute;
right: 3%;
top: 30%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-swing-trapeze 10s ease-in-out infinite;
}
/* 6. Batteur — left 4%, bottom 15% */
.shadok-batteur {
position: absolute;
left: 4%;
bottom: 15%;
width: clamp(70px, 10vw, 140px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-pulse-batteur 7s ease-in-out infinite;
}
/* 7. Marionnettiste — right 4%, bottom 20% */
.shadok-marionnettiste {
position: absolute;
right: 4%;
bottom: 20%;
width: clamp(70px, 10vw, 130px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-bob-marionnette 9s ease-in-out infinite;
}
/* 8. Clown — bottom-left */
.shadok-clown {
position: absolute;
left: 6%;
bottom: 3%;
width: clamp(70px, 10vw, 135px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-wobble-clown 8s ease-in-out infinite;
}
/* 9. Acrobate — center-bottom */
.shadok-acrobate {
position: absolute;
left: 50%;
bottom: 2%;
transform: translateX(-50%);
width: clamp(70px, 10vw, 130px);
opacity: 0.18;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-spin-acrobate 12s ease-in-out infinite;
}
/* 10. Regisseur — bottom-right */
.shadok-regisseur {
position: absolute;
right: 3%;
bottom: 4%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-rush-regisseur 7s ease-in-out infinite;
}
@keyframes shadok-sway-funambule {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-5px) rotate(2deg); }
75% { transform: translateY(-3px) rotate(-2deg); }
}
@keyframes shadok-bounce-accordeon {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-8px); }
70% { transform: translateY(-3px); }
}
@keyframes shadok-float-jongleur {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-10px); }
}
@keyframes shadok-flicker-cracheur {
0%, 100% { transform: translateY(0) scale(1); }
30% { transform: translateY(-4px) scale(1.02); }
60% { transform: translateY(-2px) scale(0.99); }
}
@keyframes shadok-swing-trapeze {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(4deg); }
75% { transform: rotate(-4deg); }
}
@keyframes shadok-pulse-batteur {
0%, 100% { transform: translateY(0); }
15% { transform: translateY(-4px); }
30% { transform: translateY(0); }
45% { transform: translateY(-6px); }
60% { transform: translateY(0); }
}
@keyframes shadok-bob-marionnette {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-7px); }
}
@keyframes shadok-wobble-clown {
0%, 100% { transform: translateY(0) rotate(0deg); }
30% { transform: translateY(-5px) rotate(-3deg); }
70% { transform: translateY(-2px) rotate(3deg); }
}
@keyframes shadok-spin-acrobate {
0% { transform: translateX(-50%) rotate(0deg); }
50% { transform: translateX(-50%) rotate(10deg); }
100% { transform: translateX(-50%) rotate(0deg); }
}
@keyframes shadok-rush-regisseur {
0%, 100% { transform: translateX(0) translateY(0); }
25% { transform: translateX(3px) translateY(-5px); }
50% { transform: translateX(-2px) translateY(-2px); }
75% { transform: translateX(2px) translateY(-6px); }
}
@media (max-width: 768px) {
.shadok-funambule,
.shadok-accordeoniste,
.shadok-jongleur,
.shadok-cracheur,
.shadok-trapeziste,
.shadok-batteur,
.shadok-marionnettiste,
.shadok-clown,
.shadok-acrobate,
.shadok-regisseur {
display: none;
}
}
</style>

View File

@@ -1,11 +1,54 @@
<template> <template>
<div class="section-padding"> <NuxtLayout>
<div class="relative overflow-hidden section-padding">
<!-- Shadok alchemist: character stirring a cauldron with sparkles -->
<svg class="shadok-alchemist" viewBox="0 0 240 320" fill="none" aria-hidden="true">
<!-- Body -->
<ellipse cx="120" cy="145" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
<!-- Head -->
<circle cx="120" cy="82" r="24" fill="currentColor" opacity="0.8"/>
<!-- Wizard hat -->
<polygon points="120,30 100,80 140,80" fill="currentColor" opacity="0.5"/>
<line x1="100" y1="80" x2="140" y2="80" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.45"/>
<!-- Hat star -->
<circle cx="118" cy="55" r="3" fill="currentColor" opacity="0.25"/>
<!-- Eyes (mischievous) -->
<circle cx="111" cy="78" r="4.5" fill="currentColor" opacity="0.2"/>
<circle cx="129" cy="78" r="4.5" fill="currentColor" opacity="0.2"/>
<circle cx="112" cy="77" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="130" cy="77" r="2" fill="currentColor" opacity="0.5"/>
<!-- Grin -->
<path d="M112 92 Q120 98 128 92" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
<!-- Arm holding spoon/stick -->
<line x1="158" y1="140" x2="175" y2="210" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
<!-- Other arm -->
<path d="M82 145 Q65 165 70 185" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
<!-- Cauldron -->
<path d="M60 220 Q60 260 120 260 Q180 260 180 220" fill="currentColor" opacity="0.4"/>
<ellipse cx="120" cy="220" rx="60" ry="15" fill="currentColor" opacity="0.3"/>
<ellipse cx="120" cy="220" rx="60" ry="15" stroke="currentColor" stroke-width="2" fill="none" opacity="0.4"/>
<!-- Cauldron legs -->
<line x1="80" y1="258" x2="75" y2="280" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
<line x1="160" y1="258" x2="165" y2="280" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
<!-- Bubbles from cauldron -->
<circle cx="100" cy="210" r="5" fill="currentColor" opacity="0.2"/>
<circle cx="135" cy="205" r="4" fill="currentColor" opacity="0.18"/>
<circle cx="115" cy="198" r="3" fill="currentColor" opacity="0.15"/>
<!-- Sparkles -->
<circle cx="90" cy="190" r="2" fill="currentColor" opacity="0.25"/>
<circle cx="150" cy="195" r="2.5" fill="currentColor" opacity="0.2"/>
<circle cx="105" cy="185" r="1.5" fill="currentColor" opacity="0.2"/>
<!-- Character legs -->
<line x1="105" y1="190" x2="95" y2="225" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.5"/>
<line x1="135" y1="190" x2="145" y2="225" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.5"/>
</svg>
<div class="container-content max-w-3xl mx-auto"> <div class="container-content max-w-3xl mx-auto">
<!-- Back link --> <!-- Back link -->
<UiScrollReveal> <UiScrollReveal>
<NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-white/50 hover:text-white/80 mb-8 transition-colors"> <NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-white/50 hover:text-white/80 mb-8 transition-colors">
<div class="i-lucide-arrow-left h-4 w-4" /> <div class="i-lucide-arrow-left h-4 w-4" />
Retour à l'accueil Retour &agrave; l'accueil
</NuxtLink> </NuxtLink>
</UiScrollReveal> </UiScrollReveal>
@@ -47,7 +90,7 @@
<p class="text-sm text-white/40 mb-4"> <p class="text-sm text-white/40 mb-4">
{{ content?.cta.note }} {{ content?.cta.note }}
</p> </p>
<UiBaseButton @click="launch"> <UiBaseButton :href="url" target="_blank" @click="launch">
<div class="i-lucide-external-link mr-2 h-5 w-5" /> <div class="i-lucide-external-link mr-2 h-5 w-5" />
{{ content?.cta.label }} {{ content?.cta.label }}
</UiBaseButton> </UiBaseButton>
@@ -55,16 +98,17 @@
</UiScrollReveal> </UiScrollReveal>
</div> </div>
</div> </div>
</NuxtLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { data: content } = await usePageContent('gratewizard') const { data: content } = await usePageContent('gratewizard')
useHead({ useHead({
title: content.value?.meta?.title ?? 'GrateWizard Coefficients relatifs', title: content.value?.meta?.title ?? 'grateWizard \u2014 Coefficients relatifs',
}) })
const { launch } = useGrateWizard() const { url, launch } = useGrateWizard()
</script> </script>
<style scoped> <style scoped>
@@ -75,14 +119,14 @@ const { launch } = useGrateWizard()
.gw-feature-card { .gw-feature-card {
padding: 1.5rem; padding: 1.5rem;
border-radius: 0.75rem; border-radius: 0.75rem;
border: 1px solid hsl(20 8% 18%); border: 1px solid hsl(var(--color-text) / 0.1);
background: hsl(20 8% 8% / 0.5); background: hsl(var(--color-surface) / 0.5);
transition: border-color 0.3s ease, background 0.3s ease; transition: border-color 0.3s ease, background 0.3s ease;
} }
.gw-feature-card:hover { .gw-feature-card:hover {
border-color: hsl(40 80% 50% / 0.25); border-color: hsl(40 80% 50% / 0.25);
background: hsl(20 8% 10% / 0.5); background: hsl(var(--color-surface-light) / 0.5);
} }
code { code {
@@ -92,4 +136,24 @@ code {
border-radius: 0.25em; border-radius: 0.25em;
background: hsl(40 80% 50% / 0.1); background: hsl(40 80% 50% / 0.1);
} }
.shadok-alchemist {
position: absolute;
right: 2%;
top: 10%;
width: clamp(120px, 16vw, 230px);
opacity: 0.3;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-alchemist 10s ease-in-out infinite;
}
@keyframes shadok-float-alchemist {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-10px) rotate(1deg); }
}
@media (max-width: 768px) {
.shadok-alchemist { display: none; }
}
</style> </style>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<HomeHeroSection /> <HomeHeroSection />
<HomeBookSection @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" /> <HomeAxisGrid @open-player="showBookPlayer = true" @open-pdf="showPdfReader = true" />
<HomeGrateWizardTeaser /> <HomeMessages />
<BookPlayer v-model="showBookPlayer" /> <BookPlayer v-model="showBookPlayer" />
<BookPdfReader v-model="showPdfReader" /> <BookPdfReader v-model="showPdfReader" />
</div> </div>

View File

@@ -1,71 +0,0 @@
<template>
<div class="section-padding">
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight text-white">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl text-white/60">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl">
<ul class="flex flex-col gap-3">
<li
v-for="chapter in chapters"
:key="chapter.path"
>
<NuxtLink
:to="`/lire/${chapter.stem}`"
class="card-surface flex items-start gap-4 group"
>
<span class="font-mono text-2xl font-bold text-primary/30 leading-none mt-1 w-10 text-right flex-shrink-0">
{{ String(chapter.order).padStart(2, '0') }}
</span>
<div class="min-w-0 flex-1">
<h2 class="font-display text-lg font-semibold text-white group-hover:text-primary transition-colors">
{{ chapter.title }}
</h2>
<p v-if="chapter.description" class="mt-1 text-sm text-white/50">
{{ chapter.description }}
</p>
<div class="mt-2 flex items-center gap-3">
<span v-if="chapter.readingTime" class="text-xs text-white/30">
<span class="i-lucide-clock inline-block h-3 w-3 mr-1 align-middle" />
{{ chapter.readingTime }}
</span>
<SongBadges :chapter-slug="chapter.stem!" />
</div>
</div>
<div class="i-lucide-chevron-right h-5 w-5 text-white/20 group-hover:text-primary/60 transition-colors flex-shrink-0 mt-2" />
</NuxtLink>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('lire')
useHead({
title: content.value?.meta?.title ?? 'Table des matières',
})
const { data: chapters } = await useAsyncData('book-toc', () =>
queryCollection('book').order('order', 'ASC').all(),
)
</script>
<style scoped>
.page-title {
font-size: clamp(2rem, 5vw, 2.75rem);
}
</style>

View File

@@ -2,20 +2,33 @@
<div class="section-padding"> <div class="section-padding">
<div class="container-content mx-auto max-w-3xl"> <div class="container-content mx-auto max-w-3xl">
<h1 class="font-display text-3xl font-bold text-gradient mb-2">Messages des visiteurs</h1> <h1 class="font-display text-3xl font-bold text-gradient mb-2">Messages des visiteurs</h1>
<p class="text-white/50 mb-8">Les mots laissés par celles et ceux qui passent par ici.</p> <p class="page-subtitle mb-8">Les mots laissés par celles et ceux qui passent par ici.</p>
<div v-if="messages?.length" class="space-y-4"> <div v-if="messages?.length" class="space-y-4">
<div v-for="msg in messages" :key="msg.id" class="message-card"> <div v-for="msg in messages" :key="msg.id" class="message-card">
<p class="text-white/80 leading-relaxed">{{ msg.text }}</p> <!-- En-tête auteur -->
<div class="mt-3 flex items-center gap-2 text-xs text-white/40"> <div class="flex items-center gap-2 mb-2">
<span class="font-semibold text-white/60">{{ msg.author }}</span> <span class="msg-author font-semibold text-sm">{{ msg.author }}</span>
<span>&middot;</span> <span class="type-pill">{{ typeLabel(msg.type) }}</span>
<span>{{ formatDate(msg.createdAt) }}</span> <span class="msg-date text-xs ml-auto">{{ formatDate(msg.createdAt) }}</span>
</div>
<!-- Texte du message -->
<p class="msg-text text-sm leading-relaxed">{{ msg.text }}</p>
<!-- Réponse -->
<div v-if="msg.reply?.text" class="reply-thread">
<div class="reply-connector" aria-hidden="true" />
<div class="reply-block">
<div class="flex items-center gap-1.5 mb-1">
<div class="i-lucide-corner-down-right h-3 w-3 reply-icon" />
<span class="reply-author text-xs font-semibold">Le Librodrome</span>
</div>
<p class="reply-text text-sm leading-relaxed italic">{{ msg.reply.text }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<p v-else class="text-center text-white/40 py-12">Aucun message pour l'instant.</p> <p v-else class="text-center page-subtitle py-12">Aucun message pour l'instant.</p>
<div class="mt-8 text-center"> <div class="mt-8 text-center">
<NuxtLink to="/" class="btn-ghost text-sm"> <NuxtLink to="/" class="btn-ghost text-sm">
@@ -34,6 +47,17 @@ useHead({
const { data: messages } = await useFetch('/api/messages') const { data: messages } = await useFetch('/api/messages')
const TYPE_LABELS: Record<string, string> = {
reaction: 'Réaction',
question: 'Question',
suggestion: 'Suggestion',
retour: 'Retour',
}
function typeLabel(type: string) {
return TYPE_LABELS[type] ?? type
}
function formatDate(iso: string) { function formatDate(iso: string) {
const date = new Date(iso) const date = new Date(iso)
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })
@@ -41,10 +65,59 @@ function formatDate(iso: string) {
</script> </script>
<style scoped> <style scoped>
.page-subtitle { color: hsl(var(--color-text) / 0.5); }
.msg-author { color: hsl(var(--color-text) / 0.75); }
.msg-date { color: hsl(var(--color-text) / 0.38); }
.msg-text { color: hsl(var(--color-text) / 0.78); white-space: pre-line; }
.reply-icon { color: hsl(var(--color-primary) / 0.5); }
.reply-author { color: hsl(var(--color-primary) / 0.8); }
.reply-text { color: hsl(var(--color-text) / 0.62); }
.message-card { .message-card {
background: hsl(20 8% 6%); background: hsl(var(--color-surface));
border: 1px solid hsl(20 8% 14%); border: 1px solid hsl(var(--color-text) / 0.1);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
} }
.type-pill {
font-size: 0.6rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.45rem;
border-radius: 9999px;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary) / 0.7);
}
.reply-thread {
position: relative;
display: flex;
gap: 0;
margin-top: 0.75rem;
padding-left: 1.25rem;
}
.reply-connector {
position: absolute;
left: 0.5rem;
top: -0.5rem;
bottom: 0.5rem;
width: 2px;
background: linear-gradient(
to bottom,
hsl(var(--color-primary) / 0.35),
hsl(var(--color-primary) / 0.15)
);
border-radius: 2px;
}
.reply-block {
flex: 1;
background: hsl(var(--color-primary) / 0.05);
border-left: 2px solid hsl(var(--color-primary) / 0.25);
border-radius: 0 0.5rem 0.5rem 0;
padding: 0.6rem 0.875rem;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,586 @@
<template>
<div class="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 -->
<rect x="15" y="120" width="80" height="5" rx="2" fill="currentColor" opacity="0.3"/>
<line x1="25" y1="125" x2="25" y2="165" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.25"/>
<line x1="85" y1="125" x2="85" y2="165" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.25"/>
<!-- Laptop on desk -->
<rect x="30" y="108" width="50" height="12" rx="2" fill="currentColor" opacity="0.35"/>
<path d="M35 108 L40 80 L70 80 L75 108" fill="currentColor" opacity="0.28"/>
<rect x="42" y="84" width="26" height="18" rx="1" fill="currentColor" opacity="0.15"/>
<!-- Code on screen < /> -->
<text x="48" y="96" font-size="8" fill="currentColor" opacity="0.4" font-family="monospace">&lt;/&gt;</text>
<!-- Body seated, tilted forward -->
<ellipse cx="110" cy="105" rx="20" ry="26" fill="currentColor" opacity="0.25" transform="rotate(-10 110 105)"/>
<!-- Head looking at screen -->
<circle cx="105" cy="68" r="15" fill="currentColor" opacity="0.3"/>
<!-- Glasses -->
<circle cx="100" cy="66" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<circle cx="111" cy="66" r="5" stroke="currentColor" stroke-width="1.2" fill="none" opacity="0.4"/>
<line x1="105" y1="66" x2="106" y2="66" stroke="currentColor" stroke-width="1" opacity="0.35"/>
<!-- Eyes behind glasses -->
<circle cx="101" cy="66" r="1.5" fill="currentColor" opacity="0.5"/>
<circle cx="110" cy="65" r="1.5" fill="currentColor" opacity="0.5"/>
<!-- Beak pointy, looking left -->
<polygon points="88,68 82,65 88,72" fill="currentColor" opacity="0.35"/>
<!-- Arms reaching to laptop -->
<line x1="95" y1="95" x2="78" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="125" y1="98" x2="80" y2="115" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs bent, seated -->
<line x1="100" y1="128" x2="90" y2="180" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="118" y1="128" x2="125" y2="180" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="88" cy="183" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="127" cy="183" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok electricien profile view, climbing utility pole, tool belt -->
<svg class="shadok-electricien" viewBox="0 0 150 210" fill="none" aria-hidden="true">
<!-- Utility pole -->
<rect x="68" y="5" width="8" height="200" rx="2" fill="currentColor" opacity="0.2"/>
<line x1="50" y1="30" x2="94" y2="30" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.25"/>
<line x1="45" y1="55" x2="99" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.22"/>
<!-- Cables hanging -->
<path d="M50 30 Q35 45 30 35" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<path d="M94 30 Q110 45 115 35" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.2"/>
<path d="M45 55 Q30 68 25 60" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.18"/>
<!-- Body climbing, profile, tilted -->
<ellipse cx="95" cy="100" rx="20" ry="25" fill="currentColor" opacity="0.25" transform="rotate(15 95 100)"/>
<!-- Head looking up at cable -->
<circle cx="100" cy="65" r="14" fill="currentColor" opacity="0.3"/>
<!-- Eyes looking up -->
<circle cx="96" cy="61" r="1.5" fill="currentColor" opacity="0.5"/>
<circle cx="105" cy="60" r="1.5" fill="currentColor" opacity="0.45"/>
<!-- Beak profile right -->
<polygon points="114,63 122,60 114,67" fill="currentColor" opacity="0.35"/>
<!-- Hard hat -->
<ellipse cx="100" cy="53" rx="16" ry="5" fill="currentColor" opacity="0.3"/>
<path d="M88 53 Q100 42 112 53" fill="currentColor" opacity="0.25"/>
<!-- Arms one gripping pole, one holding cable -->
<line x1="82" y1="90" x2="74" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="108" y1="88" x2="118" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Tool belt -->
<ellipse cx="95" cy="118" rx="22" ry="4" fill="currentColor" opacity="0.25"/>
<rect x="82" y="118" width="6" height="8" rx="1" fill="currentColor" opacity="0.2"/>
<rect x="105" y="118" width="5" height="10" rx="1" fill="currentColor" opacity="0.2"/>
<!-- Legs climbing, spread on pole -->
<line x1="85" y1="122" x2="72" y2="185" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="105" y1="122" x2="76" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet gripping pole -->
<ellipse cx="70" cy="188" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="75" cy="178" rx="8" ry="3" fill="currentColor" opacity="0.28"/>
</svg>
<!-- Shadok soudeuse welding mask flipped up, torch with sparks, apron -->
<svg class="shadok-soudeuse" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Body upright, slight lean forward -->
<ellipse cx="80" cy="110" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Apron -->
<path d="M62 95 L60 140 L100 140 L98 95" fill="currentColor" opacity="0.18"/>
<line x1="80" y1="95" x2="80" y2="140" stroke="currentColor" stroke-width="1" opacity="0.15"/>
<!-- Head -->
<circle cx="80" cy="65" r="16" fill="currentColor" opacity="0.3"/>
<!-- Welding mask flipped up on head -->
<rect x="65" y="42" width="30" height="18" rx="4" fill="currentColor" opacity="0.2"/>
<rect x="70" y="46" width="20" height="10" rx="2" fill="currentColor" opacity="0.12"/>
<!-- Eyes squinting from bright light -->
<line x1="73" y1="64" x2="77" y2="64" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" opacity="0.5"/>
<line x1="84" y1="63" x2="88" y2="63" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" opacity="0.45"/>
<!-- Beak small, front-facing -->
<polygon points="78,72 80,78 82,72" fill="currentColor" opacity="0.35"/>
<!-- Right arm holding torch -->
<line x1="100" y1="100" x2="135" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Welding torch -->
<rect x="132" y="78" width="22" height="6" rx="2" fill="currentColor" opacity="0.35"/>
<line x1="154" y1="81" x2="160" y2="78" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
<!-- Sparks from torch -->
<circle cx="158" cy="75" r="1.5" fill="currentColor" opacity="0.45"/>
<circle cx="155" cy="70" r="1" fill="currentColor" opacity="0.35"/>
<circle cx="160" cy="72" r="1.2" fill="currentColor" opacity="0.4"/>
<line x1="157" y1="76" x2="160" y2="68" stroke="currentColor" stroke-width="0.8" opacity="0.3"/>
<line x1="155" y1="78" x2="152" y2="71" stroke="currentColor" stroke-width="0.8" opacity="0.25"/>
<!-- Left arm down -->
<line x1="60" y1="100" x2="45" y2="125" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Legs standing, spread -->
<line x1="70" y1="136" x2="58" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="136" x2="102" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Big flat feet -->
<ellipse cx="55" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="105" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok admin réseau tangled in cables, router at feet, frustrated -->
<svg class="shadok-admin-reseau" viewBox="0 0 160 200" fill="none" aria-hidden="true">
<!-- Body upright but struggling -->
<ellipse cx="80" cy="95" rx="21" ry="27" fill="currentColor" opacity="0.25"/>
<!-- Head tilted, frustrated -->
<circle cx="82" cy="52" r="15" fill="currentColor" opacity="0.3" />
<!-- Eyes looking in different directions (frustrated) -->
<circle cx="77" cy="50" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="88" cy="48" r="1.8" fill="currentColor" opacity="0.45"/>
<!-- Angry eyebrows -->
<line x1="74" y1="46" x2="79" y2="47" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<line x1="91" y1="44" x2="86" y2="46" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity="0.4"/>
<!-- Beak open, yelling -->
<polygon points="73,56 65,55 73,60" fill="currentColor" opacity="0.35"/>
<line x1="67" y1="56" x2="66" y2="58" stroke="currentColor" stroke-width="1" opacity="0.3"/>
<!-- Cables tangling around body -->
<path d="M50 40 Q30 60 55 80 Q80 95 60 115 Q40 130 70 140" stroke="currentColor" stroke-width="2" fill="none" opacity="0.3"/>
<path d="M110 45 Q130 65 105 85 Q85 100 110 120 Q125 130 95 145" stroke="currentColor" stroke-width="2" fill="none" opacity="0.28"/>
<path d="M75 38 Q95 50 75 70" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.22"/>
<!-- Arms trying to untangle -->
<line x1="60" y1="85" x2="40" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="100" y1="85" x2="120" y2="68" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Legs -->
<line x1="70" y1="120" x2="60" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="90" y1="120" x2="100" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="57" cy="178" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="103" cy="178" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<!-- Router box at feet -->
<rect x="60" y="168" width="40" height="14" rx="3" fill="currentColor" opacity="0.25"/>
<circle cx="70" cy="175" r="1.5" fill="currentColor" opacity="0.35"/>
<circle cx="78" cy="175" r="1.5" fill="currentColor" opacity="0.3"/>
<circle cx="86" cy="175" r="1.5" fill="currentColor" opacity="0.35"/>
<!-- Antenna on router -->
<line x1="92" y1="168" x2="96" y2="155" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
</svg>
<!-- Shadok imprimeuse 3D watching printer build object layer by layer -->
<svg class="shadok-imprimeuse3d" viewBox="0 0 170 210" fill="none" aria-hidden="true">
<!-- 3D printer frame -->
<rect x="10" y="90" width="65" height="60" rx="3" fill="currentColor" opacity="0.2"/>
<!-- Printer rails -->
<line x1="15" y1="95" x2="15" y2="145" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<line x1="70" y1="95" x2="70" y2="145" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<!-- Print bed -->
<rect x="18" y="135" width="49" height="4" rx="1" fill="currentColor" opacity="0.25"/>
<!-- Object being printed (layered) -->
<rect x="32" y="128" width="20" height="7" rx="1" fill="currentColor" opacity="0.3"/>
<rect x="34" y="122" width="16" height="6" rx="1" fill="currentColor" opacity="0.25"/>
<rect x="36" y="117" width="12" height="5" rx="1" fill="currentColor" opacity="0.2"/>
<!-- Extruder head -->
<rect x="34" y="110" width="16" height="6" rx="2" fill="currentColor" opacity="0.35"/>
<line x1="42" y1="116" x2="42" y2="118" stroke="currentColor" stroke-width="1.5" opacity="0.4"/>
<!-- Body leaning forward, watching intently -->
<ellipse cx="120" cy="105" rx="20" ry="25" fill="currentColor" opacity="0.25" transform="rotate(-15 120 105)"/>
<!-- Head looking down at printer -->
<circle cx="108" cy="70" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes both staring down at printer, fascinated -->
<circle cx="103" cy="72" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="112" cy="73" r="1.8" fill="currentColor" opacity="0.5"/>
<!-- Beak pointing down -->
<polygon points="100,78 96,82 104,82" fill="currentColor" opacity="0.35"/>
<!-- Arms one pointing at printer -->
<line x1="105" y1="95" x2="75" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="135" y1="98" x2="150" y2="115" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<!-- Legs standing, leaning -->
<line x1="112" y1="128" x2="105" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="128" y1="128" x2="138" y2="192" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="103" cy="195" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="140" cy="195" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok cryptographe 3/4 view, cloak, giant key + padlock -->
<svg class="shadok-cryptographe" viewBox="0 0 170 210" fill="none" aria-hidden="true">
<!-- Mysterious cloak -->
<path d="M55 70 Q50 110 45 160 L125 160 Q120 110 115 70" fill="currentColor" opacity="0.15"/>
<path d="M45 160 Q40 170 38 180 L132 180 Q130 170 125 160" fill="currentColor" opacity="0.12"/>
<!-- Body under cloak -->
<ellipse cx="85" cy="105" rx="22" ry="28" fill="currentColor" opacity="0.25"/>
<!-- Head 3/4 view, slightly turned -->
<circle cx="85" cy="55" r="16" fill="currentColor" opacity="0.3"/>
<!-- Hood suggestion -->
<path d="M68 50 Q85 35 102 50" fill="currentColor" opacity="0.18"/>
<!-- Eyes mysterious, one slightly larger -->
<circle cx="80" cy="53" r="2" fill="currentColor" opacity="0.5"/>
<circle cx="91" cy="52" r="1.5" fill="currentColor" opacity="0.4"/>
<!-- Beak 3/4 profile -->
<polygon points="75,58 67,55 74,62" fill="currentColor" opacity="0.35"/>
<!-- Left arm holding giant key -->
<line x1="63" y1="95" x2="30" y2="110" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Giant key -->
<circle cx="18" cy="108" r="12" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.4"/>
<line x1="30" y1="108" x2="55" y2="108" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
<line x1="48" y1="108" x2="48" y2="118" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<line x1="53" y1="108" x2="53" y2="115" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
<!-- Right arm holding padlock -->
<line x1="107" y1="95" x2="140" y2="85" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Padlock big -->
<rect x="135" y="82" width="28" height="24" rx="4" fill="currentColor" opacity="0.3"/>
<path d="M141 82 L141 70 Q149 58 157 70 L157 82" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.35"/>
<circle cx="149" cy="95" r="3" fill="currentColor" opacity="0.2"/>
<line x1="149" y1="98" x2="149" y2="103" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<!-- Legs long, cloak partly covering -->
<line x1="75" y1="130" x2="65" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="95" y1="130" x2="108" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="63" cy="198" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="110" cy="198" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok droneuse looking up at flying drone, remote in hands -->
<svg class="shadok-droneuse" viewBox="0 0 160 210" fill="none" aria-hidden="true">
<!-- Drone above -->
<rect x="60" y="12" width="30" height="8" rx="3" fill="currentColor" opacity="0.3"/>
<!-- Drone arms -->
<line x1="55" y1="16" x2="35" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<line x1="95" y1="16" x2="115" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.25"/>
<!-- Propellers spinning -->
<ellipse cx="35" cy="14" rx="12" ry="3" fill="currentColor" opacity="0.2"/>
<ellipse cx="115" cy="14" rx="12" ry="3" fill="currentColor" opacity="0.2"/>
<!-- Propeller blur lines -->
<line x1="25" y1="12" x2="45" y2="12" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<line x1="105" y1="12" x2="125" y2="12" stroke="currentColor" stroke-width="0.8" opacity="0.15"/>
<!-- Drone camera -->
<circle cx="75" cy="22" r="3" fill="currentColor" opacity="0.25"/>
<!-- Drone legs -->
<line x1="62" y1="20" x2="58" y2="28" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<line x1="88" y1="20" x2="92" y2="28" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
<!-- Body upright, looking up -->
<ellipse cx="78" cy="115" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
<!-- Head tilted back looking up -->
<circle cx="78" cy="72" r="15" fill="currentColor" opacity="0.3"/>
<!-- Eyes looking up at drone -->
<circle cx="74" cy="68" r="1.8" fill="currentColor" opacity="0.5"/>
<circle cx="83" cy="67" r="1.8" fill="currentColor" opacity="0.45"/>
<!-- Beak pointing up -->
<polygon points="76,56 80,50 82,57" fill="currentColor" opacity="0.35"/>
<!-- Arms holding remote control -->
<line x1="60" y1="108" x2="50" y2="128" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="96" y1="108" x2="106" y2="128" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Remote control in hands -->
<rect x="42" y="126" width="70" height="14" rx="4" fill="currentColor" opacity="0.3"/>
<circle cx="55" cy="133" r="3" fill="currentColor" opacity="0.2"/>
<circle cx="100" cy="133" r="3" fill="currentColor" opacity="0.2"/>
<!-- Joysticks -->
<line x1="55" y1="130" x2="55" y2="127" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<line x1="100" y1="131" x2="102" y2="128" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.3"/>
<!-- Legs standing -->
<line x1="68" y1="138" x2="58" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="88" y1="138" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Feet -->
<ellipse cx="55" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="103" cy="198" rx="9" ry="3" fill="currentColor" opacity="0.3"/>
</svg>
<!-- Shadok mécanicienne lying under machine, legs sticking out, wrench, oil drops -->
<svg class="shadok-mecanicienne" viewBox="0 0 180 200" fill="none" aria-hidden="true">
<!-- Machine/server box above -->
<rect x="5" y="55" width="100" height="50" rx="5" fill="currentColor" opacity="0.2"/>
<rect x="12" y="62" width="35" height="12" rx="2" fill="currentColor" opacity="0.15"/>
<rect x="12" y="80" width="35" height="12" rx="2" fill="currentColor" opacity="0.15"/>
<!-- Machine details -->
<circle cx="20" cy="68" r="2" fill="currentColor" opacity="0.3"/>
<circle cx="20" cy="86" r="2" fill="currentColor" opacity="0.25"/>
<line x1="60" y1="65" x2="90" y2="65" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<line x1="60" y1="72" x2="85" y2="72" stroke="currentColor" stroke-width="1.5" opacity="0.15"/>
<!-- Body lying flat under machine, only partial visible -->
<ellipse cx="80" cy="118" rx="25" ry="18" fill="currentColor" opacity="0.22" transform="rotate(90 80 118)"/>
<!-- Head under machine (barely visible) -->
<circle cx="55" cy="115" r="12" fill="currentColor" opacity="0.2"/>
<!-- Arm sticking out with wrench -->
<line x1="42" y1="115" x2="15" y2="105" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Wrench -->
<path d="M8 100 L15 105 L12 108 L5 103 Z" fill="currentColor" opacity="0.35"/>
<circle cx="5" cy="100" r="5" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.3"/>
<!-- Oil drops falling -->
<ellipse cx="70" cy="108" rx="2" ry="3" fill="currentColor" opacity="0.3"/>
<ellipse cx="85" cy="112" rx="1.5" ry="2.5" fill="currentColor" opacity="0.25"/>
<ellipse cx="62" cy="105" rx="1" ry="2" fill="currentColor" opacity="0.2"/>
<!-- Legs sticking out from under machine the signature view -->
<line x1="90" y1="125" x2="130" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<line x1="95" y1="120" x2="145" y2="165" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
<!-- Big feet sticking up -->
<ellipse cx="133" cy="178" rx="10" ry="4" fill="currentColor" opacity="0.35" transform="rotate(-25 133 178)"/>
<ellipse cx="148" cy="168" rx="10" ry="4" fill="currentColor" opacity="0.35" transform="rotate(-25 148 168)"/>
<!-- Beak visible from side -->
<polygon points="42,118 35,116 42,122" fill="currentColor" opacity="0.3"/>
<!-- Eye visible -->
<circle cx="50" cy="113" r="1.5" fill="currentColor" opacity="0.4"/>
</svg>
<div class="container-content">
<header class="mb-12 text-center">
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
<h1 class="page-title font-display font-bold tracking-tight" style="color: hsl(var(--color-text))">
{{ content?.title }}
</h1>
<p class="mt-4 mx-auto max-w-2xl leading-relaxed" style="color: hsl(var(--color-text-muted))">
{{ content?.description }}
</p>
</header>
<div class="mx-auto max-w-3xl flex flex-col gap-6">
<div
v-for="pillar in content?.pillars"
:key="pillar.id"
class="pillar-card"
>
<div class="pillar-header">
<div class="pillar-icon">
<div :class="`i-lucide-${pillar.icon}`" class="h-5 w-5" />
</div>
<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>
</div>
<p class="leading-relaxed whitespace-pre-line mt-3" style="color: hsl(var(--color-text-muted))">{{ 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-sm" style="color: hsl(var(--color-text))">{{ pillar.project.name }}</span>
<span class="text-sm ml-2" style="color: hsl(var(--color-text-muted))">{{ pillar.project.text }}</span>
</div>
</div>
<div v-if="pillar.to" class="mt-4">
<NuxtLink
:to="pillar.to"
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
>
En savoir plus
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
const { data: content } = await usePageContent('numerique')
useHead({
title: content.value?.meta?.title ?? 'Autonomie numérique',
})
</script>
<style scoped>
.page-title {
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-codeuse {
position: absolute;
left: 1%;
top: 4%;
width: clamp(70px, 10vw, 140px);
opacity: 0.24;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-codeuse 9s ease-in-out infinite;
}
.shadok-electricien {
position: absolute;
right: 1%;
top: 3%;
width: clamp(70px, 10vw, 130px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-electricien 11s ease-in-out infinite;
}
.shadok-soudeuse {
position: absolute;
left: 3%;
top: 40%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-soudeuse 8s ease-in-out infinite;
}
.shadok-admin-reseau {
position: absolute;
right: 2%;
top: 35%;
width: clamp(70px, 10vw, 130px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-admin-reseau 10s ease-in-out infinite;
}
.shadok-imprimeuse3d {
position: absolute;
left: 2%;
bottom: 8%;
width: clamp(70px, 10vw, 135px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-imprimeuse3d 12s ease-in-out infinite;
}
.shadok-cryptographe {
position: absolute;
right: 1%;
bottom: 6%;
width: clamp(70px, 10vw, 130px);
opacity: 0.18;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-cryptographe 11s ease-in-out infinite;
}
.shadok-droneuse {
position: absolute;
left: 40%;
bottom: 2%;
width: clamp(70px, 10vw, 130px);
opacity: 0.2;
pointer-events: none;
color: hsl(var(--color-primary));
animation: shadok-float-droneuse 7s ease-in-out infinite;
}
.shadok-mecanicienne {
position: absolute;
right: 2%;
top: 60%;
width: clamp(70px, 10vw, 140px);
opacity: 0.22;
pointer-events: none;
color: hsl(var(--color-accent));
animation: shadok-float-mecanicienne 9s ease-in-out infinite;
}
@keyframes shadok-float-codeuse {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes shadok-float-electricien {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-10px) rotate(1deg); }
}
@keyframes shadok-float-soudeuse {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-7px) rotate(-0.8deg); }
}
@keyframes shadok-float-admin-reseau {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-9px); }
}
@keyframes shadok-float-imprimeuse3d {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-11px) rotate(0.5deg); }
}
@keyframes shadok-float-cryptographe {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(-0.6deg); }
}
@keyframes shadok-float-droneuse {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
@keyframes shadok-float-mecanicienne {
0%, 100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-7px) rotate(1.2deg); }
}
@media (max-width: 768px) {
.shadok-codeuse { display: none; }
.shadok-electricien { display: none; }
.shadok-soudeuse { display: none; }
.shadok-admin-reseau { display: none; }
.shadok-imprimeuse3d { display: none; }
.shadok-cryptographe { display: none; }
.shadok-droneuse { display: none; }
.shadok-mecanicienne { display: none; }
}
</style>

118
app/stores/palette.ts Normal file
View File

@@ -0,0 +1,118 @@
export type PaletteName = 'automne' | 'hiver' | 'printemps' | 'ete'
interface PaletteColors {
primary: string
accent: string
surface: string
bg: string
surfaceLight: string
text: string
textMuted: string
isLight: boolean
label: string
icon: string
}
const palettes: Record<PaletteName, PaletteColors> = {
// ══════ DARK THEMES ══════
// Automne : cuivre chaud, feuilles mortes, terre brûlée
automne: {
primary: '18 80% 45%', // cuivre profond
accent: '32 85% 50%', // ambre doré
surface: '20 10% 12%', // écorce
bg: '20 10% 7%', // terre sombre
surfaceLight: '20 8% 17%', // bois fumé
text: '0 0% 100%',
textMuted: '0 0% 65%',
isLight: false,
label: 'Automne',
icon: 'i-lucide-leaf',
},
// Hiver : bleu nuit, givre, argent lunaire
hiver: {
primary: '215 55% 52%', // bleu nuit étoilé
accent: '195 40% 65%', // givre argenté
surface: '222 14% 13%', // ciel de minuit
bg: '225 16% 8%', // nuit polaire
surfaceLight: '220 12% 18%', // brume nocturne
text: '0 0% 100%',
textMuted: '210 10% 65%',
isLight: false,
label: 'Hiver',
icon: 'i-lucide-snowflake',
},
// ══════ LIGHT THEMES ══════
// Printemps : vert soutenu, magenta chaud, lumière vivante
printemps: {
primary: '152 80% 24%', // vert émeraude sombre
accent: '338 88% 45%', // magenta profond
surface: '145 25% 85%', // prairie franche
bg: '140 28% 90%', // vert lumineux franc
surfaceLight: '148 22% 77%', // feuillage vif
text: '155 50% 6%', // encre noire-verte
textMuted: '150 22% 28%', // sous-bois dense
isLight: true,
label: 'Printemps',
icon: 'i-lucide-flower-2',
},
// Été : orange brûlant, corail profond, chaleur méditerranéenne
ete: {
primary: '18 90% 44%', // terre cuite brûlante
accent: '355 78% 50%', // corail ardent
surface: '32 40% 85%', // ocre clair
bg: '35 42% 90%', // chaleur dorée
surfaceLight: '30 32% 78%', // argile chaude
text: '20 45% 8%', // brun profond
textMuted: '22 22% 30%', // ombre terracotta
isLight: true,
label: 'Été',
icon: 'i-lucide-sun',
},
}
export const usePaletteStore = defineStore('palette', () => {
const currentPalette = ref<PaletteName>(
(import.meta.client && localStorage.getItem('palette') as PaletteName) || 'ete',
)
const colors = computed(() => palettes[currentPalette.value])
const isLight = computed(() => colors.value.isLight)
function applyToDOM() {
if (!import.meta.client) return
const c = colors.value
const root = document.documentElement
const s = root.style
s.setProperty('--color-primary', c.primary)
s.setProperty('--color-accent', c.accent)
s.setProperty('--color-surface', c.surface)
s.setProperty('--color-bg', c.bg)
s.setProperty('--color-surface-light', c.surfaceLight)
s.setProperty('--color-text', c.text)
s.setProperty('--color-text-muted', c.textMuted)
// Toggle light/dark class for CSS overrides
root.classList.toggle('palette-light', c.isLight)
root.classList.toggle('palette-dark', !c.isLight)
s.setProperty('color-scheme', c.isLight ? 'light' : 'dark')
}
function setPalette(name: PaletteName) {
currentPalette.value = name
if (import.meta.client) localStorage.setItem('palette', name)
applyToDOM()
}
return {
currentPalette,
colors,
palettes,
isLight,
setPalette,
applyToDOM,
}
})

View File

@@ -119,20 +119,13 @@ export const usePlayerStore = defineStore('player', () => {
function prevSong(): Song | null { function prevSong(): Song | null {
if (playlist.value.length === 0) return null if (playlist.value.length === 0) return null
// If more than 3 seconds in, restart current song
if (currentTime.value > 3) {
currentTime.value = 0
return currentSong.value
}
let prevIdx = currentIndex.value - 1 let prevIdx = currentIndex.value - 1
if (prevIdx < 0) { if (prevIdx < 0) {
if (repeatMode.value === 'all') { if (repeatMode.value === 'all') {
prevIdx = playlist.value.length - 1 prevIdx = playlist.value.length - 1
} }
else { else {
currentTime.value = 0 return null
return currentSong.value
} }
} }

View File

@@ -12,13 +12,20 @@ export interface ChapterSongLink {
primary: boolean primary: boolean
} }
export interface ChapterPageLink {
chapterSlug: string
page: number
}
export interface BookConfig { export interface BookConfig {
title: string title: string
author: string author: string
description: string description: string
coverImage?: string coverImage?: string
pdfFile?: string
chapters: ChapterMeta[] chapters: ChapterMeta[]
songs: import('./song').Song[] songs: import('./song').Song[]
chapterSongs: ChapterSongLink[] chapterSongs: ChapterSongLink[]
chapterPages: ChapterPageLink[]
defaultPlaylistOrder: string[] defaultPlaylistOrder: string[]
} }

View File

@@ -9,6 +9,7 @@ export default defineContentConfig({
title: z.string(), title: z.string(),
description: z.string().optional(), description: z.string().optional(),
order: z.number(), order: z.number(),
page: z.number().optional(),
readingTime: z.string().optional(), readingTime: z.string().optional(),
}), }),
}), }),

View File

@@ -0,0 +1,49 @@
---
title: "Introduction"
description: "La proposition d'un modèle économique, permis par la monnaie libre — une expérimentation concrète, économique et civile."
order: 1
page: 9
readingTime: "15 min"
---
## Introduction
[ *masc.* ]
Ce livre est un essai : la proposition d'un modèle économique, permis par la monnaie libre. Je désigne par là une seule et unique devise, dénommée June, qui applique la Théorie Relative de la Monnaie TRM.
Ce modèle n'est pas une théorie universelle. Je vous fais la proposition d'une expérimentation concrète, économique et civile. Une expérimentation qui peut démarrer sans condition, même à l'échelle d'un petit nombre. Mais c'est en passant à l'échelle d'un bassin de vie qu'elle change de portée et devient… économique.
Ce livre n'est donc pas un guide, encore moins un kit. Il ne vous dira pas quoi faire lundi. Il propose des repères concrets et conceptuels pour nourrir une réflexion collective. Le passage à l'action vous appartient.
L'expérience est destinée en premier lieu aux « monnaie-libristes » — quelque 8.500 créateurs monétaires de DUĞ1 à l'heure où j'écris ces lignes. Mais je m'adresse également à toute personne intriguée par le don avec un regard anthropologique ou économique ; aux personnes affranchies qui peuvent consacrer du temps et des savoir-faire ; à celles qui cherchent à créer un monde alternatif ; aux jeunes adultes qui ont la possibilité de faire des choix de vie.
> *Or le pouvoir de la création monétaire est avant tout celui de créer une économie. Une monnaie n'a aucun lieu d'être sans une économie à servir.*
> *Il n'est ici question que d'une économie du don. Rien d'autre.*
---
## Monnaie-libristes
[ *fém.* ]
Une partie des junistes ont la volonté d'engendrer une économie monnaie-libriste ; une autre y voit un moyen de rectifier notre économie euro ; d'autres encore y voient un jeu. Les portes d'entrée sont multiples : le logiciel libre, la blockchain et la Toile de Confiance, la sémantique du « dividende universel », les monnaies locales.
Pour témoigner de mon propre cheminement, je suis arrivé sur la june depuis l'étude de l'économie, menant à la création monétaire, en passant par la case Bernard Lietaer. Donc par la porte de la TRM, moins fréquentée.
Beaucoup d'énergie est mise sur la tentative de « convertir » des commerçantes dans une démarche très prosélyte, vivant la monnaie libre comme une monnaie locale. C'est naturel — on reproduit des schémas familiers. Mais rester là-dessus ne crée pas une économie.
> *Car l'économie avant l'échange c'est la production.*
J'utilise l'image de la première vitesse et du geste de « passer la seconde ». Rester en première, c'est énormément d'énergie pour avancer au pas.
Il manque à beaucoup d'endroits la notion de finalité. Changer de devise n'est pas suffisant. Chaque groupe peut définir sa propre finalité — il en faut une. La monnaie libre n'est pas une baguette magique.
> *Le leitmotiv de l'autonomie collective à échelle humaine est un excellent candidat, en tant que finalité.*
L'autonomie n'est pas un repli sur soi, c'est le premier leitmotiv des parents envers leurs enfants. L'année 2026, où je reprends cet essai, s'annonce comme une année de défis. J'appelle de mes vœux la démarche collective de moins subir les agendas.
L'intention ici est de proposer un modèle, comme un os à ronger — quelques repères économiques, administratifs et symboliques. Progresser sur un terrain vierge nécessite de tâtonner et, dans notre cas, de la créativité.
> *Ce n'est pas encore l'heure du « prêt-à-porter ».*

87
content/book/02-don.md Normal file
View File

@@ -0,0 +1,87 @@
---
title: "De quel don parlons-nous ?"
description: "Le don, en tant que vecteur clé d'une économie, est le geste de transmettre quelque chose en premier lieu, sans réciprocité symétrique, mais assuré d'un contre-don."
order: 2
page: 23
readingTime: "18 min"
---
Dans le titre, c'est évidemment le terme « don » qui marque le plus, car il a tendance à s'opposer au terme économie — une sorte d'oxymore. Le don flirte parfois avec la spiritualité, ce qui prend le dessus sur une notion que l'on se représente froide et intellectuelle. Mais l'est-elle vraiment ?
## Trois mots de philo, et de socio
*[ fém. ]*
Je commence par le don, en évacuant le registre spirituel : je ne parle pas du don religieux, du centuple divin, ni de celui que « l'univers me rendra ». Je parle seulement du geste.
Marcel Mauss est souvent cité dès que l'on parle du don. À l'occasion d'une boucle bretonne, j'ai pu observer que les présentes personnes avaient une interprétation un peu fantasmée des pratiques du don dans les temps anciens — y compris les lectrices de Mauss, alors qu'il présente une tension souvent redoutable : une dimension prépondérante des rapports de domination ; une décorrélation marquée avec les échanges de subsistance.
Notre héritage judéo-chrétien associe culturellement le don à quelque chose qui ne peut ne doit pas être rendu. Mais avant les évangiles, le don était un geste triple indissociable : donner — recevoir — rendre (contre-don).
J'ai entendu aussi l'idée qu'il faudrait pratiquer le troc, ou supprimer la monnaie comme finalité ultime. Ce propos repose sur l'idée que la monnaie est en elle-même porteuse de problèmes. Pourtant, c'est seulement la monnaie-dette qui génère ces déséquilibres. L'existence d'une création monétaire décentralisée change la donne. La promesse d'une économie du don, c'est que cette monnaie (produite par un DU selon la TRM) apparaît alors davantage comme un outil qui permet de créer des équilibres.
Pour finir, il y a l'idée que le don ne peut pas être mesuré. Je conclus ce chapitre en positionnant clairement le don dont je parle — non pas une définition universelle, mais une définition dans le cadre d'un modèle éco monnaie-libriste :
> Le don, en tant que vecteur clé d'une économie, est le **geste de transmettre quelque chose**
>
> en premier lieu ;
> sans réciprocité symétrique ;
> mais assuré d'un contre-don ;
> de toute forme, de toute provenance.
>
> **« Mon économie me le rendra »**
>
> → J'y trouve ma pleine mesure.
> → J'y réalise mes propres équilibres.
>
> Le don que nous manipulons est donc : **un don qui se mesure**.
>
> Une mesure qui se fait par l'acte d'affecter un DU (*fractions et multiples de DU*), au geste vécu du don ; **selon ses échelles du moment, individuelles et collectives**.
>
> Une mesure décentralisée et relativiste, pour des milliards d'estimations, toutes légitimes.
« Faire du DU la mesure du don », c'est davantage que « faire de la Ğ1 la monnaie du don ». Le DU a une valeur d'invariant qui permet de positionner toutes les valeurs relativement à lui. C'est la raison pour laquelle j'invite à rebaptiser les choses. Parmi ces propositions, la « monnaie » devient la « mesure ».
## Asymétries et Communautés
*[ masc. ]*
Les mouvements alternatifs ont tendance à réunir les « déjà convaincus » et génèrent naturellement une forme d'entre-soi. Les collectifs deviennent souvent des communautés « pseudo-isolées ».
Pour cadrer mon raisonnement : sur un collectif récent qui porte le don dans son code génétique, j'y ai recueilli beaucoup de témoignages d'usure, de sentiments d'abus, d'abandons, de ressentiments. Pourquoi ? Parce que dans la totalité de nos échanges, il y a des asymétries.
Nos échanges sont asymétriques dans la totalité des cas. Donner une journée par mois pendant un an, est-ce le même geste que donner deux semaines complètes ? Entre une journée assis à étiqueter des pots et une journée plié en deux sur une toiture, y a-t-il équilibre ? Rien n'est symétrique. Pour éviter les abus ou les déséquilibres, il suffit de se mettre d'accord sur une mesure qui fasse sens pour tous.
Une population ouverte peut difficilement légiférer sur ces asymétries permanentes ; elle peut en revanche utiliser une monnaie. C'est en cela un outil précieux, probablement le seul.
> *Résoudre le problème des asymétries est le rôle pivot, structurel, que joue la monnaie dans l'échange.*
Si je raisonne à l'échelle de mon bassin de vie, que je donne une valeur aux dons des autres et que les autres donnent une valeur à mes dons, peu importe si quelqu'un me déçoit — je m'y retrouverai dans l'économie de mon bassin de vie. Plus besoin que tout le monde se comporte selon mes critères propres.
En commençant par retrouver dans économie le sens de « *éco - oikos* », son lieu, foyer et terre élue. « Nomos », c'est originellement la façon de distribuer, puis désigne ce que l'être humain institue. Actualisé, on peut lire dans « créer une économie » le fait de définir notre façon de :
> couvrir nos besoins pour vivre
> et
> nourrir nos plaisirs de vivre.
Rien n'empêche de pratiquer un don non mesuré, par exemple pour la beauté de l'élan. Chacun définit son périmètre et ses moments. Mais dès que le don sera mesuré, il deviendra structurant pour l'économie qui émerge.
## Le cas emblématique « eco si nuestra »
*[ fém. ]*
Eco si nuestra est bien connue des junistes. On peut entendre certaines participantes déclarer vivre avec la monnaie-libre pour un tiers de tous leurs besoins. Le premier point est d'associer le DU créé à un devoir et à une durée de travail : le DU que tu crées te rend redevable d'une heure de boulot à la communauté. Elles ont par ailleurs une concertation collective sur les « prix » pour les maintenir au plus bas, et font une double corrélation avec l'heure de travail et les calories.
Une communauté ouverte sur le monde, dont le caractère pseudo-isolé s'est néanmoins révélé lors de rencontres internationales : un gros décalage de pratiques sur les « prix » a généré ce que je baptiserais pudiquement « *les larmes du pot de confiture* ».
Cela invite chaque groupe qui voudra utiliser le DU à une réflexion sur l'interopérabilité de ses règles. Quels sont nos indicateurs relatifs locaux ?
## L'expérience « made in zion »
*[ masc. ]*
Cette expérience de vie communautaire en forêt utilisait les DUs pour structurer le fonctionnement quotidien. Pour vivre : 3 DUs pour les visiteuses, 2 pour les contributrices à l'entretien et 1 seul DU pour les amélioratrices (projets). Une rétribution des tâches par tirage dans une « boîte de gratitudes » évite les débats sur la valeur de chacune. L'effet recherché est de « gamifier » — transformer en jeu.
[ Règles complètes du jeu « 321…DU », @Qoop : https://pad.p2p.legal/s/321DU.LeJeu# ]
Ce genre de règles ne peut être que circonscrit à un lieu partagé, mais cela peut donner des idées pour un grand nombre de contextes.

52
content/book/03-mesure.md Normal file
View File

@@ -0,0 +1,52 @@
---
title: "La mesure du don"
description: "Considérer le DU de Ğ1 comme unité de mesure du don — une façon très factuelle de prendre la mesure du don."
order: 3
page: 43
readingTime: "8 min"
---
## La mesure du don
*[ fém. ]*
Ce terme « mesure » a un caractère très fonctionnel, mais également une portée symbolique importante. Il s'agit de ne plus considérer la june (Ğ1) comme une monnaie au sens courant du terme, mais de considérer le DU de Ğ1 comme unité de mesure du don.
Une façon très factuelle de *prendre la mesure* du don ;-)
La mesure du don, de même que la notion de contre-don, n'est pas une négation du don — c'est une solution très élégante pour gérer les asymétries, éviter les ressentiments qui naissent tôt ou tard, car ces asymétries sont inévitables et permanentes.
La posture « *je ne veux rien en retour* », de facto, *oblige* l'autre. Elle peut même mettre dans l'embarras : celle qui reçoit ne sait pas quel est le protocole convenable. Ici, le don n'oblige pas. Le protocole est clair — il suffit de mesurer la valeur que j'attribue à ce don, mon degré de gratitude, avec le DU comme unité.
Pour bien saisir la portée de ce geste devenant structurant : je donne avant tout à mon économie, à mon bassin de vie. Si j'apprécie la personne en face, j'y mets une dimension plus affective. Mais si je ne l'apprécie pas, non seulement je peux quand même donner — mais c'est surtout désirable, car c'est mon économie, tout mon bassin de vie, que je sers.
## Retournement sémantique
*[ masc. ]*
À compter du moment où l'on ne considère plus le DU comme une monnaie, et que l'on considère tout geste dans le sens du don, on peut naturellement changer de vocabulaire.
*Je ne vends plus, je donne.*
Ma production, mon temps. Je redonne aussitôt à « l'offre » un sens plein. C'est *mon offre*.
*Je n'achète plus, je reçois.*
Des biens, des services. Je ne crains plus une dépossession monétaire, je valorise la « joie de recevoir ».
*Je ne paye plus, je mesure.*
Je donne du poids, de la masse, des degrés — à la gratitude que je souhaite exprimer, que je souhaite maintenant imprimer dans un registre partagé distribué. En transférant un DU, *j'investis sur un don*, un geste que je souhaite voir se développer dans mon économie.
*On ne négocie plus un prix, on ajuste une balance.*
*On construit nos échelles de valeurs individuelles.*
Et on les frotte à celles des autres. On construit par le même temps des échelles de valeurs collectives.
*On cherche des équilibres, ensemble.*
---
Bien sûr il y a des contextes où l'on n'a pas le temps de jouer à ce jeu des équilibres — par exemple lorsqu'il y a une notion de débit ou une queue. C'est simple : le don se situe en amont. Une buvette a fait ses estimations internes d'équilibre, elle trouve une valeur de référence pour le verre servi. Le don consiste à offrir toute la buvette elle-même. Rien ne m'empêche de gratifier davantage.
On relativise, autour de notre invariant, cadeau de la TRM, le DU.

127
content/book/04-monnaie.md Normal file
View File

@@ -0,0 +1,127 @@
---
title: "Raison d'être d'une monnaie"
description: "Voyons à quel point les modalités de création de chaque unité de monnaie sont déterminantes, comme un code génétique, pour l'économie que cette monnaie servira."
order: 4
page: 49
readingTime: "22 min"
---
[ *fém.* ]
Je ne vais pas faire un cours sur la monnaie, ni sur son histoire, mais l'évocation d'une absence de monnaie pose tout de même la question de sa nécessité, de son rôle structurant. Elle pose également la question du besoin d'une monnaie radicalement différente. Différente de l'euro, et plus généralement de ce qu'on appelle les « fiat ». Fiat n'est pas un acronyme, c'est le mot latin qui fait référence à quelque chose qui n'existe que par le fait d'avoir été déclaré comme tel. Fiat lux : que la lumière soit. Fiat euro : que l'euro soit.
Autrement dit, avec une question simple :
- Pourquoi se compliquer la vie avec une autre monnaie ?
## Au-delà de ses 3 fonctions
[ *masc.* ]
Dans une image architecturale dont les fondations seraient les institutions bancaires, la monnaie et plus précisément la création monétaire, devient la clé de voûte de l'édifice. Tout ce que l'on construit au sein de cette structure et tout ce que l'on vit dans l'édifice, notre économie, repose sur ces institutions bancaires et tient sous l'égide de la création monétaire.
La création monétaire est programmatique, comme une ligne de code. Elle détermine certains comportements de toutes les entités économiques chaque jour. Dès que j'achète ou que je vends, dès que j'estime mentalement une valeur, aussitôt je m'inscris dans cette programmatique, dans son référentiel.
Les fameuses trois fonctions formulées par Aristote :
- unité de compte : c'est la mesure.
- valeur d'échange : c'est la transaction (apporte la symétrie).
- réserve de valeur : c'est l'épargne.
La fonction d'unité de compte est plus importante qu'il n'y paraît. C'est parce qu'elle est unité de compte qu'elle peut être une valeur d'échange, et c'est en tant que valeur d'échange qu'il devient indispensable d'en posséder. Si je pense à une cafetière économiquement, je pense un chiffre en euros. Je ne me demande pas si je trouve juste cette équivalence entre un plein d'essence et une cafetière ; … l'est-elle ?
Dans notre économie monnaie-dette, l'usage de réserve de valeur a pris l'ascendant sur les échanges. L'équation de Fischer : E = Q x V. La quantité monétaire multipliée par sa vitesse de circulation mesure l'économie. Si la monnaie est davantage stockée pour faire des réserves, l'économie ralentit. Mais cette formule ne définit pas l'économie elle-même ; elle dévoile en creux à quel point l'économie est d'abord une affaire de production.
Petit message aux monnaie-libristes : il ne suffit pas de faire circuler la monnaie pour se réjouir de son économie. L'économie commence avec une production distribuée.
Je finirai en rappelant que la création monétaire est un pouvoir majeur, ceux qui le détiennent déterminent les échelles de valeur. Donner le même accès à la création monétaire pour tout le monde sans aucune distinction, est bien sûr un cadeau du DU. Mais ce n'est pas la monnaie, toute libre soit-elle, qui crée de l'égalité dans le monde réel.
> Voyons à quel point les modalités de création de chaque unité de monnaie sont déterminantes, comme un code génétique, pour l'économie que cette monnaie servira. Ou asservira, ce qui est devenu le cas de la « nôtre ».
## Bassin économique
[ *fém.* ]
> « à chaque monnaie son économie ».
Si on se mobilise sur le développement d'une monnaie aux modalités de création propres, c'est justement parce qu'elle permet de créer une autre économie. Et si on se mobilise c'est parce que le code monétaire que nous vivons actuellement, celui de notre euro, pose problème. Gros problème. Or rien ne peut y remédier, c'est un code… génétique.
Les monnaies locales ou complémentaires reposant sur une indexation euro, ne permettent pas vraiment de créer une économie différente. Les études économiques sur ces monnaies sont souvent biaisées car endogènes. Ces monnaies créent une sorte de « club », comparable aux « cartes réseaux ». Tous ces collectifs sont précieux car ils s'intéressent à la question monétaire, mais si l'on cherche un impact structurant, il est nécessaire de rompre à la racine avec le code monétaire des fiat.
## Problèmes génétiques des fiat
[ *masc.* ]
Voici trois problèmes majeurs non solvables, de nature strictement monétaire, programmés par le code de création de notre cher euro.
La quasi-totalité (>90%) de la monnaie qui circule est produite par ladite dette. Vous souscrivez un crédit de 100 k€, la banque crée 100.000 unités dont vous pouvez disposer. Les 100.000 unités n'existaient pas, elles sont créées ; puis détruites quand vous les rendez. Nous devons rendre cette quantité de monnaie, mais nous-mêmes ne la créons pas (les banques ont le droit, pas nous) ; nous devons donc capter des unités qui circulent, en produisant une valeur non encore produite.
## Leçon #1 — La monnaie-dette est une cavalerie
Dans ce référentiel monétaire, la croissance n'est pas une option.
[ *fém.* ]
Produire pour rendre la même quantité de monnaie ?
Non. Il faut ajouter lesdits intérêts aux 100 k. 3% sur 10 ans, ça fait près de 30 k, 30 %. Or ces 30.000 unités supplémentaires requises n'ont pas été créées au moment du crédit. Vous devez capter des unités à un autre, qui doit aussi le faire, qui lui aussi doit capter 30 % qui n'ont jamais été créés. Pensez-vous que tout le monde puisse gagner sa partie avec une telle règle du jeu ? Une véritable chaise musicale, où il manquerait non pas une mais le quart des chaises ?
## Leçon #2 — C'est une machine à faillites
Dans ce référentiel monétaire, la compétition, pour ne pas dire une féroce prédation, n'est pas une option.
[ *masc.* ]
Imaginez une population totale de 50 « personnes ». La monnaie créée M est de 5 millions d'unités circulantes. Mais elle doit reverser collectivement 6,5 M€. Où trouve-t-elle le 1,5 million d'unités qui n'existent pas ?
38 « personnes » sur 50 peuvent rembourser leurs 130 k, soit 3,8 millions détruits et 1,14 million captés par la banque. Il reste 60 k€ dans l'économie des 50 personnes, mais 95% dans les caisses de la banque ! 12 personnes en faillite monétaire collective doivent encore verser 1,56 million avec 60 k disponibles. Vous voyez le tableau ?
Comment un petit 3% d'intérêt devient un drainage massif proche de 95% ? Le phénomène est noyé dans la longue durée et dans un volume qui dépasse l'entendement de tout individu. Le seul moyen de créer les 1,5 M€ manquants, c'est de souscrire un crédit de 1,5 M€. La dette passe alors immédiatement à 3 M€. Cela ne résout jamais le problème, ça l'aggrave.
> C'est ce qu'on appelle une cavalerie : lorsque vous souscrivez un second crédit pour rembourser le premier, puis un troisième pour rembourser le second, ... un jeu qui finit rarement bien.
Il y a deux formes de pénurie structurellement monétaire : d'une part cette pénurie permanente roulante du fait de toujours devoir rendre plus de monnaie qu'il n'en a été créée ; d'autre part une pénurie itérative, erratique, lorsque les banques ferment le robinet. C'est la raison pour laquelle la peur de manquer invite tout le monde à mettre de côté, raison pour laquelle la fonction de valeur de réserve prend le pas sur sa fonction de valeur d'échange.
## Leçon #3 — Ce système monétaire est une pyramide arbitraire qui impose ses valeurs
Dans ce référentiel monétaire, subir ladite Loi du marché n'est pas une option.
[ *fém.* ]
Concrètement, vous allez chez le banquier présenter votre projet selon les critères des banques. Ce qu'ils signifient et les critères qui les définissent appartiennent aux banques, soit une poignée de personnes qui en déclarent les règles de fonctionnement.
En résumé, un très petit nombre de personnes décide ce qui vaut, ce qui est valeur, et ce qui ne l'est pas. Les déchets n'ont pas de valeur, de même les activités dites sociales ou solidaires. En dessous des déchets il y a par exemple le « geste infirmier à domicile ». Tout en haut il y a le traitement massif de la numérisation de tous nos faits et gestes.
Si les personnes ayant besoin d'un soin à domicile déterminaient de façon proportionnelle, selon leurs échelles de valeurs, la répartition des milliards de devises créées chaque jour, ... ces devises seraient-elles réparties de la même façon ?
Tous les projets cherchent la conformité, les requérants jouent la scène qui leur est demandée. Fatalement les projets de petite envergure, ceux dont les modèles éco sortent des sentiers battus, passent rarement les mailles du filet.
> La monnaie-libre et le code génétique qu'elle embarque sont radicalement différents de la monnaie telle que nous croyons la connaître ; elle propose un référentiel relativiste qu'il nous appartient de mettre à notre service. Nous avons maintenant une liberté de choisir, à nous de la saisir.
>
> Mais pouvoir ne signifie pas savoir, encore nous faut-il collectivement apprendre à le faire.
## Une économie mal codée
[ *masc.* ]
> On entend de nos jours de belles expressions qui parlent de « dette roulante », ou de « dette qui paie la dette ». Elles sont un voile pudique sur cette cavalerie insoutenable. Le résultat de cette cavalerie, de cette croissance requise effrénée, c'est une extraction de toutes les ressources, planétaires et humaines, qui prend des allures de pillage. Or si vous suivez le raisonnement, ce pillage est inéluctable.
>
> Le besoin monétaire est exponentiel, donc ladite croissance doit l'être, ainsi la consommation d'énergie et l'extraction de toutes les ressources.
Donc tous ceux qui affirment que la croissance est obligatoire ont raison. Tant que la source monétaire qui abreuve notre économie sera une monnaie-dette.
C'est pour cela qu'en dépit de toutes les COPs, GIEC, les Conventions citoyennes, les intentions et les mobilisations, les chiffres parlent. Il n'y a pas le moindre début d'inflexion dans l'extraction, dans la consommation d'énergie et de matières, dans aucun domaine structurant de fait.
Heureusement localement, il y a d'innombrables expériences et initiatives magnifiques. Dans une vue satellitaire elles sont marginales, mais dans le paysage elles sont vitales.
Pour conclure, si l'on veut mener une réflexion sur la décroissance, sur la sobriété, la contraction, la robustesse non performante, il faut changer de création monétaire. La bonne nouvelle, c'est que la TRM ou la monnaie-libre, propose une, on ne peut plus compatible avec ces réflexions.
> Il s'agit d'embarquer dans une aventure,
> peut-être encore marginale
> mais stratégiquement valable,
> peut-être laborieuse
> mais éventuellement ludique,
> peut-être utopique
> mais, *sans doute*, significative.
Quelle part de nos besoins, quelle part de notre économie pourrait couvrir une production sans plus vraiment d'extraction, sans plus vraiment de « laissés pour compte » ? Jusqu'où peut-on pousser les curseurs ?

177
content/book/05-trm.md Normal file
View File

@@ -0,0 +1,177 @@
---
title: "La TRM"
description: "Corréler directement le flux monétaire au flux de la vie humaine — les principes et raisonnements de la Théorie Relative de la Monnaie."
order: 5
page: 83
readingTime: "22 min"
---
## La TRM Théorie Relative de la monnaie
*[ fém. ]*
Si l'on veut comprendre la monnaie libre, il est utile de comprendre les principes et raisonnements de la TRM.
[ Page de Stéphane Laborde : https://trm.creationmonetaire.info/ ]
Je ne parle pas des démonstrations mathématiques, heureusement pour le grand nombre. Mais les postulats, leur positionnement, les questionnements d'origine et la nature des réponses qu'ils apportent.
Cet essai est directement lié à ma lecture de cette théorie, elle y est présente partout. Je vous en restitue donc une partie pour que vous puissiez suivre ce livre sans avoir lu la TRM.
---
## Flux monétaire et vie humaine
*[ masc. ]*
L'un des fondements de la TRM :
> Corréler directement le flux monétaire au flux de la vie humaine.
L'image est belle et ce n'est pas qu'une image.
La création monétaire repose ici sur l'existence physique d'un individu. La monnaie que l'individu crée apparaît avec sa naissance dans son bassin éco. De la même façon, cette monnaie créée meurt avec lui, ou lorsqu'il décide de quitter la monnaie et son économie.
Une propriété mathématique conséquente de la TRM est la « convergence à la moyenne ». En ajoutant une quantité d'unités chaque jour (le DU quotidien), la même pour tout le monde, tous les créateurs monétaires tendent vers le même nombre de DUs. Si vous êtes en-dessous de la moyenne, le nombre de vos DUs augmente ; au-dessus, il diminue. De façon asymptotique, magique mathématique.
La réflexion immédiate qui vient à l'esprit concerne « L'inflation ». Il n'y a pas une mais deux familles d'inflation bien distinctes : une inflation économique (pénurie d'un produit ou de ce qui permet de le produire) et une inflation monétaire (surabondance de monnaie dans l'économie).
Du point de vue relativiste dans la monnaie libre, le nombre de DUs finit par atteindre un plafond asymptotique dès que N se stabilise. Dans le référentiel relativiste, il n'y a pas d'inflation monétaire possible.
> Or c'est justement ce que fait précisément la monnaie libre. Il y a une création monétaire continue, corrélée aux flux de toutes les vies en cours, répartie également sur chaque personne. Dans le référentiel relativiste, il n'y a pas d'inflation monétaire possible.
Et si vous faites l'impasse sur les démonstrations, retenez simplement qu'en comptant en DUs, vous voyez que l'inflation ne peut plus être une question monétaire ; vous voyez que la quantité monétaire créée par un individu naît et meurt avec lui, telle une petite vague qui passe.
C'est vraiment beau.
*Pointe une petite larme ? Loin d'être petite, l'arme.*
---
## Symétrie dans l'espace-temps
*[ fém. ]*
Personne en aucun lieu ni aucune époque ne peut créer de la monnaie au détriment de l'accès monétaire d'une autre personne, de tout lieu et de toute époque. Une belle interprétation d'un leitmotiv de John Locke, celui de la non nuisance.
Dans la recherche originelle de Stéphane Laborde, en bon ingénieur, il y avait en premier lieu la recherche d'une mesure invariante. Il pose l'économie en tant que champ en expansion, or la mesure de ce champ ne cesse de fluctuer. Est-il possible de trouver un étalon, une unité de mesure qui ait la même valeur pour toutes, une mesure universelle ?
Cela ne peut pas être le cas de l'or. Dans le champ de l'économie, les référentiels ce sont les gens, nous toutes. Il s'agit donc de trouver la même unité de mesure pour moi ou pour une Papoue de Nouvelle-Guinée, pour une Inuit du 41e siècle. Cet étalon universel ne peut être aucune des valeurs produites ou extraites par l'économie elle-même. Il ne peut donc s'agir que d'un nombre.
> La seule valeur tangible, universelle, et seule valeur qu'on ne puisse pas totalement enlever d'une « zone économique » sans que cette économie s'éteigne immédiatement, c'est … la vie humaine.
Pour que le flux monétaire suive celui de notre vie, son taux de croissance doit être corrélé à la croissance de l'individu. D'où une formule logarithmique sur l'espérance de vie (ev=80), qui donne un taux de croissance proche de 10% annuel.
Bingo, c'est la naissance du Dividende Universel, le DU.
> - Créer soi-même l'unité de mesure de l'économie et toutes nos valeurs, pour manipuler et partager un invariant.
> - Créer soi-même sa part de monnaie, qui suit le flux de sa propre vie, égale à toute autre sur un principe de non-nuisance.
>
> C'est la même chose, la même formule, la formule du DU !
Plutôt génial non ?
[ Formule du DUğ1 : DU*t+1* = DU*t* + c² × (M/N)*t* / 182.625 ]
[ 5, 48 % / *an* ≤ c ≤ 9, 22 % / *an*, selon que l'on prenne ev ou ev/2 pour symétrie ]
*Cela tombe bien : « ex perfecto nihil fit ».*
*[ de la perfection rien ne naît formule alchimiste ]*
---
## Relativité
*[ masc. ]*
Que peut m'inspirer une unité de mesure universelle relativiste pour estimer les valeurs ? Je peux mesurer toutes les valeurs de façon relative — relativement à la masse monétaire créée dans mon bassin éco, « M » ; et au nombre de personnes « N » qui créent cette monnaie. Estimer les valeurs par rapport à la moyenne « M/N » est un premier repère très intéressant.
Une observation terrain dévoile rapidement un biais de genèse : le gros décalage monétaire entre les personnes qui créent leur DU depuis plus de 8 ans et ceux qui viennent de commencer. C'est paradoxal, car la monnaie-libre fait la promesse d'une « égalité ». Mais c'est une égalité d'accès à la création monétaire, pas une égalité de richesse.
De plus, la richesse n'est pas monétaire ; on s'en rend compte dès que l'on quitte le référentiel de la monnaie-dette. Dans celui de la TRM, ce n'est plus la modalité de création monétaire elle-même qui crée les écarts. C'est dans les économies que nous créerons, par exemple une économie du don, que les écarts peuvent être réduits, sans commune mesure.
---
## Ancienneté
*[ fém. ]*
Pour corriger ce biais de genèse dans les échanges eux-mêmes, on peut appliquer un *coefficient relatif à l'ancienneté*. Une simple formule qui baisse le « prix » pour les nouvelles créatrices et l'augmente pour les plus anciennes, avec un effet qui s'amenuise dans le temps.
J'ai commencé à produire un petit utilitaire pour manipuler facilement ce coefficient. Il suffit de renseigner une valeur de référence pour soi-même et un pourcentage de réduction pour une débutante. L'utilitaire donne alors la valeur relative pour toutes les anciennetés.
[ *GrateWizard* : https://pricewizard.vercel.app ]
Ce coefficient ne fait aucun jugement de valeur sur le comportement économique de l'une ou l'autre partie. C'est un correctif mathématique d'un biais de genèse mathématique, qui s'opère uniquement dans les échanges avec le consentement des parties.
---
## Volume des offres
*[ masc. ]*
Une autre observation terrain : les offres sont réduites en variété et en volume. Pour un problème de volumes limités, on peut estimer les valeurs relativement aux quantités produites. Pratiquer une valeur de référence qui augmente avec le volume reçu — inversion de flux. Prendre trop de volume peut constituer une nuisance à l'autre menacé de pénurie ; on ne l'empêche pas, mais on le régule de façon organique. Cette inversion de flux est très symbolique.
Beaucoup déplorent les déséquilibres entre les personnes qui fournissent beaucoup et d'autres qui « se contentent de consommer ». Nous mettons immédiatement les pieds dans les jugements de valeur moraux. Mais si l'on est réaliste, on sait que c'est inévitable, donc voyons comment réduire ce recours.
---
## Diversité des offres
*[ fém. ]*
Sur les personnes *« qui ne font rien pour recevoir des DUs »*, j'évite de juger et je cherche des vecteurs d'attraction qui déclencheraient le mouvement.
Il peut s'agir initialement d'un produit ou d'un service dont tout le monde fait l'usage. Si mon économie fournit de la bière parce qu'elle aura trouvé le moyen de la produire, alors tout le monde pourra se dire « je peux attraper de la bière artisanale » avec des DUs. Disposer de telles valeurs dans un bassin de vie changerait la donne. Mais à nouveau cela commence par une production.
En attendant, on peut imaginer un coefficient relatif au solde. L'idée : soustraire le nombre de DUs créés de mon solde. S'il reste positif, c'est que j'ai reçu plus de DUs qu'il n'en valorise mes offres reçues ; j'ajuste donc le degré de gratitude en l'augmentant pour les soldes négatifs et en le diminuant pour les soldes positifs.
Coefficients relatifs à la moyenne monétaire, à l'ancienneté, progressifs au volume, relatifs au solde, à la pénibilité, … à tout ce qui vous semble juste. C'est ainsi qu'une économie peut remodeler nos échelles de valeur collectives.
Les vices de notre économie montrent à quel point il est illusoire de vouloir rectifier notre économie euro. Ces leçons donnent donc de bonnes raisons de vouloir en créer une autre. La question n'est pas tant pourquoi se mobiliser, elle est davantage :
> « qui va le faire ? »
---
## Échelles de valeurs
*[ masc. ]*
La question n'est pas tant de considérer un principe puis de dire qu'on y tient beaucoup. Il s'agit davantage, dans un contexte vécu, de positionner ce qui passe devant.
Dès que l'on compte en euros, on navigue dans des chiffres et des ordres de grandeur qui ne nous appartiennent pas. Si l'on trouve qu'un produit est trop cher, c'est évidemment dans une lecture des prix du marché, et non de la valeur que le produit devrait avoir par rapport aux autres, au travail qu'il représente, ni même souvent au besoin que j'en ai.
L'exercice de construire nos propres échelles de valeurs peut paraître difficile, mais il peut devenir amusant. Naturellement nous sommes tous paumés dès que l'on ne compte plus en euros, parce que l'on perd notre référentiel collectif. Mais c'est justement cela qui nous permet de les reconstruire, cette fois-ci de façon totalement décentralisée, sur chaque bassin de vie.
C'est en cela que le DU devient une sorte de condition car c'est notre invariant. Il aura toujours la même valeur relative pour tout le monde. Il nous permet d'avoir un référentiel commun tout en naviguant dans des milliards d'échelles de valeurs différentes.
> Chercher à constituer une seule communauté est une illusion, c'est même selon moi une grosse erreur stratégique. Au contraire, il me semble plus fertile de multiplier les groupes locaux par affinité et proximité, de créer donc « recruter » des équipes ; puis entretenir des relations avec ses groupes voisins et d'autres plus lointains, cultiver des protocoles.
Dès que j'affecte un DU, je signifie une valeur aussi simple soit-elle, cela devient un geste d'investissement, je construis une économie.
---
## Convergence à la moyenne
*[ fém. ]*
Imaginez 2 personnes, l'une avec 1.000, l'autre avec 9.000. L'écart est énorme. En ajoutant 1.000 aux deux, ça donne 2.000 et 10.000. L'autre n'a plus que 5 fois plus. Vous recommencez ; l'écart tombe à facteur 3 (4k contre 12k) ; 4 coups plus tard, l'autre n'a plus que 2 fois plus (8k contre 16k). Ainsi de suite, elles finissent par se rejoindre à la moyenne.
Pas besoin de déshabiller Jacques pour habiller Paul, pas besoin de prélever les uns pour établir des équilibres avec les autres. Plutôt bouleversant par rapport à nos schémas quantitatifs de redistribution monétaire, non ?
Lorsque le nombre de créateurs monétaires sera stable, tout le monde tendra vers une moyenne qui oscille entre 3.742 et 3.925 DUs. Au jour où j'écris ces lignes, la moyenne est à moins de 1.300 DUs.
En contrepartie, j'ai la garantie de ne jamais totalement manquer dans la durée. Dans les passes difficiles, je sais pouvoir compter sur une ressource mutualisée par mon économie, car mon DU me permet de valoriser quelques dons reçus. Un petit espace vital sécurisé, pour peu que cette économie soit suffisamment développée.
---
## Commun monétaire
*[ masc. ]*
> Considérer notre création monétaire comme un commun donne une bonne résonance au terme Dividende Universel, car il suggère le fait d'être tous co-propriétaires à part égale relative du capital monétaire de notre bassin de vie.
Faire des petites projections, mettre un peu de chiffres sur son bassin de vie, est très éclairant. Dans le mien par exemple, un premier jalon repère à 1.024 créateurs monétaires pour une population de 61.444 personnes donnerait un capital commun monétaire de près de 4 millions de DUs. Que pourrait-il couvrir, quelle économie peut-il alimenter ?
Aujourd'hui nous sommes approximativement 150, avec une moyenne au doigt mouillé de 800 DUs dans la vallée, soit 120.000 DUs. Par quoi on commence ?

140
content/book/06-produire.md Normal file
View File

@@ -0,0 +1,140 @@
---
title: "Produire"
description: "Couvrir les besoins, pour vivre, nourrir les plaisirs de vivre. Avant tout comment produire, comment distribuer, sans nuire."
order: 6
page: 121
readingTime: "28 min"
---
## Produire
[ *fém.* ]
Qu'avons-nous déjà vu ? Que l'économie commençait avec une production distribuée. C'est le geste technique jalon pour ainsi dire, mais ce n'est pas sa finalité.
Je complète ici ma proposition, qui n'est pas une définition descriptive mais une formule simple pour exprimer la raison d'être d'une économie :
> *Couvrir les besoins, pour vivre,*
> *nourrir les plaisirs de vivre.*
>
> *Avant tout comment produire,*
> *comment distribuer, sans nuire.*
Maintenant que nous avons détaillé le retournement de ce modèle éco monnaie-libriste, de cette « économie du don », voyons comment amorcer.
Nous allons commencer avec une économie de proximité. Le critère est simple : ce qui nous semble accessible. La proximité est naturellement géographique, mais il peut y en avoir d'autres formes.
## Produire
[ *masc.* ]
Pour couvrir les besoins et les plaisirs de vivre, devoir préalablement produire peut paraître comme une lapalissade, mais à l'usage je me rends compte que c'est un détail négligé par les « monnaie-libristes », les « selistes » et autres expérimentateurs alter-économistes.
Produire, avant de … *donner* versus *partager* versus *répartir* versus *distribuer* versus *échanger*.
C'est aussi dans la façon de produire que l'on pourra changer ou créer un monde. La façon d'échanger peut elle-même être intégrée davantage dans la façon de produire, comme une extension organique plus naturelle.
Quelle économie peut-on créer, en poussant les curseurs de notre affranchissement aux différentes emprises structurelles ? Notamment celle de la mondialisation extractiviste, de la pétrochimie, de la centralisation numérique ?
À nouveau il s'agit de pousser les curseurs, pas de remplacer du jour au lendemain. Plutôt que « comment vivre sans pétrole ? », la question devient « combien de jours tient-on sans pétrole ? Peut-on ajouter un zéro à ce nombre ? »
## « Passer la seconde »
[ *fém.* ]
L'image évoque une vitesse, mais elle désigne plus précisément un « régime moteur ». Tant que l'on « reste en première », nous mobilisons une énergie trop importante pour un mouvement limité.
**1. En première : productions individuelles.**
- Ğmarchés, productions maison.
- Prestations individuelles et services personnels.
- Phénomène du vide-grenier.
- Position de mécène pour lesdits « producteurs ».
**2. Passer la seconde : productions collectives.**
- Productions récurrentes en volume ; et distributions.
- Filières semi-artisanales, boucles bouclantes.
- Réseaux décentralisés (épicerie, restaurant, réparation,..).
- Mutualisation locale des équipements et outils.
- Centrales villageoises (de quartier urbains et ruraux ?).
**3. Passer la troisième : déploiements logistiques.**
- Relation éco inter-bassins et ruraux-urbains.
- Transport et distribution décentralisée « à l'échelle ».
- Centrales villageoises autoconso (« grid locale »).
**4. 5. Quatrième Cinquième : industrialisation versatile et gestion de communs ? *le logement* ?**
Évoquer une 4ème voire une 5ème vitesse me permet de glisser ce terme d'industrialisation versatile, et de poser le logement comme un graal pour notre économie monnaie-libriste.
Le propos est d'inviter à une réflexion plus stratégique et à ce changement de régime. Mars 2026, l'écosystème sera capable de recevoir plus de volume et d'usage, c'est l'heure d'embrayer.
## Économie de greffe
[ *masc.* ]
Le propos n'est pas un remplacement de notre économie, une totale substitution. Ce serait ignorer sévèrement … l'économie. Le propos plus réaliste est de créer une balance, pousser les curseurs. Progressivement.
Une première réflexion porte sur la capacité d'une économie de produire ses moyens de production. Une économie qui émerge n'a pas cette capacité et repose donc sur des outils issus de l'économie euro.
Nous pouvons appeler ce phénomène « transfert d'énergie fiat ». Il est très utile lorsque ce transfert a lieu pour produire. Lorsque c'est pour une consommation directe, c'est davantage une illusion.
Une proposition de cohabitation, pour tous nos comportements économiques, est de ne pas mélanger les deux économies. Ce serait comme mélanger deux syntaxes de programmation pour coder une même fonction. En revanche, on peut y voir l'image du mélange de l'huile et de l'eau : deux espaces qui ne demandent qu'à se séparer, mais tout en restant l'un dans l'autre.
Savoir pratiquer l'une ou l'autre, savoir passer de l'une à l'autre sans trop de difficulté, est très très puissant.
S'il s'agit d'une propriété privée, il est nécessaire de trouver la motivation intrinsèque du propriétaire, mais une amitié peut suffire. S'il s'agit d'une propriété publique, il faut être très rigoureux sur le vocabulaire et bien s'attacher au caractère ludique, à « gamifier » : une « chorégraphie du don » sonne mieux qu'une économie du don. Le secret est d'être sincère, mais c'est ce que font les cœurs généreux.
Comment financer en euros les besoins euros de nos productions ? Mauvaise nouvelle, ce sera beaucoup plus difficile à faire qu'à dire. Ma priorité est de chercher des idées dans l'économie euro pour financer les besoins euros de l'économie du don. Le plus simple est donc de produire ou rendre des services avec un modèle éco classique d'une part, et mener une expérimentation de modèle éco monnaie-libriste d'autre part.
Une forme de don que l'on peut cibler s'apparente davantage au mécénat, et s'adresse aux personnes affranchies, des âmes entreprenantes qui estiment avoir suffisamment sécurisé leurs revenus. Elles peuvent investir dans des moyens de production locaux, pour une double économie.
## Connaître son bassin de vie
[ *fém.* ]
Je pars de mon vécu, un bassin de vie de 60.000 personnes. Je retiens un seuil raisonnable, qui manifesterait le franchissement d'un palier sensible, de 15 %, soit 2.250 personnes impliquées dans les productions collectives, les « *mobiz* ».
Pour amorcer, je cherche un nombre économiquement structurant d'esprits entreprenants. Dans tous les mouvements collectifs, s'il n'y a pas une poignée de personnes qui animent au départ, le collectif finit par se déliter. Ce sont les « sherpiz ». Je retiens un seuil de 3,5 %, soit près de 80 personnes pour notre vallée.
L'échelle la plus gérable à l'amorce se rapproche davantage d'un bassin de 1.000 personnes, soit 75 mobiz et 3 sherpiz.
Cela pose la question des ratios actuels de notre économie euro. Quelle est la part produite localement de ce qui est consommé ? Quels sont les moyens de productions locaux ? Quels sont les ordres de grandeur ? D'où partons-nous ?
Il germe l'idée que nous pourrions produire nos propres données, construire nos propres indicateurs, comme une sorte d'observatoire qui s'autoalimenterait de nos protocoles et outils. En attendant, connaître son bassin de vie et partir du terrain me semble nécessaire, pour ne pas « théoriser dans l'éther ».
## Gestion « à l'anglaise »
[ *masc.* ]
Je suis tenté d'ériger en « règle d'or » le fait de ne prendre aucun risque pour son outil de travail. D'abord sortir la tête de l'eau, pouvoir respirer, est nécessaire pour envisager autre chose.
La « gestion à l'anglaise » est une gestion manipulable des « entrées - sorties », très proche d'une gestion de trésorerie. Dans ma métaphore du passage de vitesse, le point mort peut désigner avant tout le fait d'être en « roue libre ».
Ce calcul combine le seuil de rentabilité saisonnier et les planchers de trésorerie. D'abord un jour de l'année, celui où l'activité commence son bénéfice net — c'est-à-dire le moment où tous les coûts, les provisions et les rémunérations de l'année entière, sont couvertes. En fonction de l'activité et de sa saisonnalité, elle peut devenir un jour dans le mois ou une heure précise d'un jour de la semaine.
Le propos de ce calcul de la roue libre, c'est que lorsque l'heure a sonné, alors je suis totalement libre… de donner.
L'économie euro me commande de m'agrandir, de me développer. Elle ne suggère jamais que je puisse alléger mon labeur. Mais rien n'oblige. À l'heure de la roue libre, j'ai le choix.
Comment justifier alors mes consommations ? Je peux les déclarer en terme de don, avec transparence : lister les matières données en annexe, et en contre partie, la mesure de ces dons en DUs reçus. Le propos n'est pas de tromper ni de dissimuler, mais d'être dans la sincérité d'un geste. Nous mettons les pieds dans une terra incognita, donc le législateur va devoir défricher et cheminer.
Nous pouvons lui présenter la dimension artistique de ce geste collectif : nous sommes réellement en train de transformer une formule mathématique en une chorégraphie sociale de dons. J'ai d'ailleurs failli proposer en titre de ce livre *« une chorégraphie du don »*.
Voici un scénario illustratif. Un food truck relativement prospère a calculé une roue libre hebdomadaire à 18h le jeudi. Il décide de donner son vendredi. Il met à dispo ses ingrédients du jour et son four, crée un lieu, apprend à faire des criques avec les sortants des « bahut » et de la « ZAC ». Aucun euro dans tout ça. Dans ce petit récit, en dehors d'une distribution de pizza, il a créé un lieu, une connexion pour la relation entre les étudiants et les employés de la zac. Le vendredi, le pizzaiolo regarde et jouit du *spectacle des criques* dans son food-truck.
## Économie de flux inversés
[ *fém.* ]
Je donne avant de recevoir. En premier lieu. Ce n'est pas vraiment une règle morale, c'est une règle dynamique.
Cela commence donc par un don de son temps, de son talent, de son humeur, de soi. Notamment les premières générations pionnières qui doivent bâtir cette économie, pour que les générations suivantes puissent embarquer dans un manège qui tourne.
Raisonner en terme de flux permet aussi de les identifier, puis d'y « veiller », les maintenir comme des canaux d'irrigation. Toute personne qui rentre dans l'économie monnaie-libriste voit quels sont les flux à « servir » et peut choisir ses contributions.
Il y aura probablement quelques projets qui nécessiteront une avance de DUs, pour des gratitudes anticipées. Prenons l'exemple du paysan meunier, ressource précieuse pour tout groupe local. Une équipe se met à disposition pour une chorégraphie dans les travaux des champs, mais le meunier ne peut encore valoriser ce don car il débute dans la monnaie-libre. Une caisse d'investissement locale peut lui fournir les DUs nécessaires à cette valorisation initiale. Il pourra ensuite recevoir ses propres DUs par le don de farine.
C'est à partir du moment où ce type de transaction croise d'autres transactions que l'économie tisse sa toile. Car si l'on projette la filière pain toute seule, on peut imaginer s'en passer — mais ce serait à nouveau la conception d'un circuit fermé.

View File

@@ -0,0 +1,83 @@
---
title: "Échanger"
description: "L'échange est davantage un baromètre, qu'une variable de l'économie. Filières, boucles, distribution et Ğ(marchés)."
order: 7
page: 147
readingTime: "20 min"
---
## Échanger
[*masc.* ]
Je retournais tout à l'heure le PIB en tant que variable économique plutôt que baromètre ; lorsque je dis que l'économie est avant tout une affaire de production, je cherche à exprimer que l'échange est davantage un baromètre, qu'une variable de l'économie.
La façon dont nous échangeons est davantage un symptôme de notre façon de produire et distribuer, qu'un vecteur lui-même de notre fonctionnement collectif.
Le film du billet qui circule et libèrerait l'économie en acquittant les dettes de chaque personne est relativement trompeur. En posant la probabilité à 10% qu'un échange soit symétrique entre 2 personnes d'une même commune, pour que cela fasse une boucle de 5 personnes, la probabilité est schématiquement de 1 sur 10 puissance 5. À 1% de symétrie possible, il faudrait 10 milliards de situations vécues par ces 5 personnes. Alors que la démonstration du film donne l'impression de révéler une loi dynamique de l'économie.
Il n'en reste pas moins que la façon d'échanger est un sujet digne d'une grande attention. C'est à cet endroit que l'on pourra construire nos échelles de valeurs collectives, trouver des équilibres, discuter des moyens inédits de produire et distribuer sans nuire — puisque l'endroit de l'échange est celui de la rencontre.
## Filières et boucles
[ *fém.* ]
Je reprends l'exemple de la filière verticale du pain. Une fois pleinement goûté l'enthousiasme pour les 2 ou 3 personnes qui fournissent du pain régulièrement, voici quelques questions utiles :
- Combien de temps vont-elles tenir ? Peuvent-elles fournir tout le groupe local ?
- À notre échelle, quels volumes et rythmes pourrait-on tenir dans la durée ? Avec combien de personnes ?
- Quelle relation peut-on créer entre les extrémités de la filière ?
- Quelles sont les limites ? Quels curseurs pouvons-nous pousser progressivement ?
J'ai déjà évoqué la valeur stratégique clé des filières qui produisent des biens déjà utilisés comme des « monnaies sociales », la bouteille de vin ou de bière. Or tous les produits de fermentation sont à la portée de tous. N'est-ce pas l'occasion de nous réapproprier le sens de la responsabilité à cet endroit, de ré-inventer nos propres protocoles de prudence et là encore, inverser les flux ?
La question des boucles d'échange circulaires est très intéressante. On peut relativiser son rôle dans l'amorce d'une économie, mais ces boucles sont le symptôme d'une économie qui tisse réellement son réseau. On se rend compte rapidement de deux éléments clé :
- Les boucles doivent boucler.
- Les boucles doivent se croiser.
Une boucle qui ne boucle pas est une impasse qui ne peut générer à terme que du ressentiment et de l'abandon. Le problème des asymétries, quand on le transpose à une boucle complète, montre que cela ne peut se faire réalistement que par un croisement des boucles.
C'est difficile de vouloir *programmer* cela, mais on peut le rendre explicite et partager une visualisation collective des boucles. Chacun pourrait identifier les trous et y pourvoir.
## Distribuer
[ *masc.* ]
Si l'on part de la production, on arrive sur cette notion de distribution avant celle d'échanges. Et puisque dans une économie du don j'inverse les flux, la demande précède l'offre : lorsqu'une équipe décide de lancer une production, c'est qu'elle répond à un besoin identifié dans son groupe local.
Chaque production collective pourra expérimenter dans ce registre : quelle part affecter à toutes les personnes qui ont donné de leur temps, quelle part réserver aux Ğmarchés, quelle part pour les échanges avec un autre bassin de vie, un « Ğ1tada » en Espagne ou une « université d'été » à Toulouse ?
C'est également à cet endroit de la distribution que peuvent avoir lieu les réflexions sur la notion d'épicerie décentralisée, comme une extension naturelle de la production et comme un nœud pour le tissage de l'économie.
## Connecter avec l'existant
[ *fém.* ]
Les initiatives pour développer une économie de proximité, circulaire ou solidaire, n'ont pas attendu la monnaie libre. Il y a également toutes les initiatives de collectifs solidaires (SEL, monnaies locales, assos caritatives), qui reposent essentiellement sur un immense bénévolat. Elles s'inscrivent malgré elles dans l'économie euro moribonde, à l'endroit des « laissés pour compte ».
Toutes ont l'occasion de se saisir de la monnaie libre pour changer la donne, et consolider cette énergie coopérative afin de produire plus d'effets. Pour la première fois elles peuvent imaginer des modalités de fonctionnement qui intègrent les mobilisations et les gratitudes, afin de décentraliser les efforts et la logistique sur les personnes qui en bénéficient directement.
Cela inclut les filières de récupération, de recyclage et autres dérivés. Ces lieux et organisations pourraient servir de plateforme logistique pour les besoins de collecte en volume des productions collectives événementielles, s'inscrire dans une mutualisation des outils et des équipements.
Puisque je parle de lieux, je finirai sur le problème majeur de l'espace, pour produire ou stocker. Il y a la voie des mécènes aux grandes propriétés. Il y a la voie du lieu public, avec la Mairie ou la collectivité propriétaire. Il y a la voie de l'établissement public, notamment les écoles, où la négociation est plus intime — un proviseur, un ou deux profs engagés. Il y a la voie du lieu recevant du public, privé mais qui peut « privatiser » des moments sur invitation.
Lorsque nous atteindrons des seuils critiques, il sera envisageable qu'un groupe local de junistes suffisamment monétisé puisse acquérir un lieu et y développe une économie monnaie-libriste. Ce sera un grand pas.
Lorsqu'une économie du don pourra loger une petite proportion de sa population, alors elle aura atteint un niveau et un rythme de croisière qui ne pourra plus reculer.
## Ğ(marchés)
[ *masc.* ]
Tous les groupes locaux pratiquent les Ğmarchés. Je constate simplement qu'il en manque un, à ma connaissance : nulle part je n'ai vu un marché qui offre la liberté de choisir sa monnaie, à 100 %. C'est intrigant car c'est pourtant le format qui s'inspire le plus de la TRM.
La quête chronique de tous nos marchés est de trouver des producteurs, des pros, afin de fournir les produits que l'on trouve sur les marchés euros. Seulement voilà, ils ne trouvent pas de retour symétrique à leur engagement.
Mon idée : si je veux adresser les exposants d'une grosse foire de producteurs locaux (dont j'ai un fichier de 300 adresses), je peux potentiellement en séduire une trentaine pour participer à une expérience laboratoire. Je les fais rentrer dans un jeu avec des règles précises et on en fait le bilan à la clôture. Pour les convaincre, je dois leur garantir la couverture de tous leurs coûts marginaux : « vous ne gagnerez peut-être pas beaucoup d'euros, mais vous êtes sûrs de ne pas en perdre ». Comment ? En mutualisant les recettes euros et en pratiquant un cercle de répartition adapté en clôture de marché.
L'idée est un guichet à l'entrée pour opérer les transactions — un peu à la manière des octrois des XII° et XIII° siècles, avant Philippe le Bel. Des « lutins » ou des « pages » formés pour accompagner les hôtes sur les stands, rendre possible le jeu des coefficients relatifs, et ainsi intégrer la jeunesse dans cette aventure qui lui est d'ailleurs surtout destinée.
Ce sera donc à ma connaissance une première expérience de Ğ(marché) [ prononcer G libre marché ], bien garni de productrices, où le chaland pourra choisir sa monnaie, choisir son économie ; soit un prix en euro, soit une gratitude en DUs.
Une comm publique s'impose, très subtile, invitant à jeu privé, sur invitation « codée » — qui prend la forme d'un Ğ(marché), qui n'est ni un marché, ni un 'non marché' … du moyen âge prospère. À cette époque cohabitaient 2 économies, l'une régie par la devise du territoire, l'autre par une mesure universelle des dons. Le pèlerin peut choisir, passer de l'une à l'autre. Tout le monde déguisé. Buvette double et improvisation Aztèque du XII° siècle à l'apéro. Ambiance.

View File

@@ -0,0 +1,109 @@
---
title: "Relation institutionnelle"
description: "La relation institutionnelle est délicate à plusieurs titres : l'institution est par définition la garante de l'ordre établi."
order: 8
page: 163
readingTime: "18 min"
---
[ *fém.* ]
La relation institutionnelle est délicate à plusieurs titres :
- L'institution est par définition la garante de l'ordre établi, le prolongement incarné de l'autorité centrale.
- Elle est jalouse de son pouvoir et trouve les initiatives libertaires suspectes (décentralisation, autonomie, affranchissement, espace juridique à défricher, etc.), donc rapidement coupables.
- Il y a dans notre aventure un esprit DIY, système D, « on peut se débrouiller, pas besoin de tutelle » qui émancipe de la dépendance à l'Institution, qui vit cela comme une agression contre sa légitimité.
En contrepartie, pas question de cloisonner, d'exclure, ou de s'opposer. Beaucoup trop d'énergie perdue dans de telles postures dogmatiques.
Il est important de distinguer les institutions des personnes qui évoluent en leur sein. On peut toujours s'adresser aux personnes physiques et non aux rôles qu'elles endossent. L'idée serait que élus et fonctionnaires cheminent avant de se demander comment la collectivité pourrait servir cette économie.
## Impôts, taxes et cotisations
[ *masc.* ]
Premier point : le propos n'est pas d'échapper aux taxes, aux lois, aux cotisations. En aucun cas.
Le propos est d'échapper aux lois dynamiques de la création monétaire. Or ces lois ne sont justement pas La loi — elles sont au-dessus des lois du législateur.
La loi PACTE et le règlement MICA ne s'intéressent de facto qu'à la transaction qui échange un actif numérique avec un euro. Dans une économie du don, nous n'avons pas du tout la pratique de cette transaction. Pourquoi ? D'abord parce que le DU est une mesure — quel sens donner au fait de vendre une mesure ? Vendre des DUs en euros, c'est automatiquement retourner dans l'économie euro.
Son positionnement général est le suivant :
- Transparence et sincérité : bonne foi.
- Prudence individuelle et collective.
- Esprit de chercheur, de labo, de jeux de rôles.
- Décentralisation des décisions et des responsabilités.
- Exploration de terrains « vierges » et des marges.
Éviter les plans sur la comète semble une attitude fertile ; on verra bien quand on aura un volume significatif.
## Environnement légal
[ *fém.* ]
Loi PACTE - Règlement MICA.
Aujourd'hui seuls les mouvements d'euros sont taxés. Dans notre cas de Juniste, seulement si vous vendez des junes pour toucher des euros : c'est sur la plus-value réalisée que vous serez taxée (flat tax 30%).
Ce qui n'a pas été vraiment étudié, c'est la possibilité juridique de réfuter la définition — non pas parce que la june n'y répondrait pas, mais parce que cette définition s'applique à un usage de l'unité qui n'est pas du tout le nôtre. Comme avoir un usage du couteau qui n'est pas du tout celui d'une arme.
Par exemple : ledit actif, s'il n'a pas fait l'objet d'une acquisition, n'est-il pas davantage un passif, compris comme notre capital propre ? D'ailleurs le terme dividende universel ne désigne-t-il pas la part de la masse monétaire créée comme un commun, dont nous serions tous actionnaires à part égale relative ?
Dans notre cas, nous manipulons une mesure universelle, le DU, pour donner une mesure aux gestes d'une performance artistique collective, une chorégraphie du don. Nous ne sommes pas du tout concernés par la définition de l'actif numérique, tant que nous ne l'utilisons pas en tant que tel. C'est une posture totalement sincère. Nous n'avons rien à cacher et consignons nos mesures de façon transparente.
Lorsque le législateur aura trouvé les solutions pour calculer des taxes dans sa monnaie sur l'économie du don, nous trouverons les solutions pour aborder cette réserve dans nos productions collectives.
## TVA
[ *masc.* ]
La question de la TVA est plus ambiguë car sa définition s'affranchit de la monnaie utilisée. Il doit être fait une corrélation entre les produits donnés et le chiffre d'affaire prévisionnel qui aurait dû être encaissé en euros.
Fort heureusement, il y a une tolérance dans la pratique du don — les producteurs peuvent le faire, y compris les épiceries. Dans ce cas, les seuils et les frontières sont flous. Voire introuvables dans les textes. À nouveau, tout est une question de dosage, d'intentions et de bonne foi.
Simple règle de prudence : si vous craignez d'être pris au dépourvu, mettez de côté une provision pour couvrir ce montant.
## Bénévolat et cotisations sociales
[ *fém.* ]
Ce sujet est peut-être le plus délicat, car il touche à ce que nous avons de plus précieux — notre temps. C'est aussi ce dont l'économie euro et sa législatrice sont le plus jalouses. Elle pose donc cette question en terme de travail dissimulé.
Si j'ai besoin d'une asso pour mener mes expériences, mes événements, mes productions, il me suffit de déclarer le bénévolat. Je peux déclarer dans ma déclaration de revenu la mesure de mes DUs si la législatrice tient finalement à en faire une monnaie. Mais pour l'instant sa seule monnaie c'est l'euro, et la seule phrase juridique que l'on puisse trouver dans les textes de loi pour définir la monnaie, c'est :
> « la monnaie de la France est l'euro ».
>
> Point. Rien d'autre ? Non.
C'est très drôle, de considérer les plus de 200.000 pages de textes de lois qui légifèrent sur absolument tout, et qui ne laissent qu'une phrase sibylline pour définir ce qui régit à sa racine notre économie.
Pour les esprits les plus joueurs, le caractère éphémère des organisations, des productions, leur côté ponctuel et décentralisé, peut devenir à la fois un leitmotiv ludique et un gage de robustesse. La législatrice ne va pas tout de suite trouver comment gérer ces phénomènes. Laissons-lui le temps d'aviser. Il ne s'agit pas de tricher — c'est simplement à chacun de poser son équilibre.
## Le financement de notre écosystème
[ *masc.* ]
Il faut réaliser que nous flirtons avec le domaine des cryptos, et que dans ce domaine les budgets de développement logiciel sont colossaux — l'échelle de grandeur s'exprime en millions d'euros.
Il est donc a minima remarquable que nous puissions faire vivre la june depuis plus de 8 ans sans aucune capitalisation, uniquement sur le bénévolat, les dons de la population juniste et 35 k€ d'une subvention de l'Ademe.
Les subventions constituent une source de financement potentielle, mais embarquent avec elles tous les questionnements de la relation institutionnelle. L'Ademe est unique dans le sens où elle a exceptionnellement une réelle autonomie sur ses actions. Chercher des subventions est un très gros boulot, un métier — ce travail requiert un tiers temps formé, ce qui est ambigu car un tel rôle appelle un recrutement dont le caractère récursif pose question.
On peut constater que les financements par le don dépassent les montants de subvention, mais ils ne sont alimentés que par 10 % des junistes. Il y a un réel potentiel d'auto-financement. Quoi qu'il en soit, ce financement est totalement dérisoire en regard de la prouesse technique de déployer une blockchain décentralisée — notre june est un ovni.
## Symboles et sémantique
[ *fém.* ]
Il suffit d'inverser le flux. L'ancienne vendeuse devenant offreuse, sait bien que la personne qui reçoit est encore plus démunie pour déclarer un chiffre. L'idée est de s'entre-aider. C'est la personne qui offre qui est la mieux placée pour donner un repère — non pas en prix, mais en offre.
> « Je positionne mon point d'équilibre à 2,75 DUs, et toi ? Ok pour jouer avec les coeff. ? »
Un bien ou service n'est plus trop cher ou bon marché, c'est un nombre de DUs qui devient peu gratifiant ou trop. On ne fait que gagner du degré de gratitude, dans les deux sens.
Je produis pour donner, mon économie me donne en retour. Je n'ai pas besoin de ressentir une symétrie dans mes échanges, je retrouve mes équilibres autour du DU. Cette symétrie est mutualisée à l'échelle de toute mon économie du don.
Le DU n'a pas de valeur, il mesure les valeurs, c'est l'unité universelle que notre existence produit.
Pour conclure ce chapitre sur un clin d'œil : le jour où nous évaluerons la valeur d'un euro en DU, par exemple pour suivre les fluctuations réelles de l'euro, alors la monnaie libre aura remporté sa plus grande victoire symbolique.

View File

@@ -0,0 +1,75 @@
---
title: "Autres greffes"
description: "Dans l'esprit du décloisonnement, de la factorisation ou de la recherche de synergie, toute réflexion de greffe peut s'avérer fertile et créative."
order: 9
page: 181
readingTime: "12 min"
---
[ *masc.* ]
Dans l'esprit du décloisonnement, de la factorisation ou de la recherche de synergie, toute réflexion de greffe peut s'avérer fertile et créative.
Il faut toujours en contrepartie faire très attention de répondre à un besoin, de ne pas tomber dans le travers fatal de proposer des solutions à des absences de problème. Il me semble beaucoup plus doux et opportun de suggérer et laisser venir, que de chercher à convaincre et faire acte de prosélytisme.
## Pôle Emploi et mission locale
[ *fém.* ]
Typiquement ces lieux sont des foyers de disponibilités, avec une disposition d'esprit propice à un engouement pour une aventure monnaie-libriste. Parce que nous cherchons à créer une économie qui échappe à la violence programmée de l'économie euro.
Mais attention : lorsque ces personnes fréquentent encore les institutions liées à l'absence de revenu euro, c'est que leur besoin immédiat est de « rentrer des euros ». Il faut être très vigilante sur les faux espoirs que l'on peut faire germer. En aucun cas la monnaie libre ne résoudra les problèmes de notre économie euro — elle n'a le pouvoir que d'en animer une autre.
## ESS
[ *masc.* ]
Toute la constellation que l'on appelle « Économie Sociale et Solidaire » est une source potentiellement riche du fait de probables convergences sur des intentions et des mobilisations.
La vigilance à entretenir est la même que précédemment. Attention de ne pas réinventer le fil à couper le beurre. Une autre vigilance porterait sur l'institutionnalisation de ces organisations, leur fragilité et leurs dépendances financières — elles sont rarement libres de facto.
Une petite dédicace aux fablab, qui peuvent devenir des lieux très structurants : une capacité à concevoir des outils locaux, un développement logiciel, et un tropisme génétique pour l'éducation populaire. Dans la perspective d'une autonomie numérique à l'échelle d'un bassin de vie, les fablabs peuvent devenir très précieux.
Les fablabs sont écartelés entre leur vocation d'éducation populaire et la pression des financeurs qui requiert un « modèle économique ». L'idée de lancer une FabUnit, jambe dédiée au modèle économique, pour libérer la deuxième jambe pour une éducation et une recherche populaire sans retenue.
> « du déchet à l'ustensile et au mobilier ».
## Assos populaires et caritatives
[ *fém.* ]
Je pose ce titre pour ne pas oublier ces associations plus historiques de solidarité — le secours populaire, les restos du cœur, les diaconats protestants, les scouts... Dans notre culture cloisonnée, on oublie qu'elles constituent un foyer considérable de générosité, d'élan altruiste et d'énergie consacrée.
Le DU peut potentiellement étendre ou fluidifier une boucle locale, une entraide, sans confusion réglementaire ou administrative.
## Productions agricoles, maraîchages
[ *masc.* ]
Les producteurs agricoles et les maraîchers sont les cibles prioritaires de la plupart des groupes locaux. Vous êtes naturellement très précieux, la notion d'autonomie est très concrète pour vous — et paradoxalement cela peut rendre plus difficile l'adoption de la monnaie libre. Pourquoi s'encombrer d'une monnaie alors que vous vous en êtes bien passé jusqu'ici ?
La seule justification serait de vouloir généraliser cette autonomie à l'échelle du bassin de vie, l'ouvrir à de nouvelles populations. Expérimenter la monnaie libre vous relie directement à son réseau, non seulement technique mais avant tout humain.
## Artisanat Commerce Entreprise
[ *fém.* ]
Un mécénat militant est bienvenu et généreux. Il peut constituer le premier pas pour développer la relation avec un groupe local. Mais il ne durera probablement pas. C'est l'occasion de chercher des motivations qui ne soient pas celle de l'intérêt financier.
Côté juniste, pour les âmes prosélytes qui veulent convaincre les professionnels, une posture structurante serait de systématiquement proposer ses propres services ou produits. Une boulangère qui réfléchit à offrir un petit pourcentage de sa production voit quatre puis six junistes parmi ses clients lui proposer régulièrement leurs services : couturière, réparateur de vélos, graphiste, préparateur de pommades... Elle trouve petit à petit du sens à offrir et disposer de DUs pour gratifier les services de ses clients.
Le circuit se dessine, sans plan préalable. Mais ici dans cette petite comptine, il commence par les offres.
## Lycées - Écoles
[ *masc.* ]
Haa la jeunesse.
Elle est la destination définitivement la plus précieuse et stratégique. La génération des 15-25 ans constitue de facto le renouvellement des pensées et des initiatives. Nous ne multiplierons jamais assez les greffons avec les 15-25 ans.
Il faut bien réaliser que c'est pour cette génération que nous œuvrons. Nous embarquons une aventure dont le pas de temps est de 10 ans. Il faut s'attendre à beaucoup d'échecs, de tâtonnements. Une économie qui émerge ne peut être qu'incomplète, bancale, fragile. En contrepartie, 10 ans ça passe vite. Nous en sommes aujourd'hui à 8, l'âge de l'enfance qui découvre le monde ? Dans 10 ans, la monnaie libre fêtera sa majorité.
Dès que nous parviendrons à intéresser les jeunes et qu'ils intègrent, voire portent le mouvement, cela change automatiquement la donne. Ce sera alors très excitant d'accompagner ce phénomène. Chaque participation des 15-25 ans donnera immédiatement beaucoup plus de sens et d'énergie à cette aventure.
Quel intérêt peuvent-ils y trouver ? Découvrir qu'une création monétaire a plus d'effet sur le quotidien qu'un raisonnement politique, jouer au Ğeconomicus ? Comprendre les cryptos et découvrir un ovni ? Mettre les pieds dans un espace vierge où tout est à inventer, un monde à créer ? Construire la filière bière ? Lancer une activité ? Organiser un bel événement avec un final concert ?

View File

@@ -0,0 +1,46 @@
---
title: "Et maintenant ?… action ?"
description: "Une chorégraphie du don ne se télécharge pas en fichier .ods. Elle s'improvise à plusieurs, elle se répète, elle se rate parfois."
order: 10
page: 193
readingTime: "5 min"
---
[ *fém.* ]
Je crois que l'événement est un bon outil pour régénérer de l'énergie, je mise sur cette vertu de l'événementiel, une date, un début et une fin, une cadence, un rassemblement, un marqueur.
Je suis souvent chagriné par les entre-soi manifestes, non seulement des monnaie-libristes, mais aussi de nombreuses organisations altermondialistes ou solidaires. Comment rendre tout ça plus perméable, poreux, interconnecté ?
Paradoxalement l'idée de la fédération ne semble pas si fructueuse. Fédérer requiert un prosélytisme et convoque la notion de représentation, de la voix unique ou officielle. Le propos me semble davantage tenir dans des coopérations ponctuelles, la factorisation des efforts, la visibilité collective, la complémentarité.
Plus modeste, plus réaliste.
> Sa raison d'être ? Passer la seconde.
> De quoi ? D'une autonomie collective.
> Où ça ? À l'échelle du bassin de vie.
> Pour qui ? Toute personne qui veut une émancipation populaire, agir, ne pas subir.
Un événement structurant sur deux jours, reposant sur deux jambes : vie numérique et économie ; logiciel libre et monnaie libre.
Le chapitre économie pourrait contenir un Ğ(marché), un Ğeconomicus, et produire des feuilles de route pour les productions à suivre. Le chapitre logiciel libre pourrait contenir une install party linux, la présentation d'un bouquet de services et le déploiement effectif des premiers nœuds du réseau (stockage ipfs, réseau monétique, ia localisée, messagerie chiffrée, nextcloud, forum, wiki, cms, visio, …).
Bienvenue au Librodrome, un rendez-vous chorégraphique et ludique, une performance artistique d'émancipation civile économique.
Je me suis dispensé de toute fiche prête à l'emploi. Ce n'est pas démissionnaire, c'est fait pour déclencher les initiatives sans les préfigurer. Voici quelques questions déterminantes pour une réflexion orientée vers l'action.
## Quelles sont nos ressources ?
Les premières sont humaines. C'est nous. Quelle disponibilité je peux dégager ? Quelles sont mes aptitudes, les rôles que je peux jouer ? Un premier topo des ressources matérielles et logistiques permet de voir d'où on part, et de repérer ce qui manquerait.
## Quel chemin ?
Au-delà des productions individuelles, quel besoin ou plaisir voulons-nous couvrir ? Par quoi on commence ? À quelle échelle — à rayon de vélo, à l'échelle du groupe local, de la vallée ?
Étaler les envies les plus mobilisantes puis faire le tour des contraintes et des facilités permet d'y voir plus clair. Quand une idée commence à agréger un bon nombre de personnes, projeter l'ambiance de la production choisie, sans se brider, met de bonne humeur. Lorsqu'une équipe prend forme autour d'une production, dépoussiérer très tôt le traitement des conflits et des départs paraît sage.
## Quel est notre rythme cardiaque ?
Quels protocoles pour prendre rapidement les décisions ? Quels espaces pour raisonner ensemble, quels outils ? Ces aspects peuvent être traités sans précipitation. Attention aux « commissions » hâtives dans les petits nombres, car elles satellisent et nuisent à la fluidité. Plutôt que de préfigurer comment doivent être prises les décisions, un « observatoire des décisions » qui consigne comment elles sont prises dans les faits, constituerait une base de réflexion initiale très riche. Pour donner sa chance à de nouvelles façons de faire, il faut leur donner la chance de se manifester.
> « Une chorégraphie du don ne se télécharge pas en fichier .ods. Elle s'improvise à plusieurs, elle se répète, elle se rate parfois, elle se réécrit selon les personnes et les lieux. Ce livre a seulement tenté de décrire quelques pas de danse possibles, à vous d'inventer les vôtres. »

177
content/book/11-annexes.md Normal file
View File

@@ -0,0 +1,177 @@
---
title: "Chapitres annexes, sujets connexes"
description: "Le monde numérique est telle une galaxie, avec différentes constellations. La june évolue au croisement de deux grandes constellations qui se superposent en partie, les cryptos et le logiciel libre."
order: 11
page: 199
readingTime: "28 min"
---
[ *masc.* ]
L'enjeu numérique est tout aussi important que celui de l'économie. Avec l'avènement des ia et le potentiel déferlement des robots domestiques que 2026 semble annoncer, la question de la vie numérique va s'imposer comme cruciale. En outre, notre aventure monnaie-libriste est issue de cette dimension nécessairement logicielle. La june évolue au croisement de deux grandes constellations qui se superposent en partie, les cryptos et le logiciel libre.
---
## *Les cryptos*
[ *fém.* ]
Je commence avec un chapitre sur les cryptos, puisque techniquement, la june en est une. Nous allons voir en contrepartie à quel point elle est un ovni et n'est comparable en quasiment rien avec les autres.
### La June est une crypto
[ *masc.* ]
Si l'on considère la définition d'une crypto selon qu'elle repose sur l'usage de clés cryptographiques, un protocole de consensus et un registre distribué ; et qu'elle produit et manipule une unité numérique échangeable ; alors oui, la june répond à toutes ces définitions. La june est une crypto complète, qui dispose et déploie son propre réseau monétique, les nœuds que nous appelons « forges ».
### Introduction sur un DeX vs. CeX
[ *fém.* ]
Une petite partie de junistes n'attend que l'introduction de la june sur un marché (DeX). Une autre partie redoute ce moment, car l'absence totale de relation structurée entre la june et toute autre devise suffit en elle-même à la distinguer.
En ce qui me concerne, ma réserve porte sur les lois cinétiques. D'un côté, je proposais la mise en œuvre d'une économie : longue durée, rythme lent, progressif, itératif. De l'autre côté, les marchés cryptos sont un jeu de prédation monétaire, aux volumes considérables et aux vitesses vertigineuses.
> *« oui bien sûr bravo c'est super ce que vous faites, tu me tiendras au jus, et bonne chance hein. Moi j'ai une nouvelle idée pour faire 100€ par semaine avec un bot sur le ĞeX, je vais essayer ça ce we. »*
Si deux flux dynamiques rentrent en contact direct, le jeu addictif de la spéculation monétaire l'emporte et finit par siphonner tout effort de création économique.
Ma position serait de créer une seconde monnaie TRM, dédiée à cette introduction sur un DeX — une sorte de « sas » symbolique et technique. Si cette devise sur les marchés monte en flèche sur un quiproquo ou s'écroule à zéro, peu importe : la june peut continuer son petit bonhomme de chemin, tranquillement, sans se soucier de l'autre. Du point de vue des amateurs des cryptos, régler les variables de ce fork — *la F1, fork one* — de façon plus appropriée aux marchés serait passionnant : espérance de vie courte, taux de croissance plus élevé, actualisation du DU quotidienne, …
### Question du « bankrun »
[ *masc.* ]
Si l'on craint l'emprise bancaire sur nos comptes, les cryptos sont-elles réellement une réponse ? J'ai commencé mon exploration avec un « investissement » de 16k€ selon une stratégie baptisée « bankrun de l'ignorant ». Je répartis 50-50 à tous les endroits où il y a polarité : 50 % stable coins, 50 % cryptos ; puis les stables moitié dollar, moitié euro. Dans les cryptos, j'ai du bitcoin — mais « wrapé » sur le réseau solana, le réseau qui me semble le plus éloigné dans sa conception du bitcoin.
Attention, il ne s'agit en rien d'une recommandation. C'est un dispositif de recherche, une expérience de laboratoire, dont je livrerai les résultats.
Je suis également passé par la case Nastasia Hadjadji et son enquête « no crypto ». Elle dresse un tableau sans concession et dénonce les fondements structurels et les dérives comportementales. La crypto est devenue une légende, un récit écrit pour des esprits libertaires. La promesse de décentralisation n'est plus tenue : 80 % du minage actuel des bitcoins sont dans les mains de 5 entreprises. Toute la structure financière peut être qualifiée de « ponzinomics » — un circuit fermé qui consomme de l'énergie, des technos de pointe et des dollars, sans rien produire en sortie. Pour finir, les arguments sur la folie que représente cette industrie produisant des nombres, vis-à-vis de l'extraction de matière et de consommation d'énergie, sont valables. Il faut reconnaître que nous sommes en plein scénario « Shadok ».
### Réseau monétique
[ *fém.* ]
D'autres cryptos, comme solana ou polkadot, se distinguent grandement du bitcoin car ils ont davantage été conçus pour la décentralisation.
[ entracte publicitaire : notons au passage que la june est de loin la championne dans ce registre, car elle va jusqu'à donner un handicap aux machines les plus performantes ; et elle prend le raspberry pi comme référence pour servir un nœud. ]
Notre blockchain ne repose sur aucun calcul de rétribution dans la devise. C'est le seul écosystème de la famille des cryptos, dont les « forgerons » ne sont rétribués que par une caisse de dons.
Je suis maintenant surtout intéressé par les possibilités d'exploiter un réseau monétique dont on puisse maîtriser les nœuds à petite échelle. Cette expérience m'a permis de réaliser à quel point il est précieux pour la june d'avoir fait le choix de déployer son propre réseau, indépendant de tout autre modèle de création de devises — exempte d'interférence extérieure.
Quant au bankrun, la volatilité des cours est telle qu'un « placement épargne » dont on ne veut pas se préoccuper tous les jours dépend totalement d'acquisitions aux moments des planchers les plus bas. Il faut avoir de la patience et une extrême prudence sur ses attentes.
Comme je ne pouvais décemment pas terminer un livre baptisé « économie du don » sur le chapitre des cryptos, je finirai sur un chapitre définitivement plus noble et fertile, j'ai bien sûr nommé … le libre.
---
## *Le logiciel libre*
[ *masc.* ]
Notre aventure de la monnaie libre s'inscrit dans la lignée directe du logiciel libre. Duniter est naturellement sous licence AGPL. Il est important de mesurer ce qui se joue à l'endroit de linux et du logiciel libre : c'est finalement le domaine où l'émancipation a le plus trouvé les modalités et les outils pour exister et se développer, au point que le monde non libre en dépende partiellement. Un affranchissement radical des « gafam », dont il est très fructueux de s'inspirer pour d'autres domaines.
L'époque où il fallait être geek pour utiliser linux est révolue. Les interfaces n'ont plus rien à envier aux deux autres systèmes, les stores et les applis sont au même niveau. Je trouve LibreOffice de meilleure qualité que la suite du leader mondial. C'est seulement la migration de toutes ses données et de ses usages qui reste un peu laborieuse.
Pour tout développeur, ce petit écosystème est une aubaine et un laboratoire. La communauté est très ouverte, transparente sur le git et le forum duniter. Cette blockchain hybride qui héberge une création monétaire et une toile de confiance, prête pour une migration symbolique le 8 mars 2026 (dans le framework rust Substrate), est unique en son genre et vaut le détour.
Les financements euros de cette constellation reposent essentiellement sur les dons des usagers et quelques subventions. L'association Axiom Team s'est mise au service de ce financement pour les devs, en opérant une rupture avec le cheminement de type fédération : elle met en œuvre des tableaux de bord pour permettre un fléchage des donateurs et suivre les affectations de chaque don.
J'aimerais également rendre hommage à toutes les personnes totalement réfractaires au numérique. Je n'ai pas une bonne nouvelle pour elles, le phénomène va s'amplifier. Je ne peux qu'appeler de mes vœux que les groupes locaux prennent ce problème à bras le corps et rendent possible une vie numérique sans aucun contact avec l'écran — au moins pouvoir vivre la monnaie libre ;-).
De même que pour le numérique, on peut comprendre toutes les résistances à un tel retournement des pratiques. Les craintes d'une dépendance au groupe, d'un contrôle social, d'un attachement idéologique à la propriété individuelle, sont toutes légitimes. Mais à nouveau, le propos est de pouvoir choisir son économie, pouvoir passer de l'une à l'autre, que chacun puisse mettre son curseur selon l'humeur. Le propos est de rendre possible.
Pensez à transmettre le livre si vous l'avez dans les mains. Les livres sont faits pour circuler, comme la monnaie ;-)
Merci à toutes et tous pour la lecture, notamment les huit âmes charitables qui ont bien voulu contribuer à la relecture.
Je prévois de continuer la réflexion, de façon plus collective, sur le domaine librodrome.org. Vous êtes les bienvenus pour y contribuer.
---
## Acronymes
**CC-BY-NC** : Licence Creative Commons Attribution, Pas d'Utilisation Commerciale.
**AGPL** : Affero General Public License, licence libre (utilisée par Duniter).
**TRM** : Théorie Relative de la Monnaie, par Stéphane Laborde.
**DU** : Dividende Universel (unité de création monétaire d'une monnaie libre, selon la TRM ; unité relative pour estimer les valeurs).
**Ğ1** : Unité de compte quantitative de la première monnaie libre, la « June ».
**DUĞ1** : Dividende Universel de Ğ1 (forme précise, avec symbole Ğ1).
**SEL** : Systèmes d'Échanges Locaux.
**JEU** : Jardin d'Échange Universel.
**ESS** : Économie sociale et solidaire.
**DeX** : Decentralized Exchange (plateforme d'échange décentralisée).
**CeX** : Centralized Exchange (plateforme d'échange centralisée).
**GPU** : Graphics Processing Unit (processeur graphique, utilisé pour les calculs de crypto et ia, progressivement remplacé).
**TTC** : Toutes Taxes Comprises.
**DAC8** : Directive européenne sur la coopération administrative (échange d'informations sur les crypto-actifs).
**EHPAD** : Établissement d'Hébergement pour Personnes Âgées Dépendantes.
**ZAC** : Zone d'Aménagement Concerté.
---
## Néologismes, anglicismes ou expressions spécialisées
**Monnaie libre** : désigne la monnaie basée sur la TRM (Ğ1 / June), dont le Dividende Universel, le DU, est la seule et unique modalité de création des unités monétaires.
**Monnaie-libristes / monnaie-libriste** : personnes qui utilisent la monnaie libre (pour l'instant il n'y en a qu'une ;-).
**Économie du don** : ici, modèle économique structuré autour du don (avec mesure en DU). C'est l'objet du livre.
**Économie monnaie-libriste** : économie dont l'infrastructure monétaire est la monnaie libre (Ğ1/June).
**Monnaie-dette** : terme qui désigne les monnaies créées par crédit bancaire. Elles sont obligatoires pour payer l'impôt dans la quasi-totalité des pays du monde.
**Monnaie fiat** : monnaie étatique ou bancaire, sans contrepartie matérielle, imposée par la loi dans le sens où un paiement par la monnaie fiat ne peut pas être refusé par un vendeur. Les fiat sont toutes des monnaies-dettes.
**Économie fiat** : économie structurée autour d'une monnaie fiat.
**Librodrome** : nom de code d'un événement en gestation, accompagné d'une plateforme coopérative de productions collectives.
**« Passer la seconde » (image)** : métaphore récurrente pour désigner le passage d'une production individuelle à une production collective.
**Bassin de vie / bassin économique** : unité territoriale et sociale de référence pour penser l'économie locale.
**Économie de greffe** : métaphore pour une économie qui se greffe sur l'existant sans le remplacer.
**Économie de flux inversés** : lecture vectorielle de l'économie, en termes de fluides en mouvement ; suggestion clé de chercher à inverser le sens, la flèche, des vecteurs.
**Économie du bénévolat / économie domestique** (comme catégories) : usages spécialisés, mais déjà largement employés dans les sciences sociales.
**Framework** : cadre conceptuel et pratique de travail, un environnement de travail, interfaces, outils, langages et protocoles.
**Open source / en open source** : modèle de transparence et d'ouverture appliqué ici à l'économie / aux règles du jeu.
**Pseudo-isolées / communautés pseudo-isolées** : terme emprunté à la socio/économie, revalorisé ici en tant qu'échelle peut-être appropriée ou désirable.
**Boîte à gratitudes** : dispositif ludique de tirage au sort pour rétribuer les tâches (expérience « made in zion »).
**Gamifier** : francisation de *to gamify*, transformer une pratique en jeu.
**Économie du don / mesure du don** : distinction fine entre ladite « monnaie du don » et « DU comme mesure du don ».
**Mesure** (pour « monnaie ») : proposition de rebaptiser la monnaie en « mesure » dans ce modèle.
**Eco si nuestra** : communauté espagnole qui « passe la seconde » en ajoutant des règles légiférées de fonctionnement.
**Made in zion** : nom d'une expérience communautaire en forêt.
**Mocica** : nom d'un projet de société sans monnaie / de gratuité généralisée.
**Ğmarchés** : nom donné à des marchés utilisant la Ğ1. Petite variété dans les formes et les usages de ces marchés, selon la culture et l'humeur des groupes locaux qui les animent.

View File

@@ -1,80 +0,0 @@
---
title: "Chapitres annexes — sujets connexes"
description: "Approfondissements : cryptomonnaies, June et blockchain, logiciel libre, réseau monétique et questions techniques."
order: 11
readingTime: "25 min"
---
Ces chapitres annexes abordent des sujets techniques et connexes qui complètent le propos principal du livre. Ils sont destinés aux lecteurs qui souhaitent approfondir certaines questions ou clarifier des points techniques.
## Les cryptos
Le mot « crypto » est devenu un fourre-tout. Il désigne pêle-mêle le Bitcoin, l'Ethereum, les NFT, les memecoins, les stablecoins, les tokens de DeFi... Cette confusion est problématique, car elle amalgame des projets aux philosophies radicalement différentes.
Les cryptomonnaies « classiques » (Bitcoin, Ethereum) partagent un trait commun : elles reproduisent, voire amplifient, les asymétries du système financier traditionnel. Le Bitcoin, par exemple, est créé par le **minage** — un processus qui favorise ceux qui disposent du plus de puissance de calcul (et donc du plus de capital). Les premiers mineurs ont accumulé des quantités astronomiques de bitcoins à moindre coût, créant une oligarchie monétaire encore plus concentrée que celle du système fiat.
La spéculation est le moteur principal de l'écosystème crypto classique. On achète des tokens non pas pour les utiliser, mais pour les revendre plus cher. C'est un casino déguisé en innovation technologique.
## La June est-elle une crypto ?
Techniquement, oui : la Ğ1 utilise une blockchain (Duniter) pour enregistrer les transactions. Mais philosophiquement, elle est aux antipodes des cryptos spéculatives.
Les différences fondamentales :
- **Création monétaire** : dans le Bitcoin, la monnaie est créée par le minage (asymétrique). Dans la Ğ1, elle est créée par le Dividende Universel (symétrique).
- **Objectif** : le Bitcoin vise à être une « réserve de valeur » (une forme d'or numérique). La Ğ1 vise à être un **outil d'échange** au service d'une économie du don.
- **Identité** : dans le Bitcoin, les utilisateurs sont anonymes. Dans la Ğ1, chaque compte est lié à une **personne réelle** via la toile de confiance.
- **Spéculation** : le Bitcoin est conçu pour prendre de la valeur avec le temps (déflation). La Ğ1 est conçue pour maintenir un **équilibre** entre les membres (convergence à la moyenne).
- **Énergie** : le Bitcoin consomme autant d'électricité qu'un pays de taille moyenne. La Ğ1, qui utilise un consensus par toile de confiance (et non par preuve de travail), a une empreinte énergétique négligeable.
Dire que la Ğ1 est une crypto est donc techniquement correct mais sémantiquement trompeur. C'est comme dire qu'un vélo et un char d'assaut sont tous les deux des véhicules : c'est vrai, mais ça ne dit pas grand-chose d'utile.
## Introduction sur un DeX vs. CeX
Dans l'univers crypto, on distingue les **CeX** (Centralized Exchanges) et les **DeX** (Decentralized Exchanges).
Un **CeX** est une plateforme centralisée (comme Binance ou Coinbase) où un intermédiaire gère les ordres d'achat et de vente, détient les fonds des utilisateurs, et applique ses propres règles. C'est pratique, mais c'est un point de centralisation et de vulnérabilité : si la plateforme fait faillite ou se fait pirater, les utilisateurs perdent tout (cf. l'affaire FTX).
Un **DeX** est un protocole décentralisé où les échanges se font directement entre utilisateurs, via des smart contracts, sans intermédiaire de confiance. C'est plus lent, parfois plus complexe, mais c'est plus cohérent avec l'esprit de décentralisation.
La Ğ1 n'est pas cotée sur les exchanges crypto classiques (ni CeX ni DeX). C'est un choix délibéré : la June n'est pas un actif spéculatif. Elle ne doit pas être achetée et revendue comme un token. Elle doit être **co-créée** par ses membres et **utilisée** dans l'économie réelle.
## Question du « bankrun »
Le « bankrun » est un scénario dans lequel tous les détenteurs d'une monnaie cherchent simultanément à la convertir en une autre, provoquant l'effondrement de sa valeur.
Ce scénario est pertinent pour les monnaies adossées à une réserve (comme les stablecoins ou les monnaies locales convertibles). Si la réserve est insuffisante pour couvrir toutes les conversions, le système s'effondre.
La Ğ1 n'est **pas** sujette au bankrun, pour une raison simple : elle n'est adossée à rien. Il n'y a pas de réserve en euro, pas de promesse de conversion, pas de « prix plancher ». La valeur de la Ğ1 repose uniquement sur la **confiance** des membres dans le réseau et sur l'**utilité** de la monnaie dans l'économie réelle.
Si tous les membres cessaient d'utiliser la Ğ1 demain, elle perdrait effectivement toute valeur. Mais ce scénario est le même pour n'importe quelle monnaie, y compris l'euro : une monnaie vaut quelque chose parce que des gens l'acceptent. Si plus personne ne l'accepte, elle ne vaut plus rien.
La meilleure protection contre le « bankrun » de la June est le développement de l'économie réelle en monnaie libre. Plus il y a de biens et services disponibles en June, plus la monnaie est utile, plus les membres ont intérêt à la conserver et à l'utiliser.
## Réseau monétique
Le **réseau monétique** de la Ğ1 est l'ensemble des outils techniques qui permettent d'effectuer des transactions en monnaie libre.
L'infrastructure repose sur **Duniter**, le logiciel qui gère la blockchain de la Ğ1. Duniter est un logiciel libre, développé par la communauté, qui implémente les règles de la TRM : Dividende Universel, toile de confiance, consensus décentralisé.
Côté utilisateur, plusieurs applications permettent d'interagir avec la Ğ1 :
- **Cesium** : l'application historique (web et mobile) pour gérer son compte, envoyer et recevoir des Ğ1
- **Tikka** : une application mobile plus récente et plus ergonomique
- **Gchange** : une place de marché en ligne pour publier des annonces de vente/achat en Ğ1
Le réseau monétique de la Ğ1 est encore jeune et en développement actif. L'ergonomie et la fiabilité des outils s'améliorent continuellement. C'est l'un des chantiers les plus importants de la communauté : des outils simples et fiables sont indispensables pour l'adoption à grande échelle.
La transition de Duniter v1 vers v2 est en cours, avec des améliorations significatives en termes de performance, de scalabilité et de fonctionnalités.
## Le logiciel libre
La monnaie libre et le logiciel libre partagent un ADN commun. Les quatre libertés du logiciel libre (utiliser, étudier, modifier, redistribuer) font écho aux quatre libertés économiques de la TRM.
Richard Stallman, le fondateur du mouvement du logiciel libre, a montré dès les années 1980 qu'un commun numérique — le code source — pouvait être géré de manière coopérative, sans propriétaire exclusif, par une communauté de contributeurs bénévoles. Le résultat est impressionnant : Linux, Firefox, WordPress, LibreOffice, et des milliers d'autres logiciels libres sont aujourd'hui utilisés par des milliards de personnes.
La monnaie libre est au système monétaire ce que le logiciel libre est au système informatique : une alternative fondée sur la **liberté**, la **transparence** et la **coopération**.
Toute l'infrastructure technique de la Ğ1 est en logiciel libre. Le code est ouvert, auditable, modifiable par quiconque. Les développeurs contribuent bénévolement (souvent rémunérés en June par la communauté). C'est un commun numérique au service d'un commun monétaire.
> Coder la liberté, ce n'est pas seulement écrire du logiciel libre. C'est aussi coder les règles d'une monnaie libre — une monnaie dont le « code source » est ouvert, compréhensible et juste.
Cette convergence entre logiciel libre et monnaie libre n'est pas un hasard. C'est le même mouvement de fond : la conviction que les infrastructures essentielles de la société — le code informatique, le code monétaire — doivent être des **communs**, gérés démocratiquement, au bénéfice de tous.

View File

@@ -1,73 +0,0 @@
---
title: "Autres greffes"
description: "Applications concrètes de l'économie du don dans divers secteurs : emploi, ESS, agriculture, artisanat, éducation."
order: 9
readingTime: "15 min"
---
L'économie de greffe ne se limite pas aux marchés et aux circuits alimentaires. Elle peut se déployer dans de nombreux secteurs de la vie économique et sociale. Ce chapitre explore quelques pistes de greffes, certaines déjà amorcées, d'autres encore en gestation.
## Pôle Emploi et mission locale
Les demandeurs d'emploi sont parmi les premières victimes de l'asymétrie monétaire. Sans revenu en euro, ils sont exclus de l'économie — alors même qu'ils ont des compétences, du temps et de l'énergie à offrir.
La monnaie libre offre une issue à cette impasse. Un demandeur d'emploi peut produire et échanger en June, développer ses compétences, entretenir son réseau, et maintenir une activité économique réelle — même en l'absence de « travail » au sens classique.
Les Pôles Emploi et les missions locales pourraient jouer un rôle de relais en orientant les demandeurs d'emploi vers les communautés June locales. Non pas comme un substitut à l'emploi salarié, mais comme un **complément** qui maintient le lien social et économique pendant les périodes de transition.
Certaines expériences locales vont dans ce sens : des ateliers de présentation de la monnaie libre organisés en partenariat avec des structures d'insertion, des Ğmarchés accueillant des personnes en réinsertion professionnelle.
## ESS (Économie Sociale et Solidaire)
L'Économie Sociale et Solidaire partage de nombreuses valeurs avec la monnaie libre : solidarité, gouvernance démocratique, primauté de l'humain sur le capital, ancrage territorial.
Les structures de l'ESS — coopératives, mutuelles, associations, fondations — sont des partenaires naturels pour le développement de l'économie en monnaie libre. Elles disposent de réseaux, de compétences juridiques, d'une légitimité institutionnelle.
Les greffes possibles sont nombreuses :
- Des **AMAP** (Associations pour le Maintien d'une Agriculture Paysanne) qui acceptent la June
- Des **ressourceries** et **recycleries** qui pratiquent le double pricing
- Des **coopératives d'activité** qui accompagnent des entrepreneurs en monnaie libre
- Des **tiers-lieux** qui accueillent des Ğmarchés et des ateliers
Le dialogue avec l'ESS est aussi l'occasion de faire connaître la monnaie libre à un public plus large, et de montrer qu'elle n'est pas un gadget technologique mais un outil de transformation sociale.
## Associations populaires et caritatives
Les associations caritatives — Restos du Cœur, Secours Populaire, Emmaüs, etc. — distribuent des biens aux plus démunis. Leur action est indispensable, mais elle maintient une logique d'**assistance** : les bénéficiaires reçoivent, mais ne participent pas en tant qu'acteurs économiques.
La monnaie libre propose un changement de paradigme. Plutôt que de distribuer des biens, on peut distribuer du **pouvoir d'achat** en June. Le bénéficiaire n'est plus un assisté passif : il devient un **acteur économique** qui choisit librement ce qu'il achète, à qui, et à quel moment.
Ce passage de l'assistance à l'**autonomie** est fondamental. Il restaure la dignité des personnes en situation de précarité. Il les intègre dans un réseau d'échange où elles sont traitées comme des égales — pas comme des bénéficiaires de charité.
Des expériences pilotes associant monnaie libre et action caritative pourraient ouvrir des perspectives considérables.
## Productions agricoles, maraîchages
L'agriculture est le secteur le plus naturellement adapté à la monnaie libre. Les maraîchers produisent des biens essentiels, en circuit court, à une échelle compatible avec les communautés locales.
De nombreux maraîchers acceptent déjà la June, en tout ou en partie. Certains vont plus loin : ils achètent des semences, du matériel, des services en June. Ils créent ainsi des mini-filières en monnaie libre, depuis la semence jusqu'à l'assiette.
Le défi pour l'agriculture en monnaie libre est celui de la **viabilité économique**. Un maraîcher doit payer ses charges en euro (foncier, matériel, assurances, cotisations). Tant que ces charges ne sont pas couvertes en June, la part de l'activité en monnaie libre reste limitée.
La solution passe par la densification du réseau : plus il y a de producteurs et de prestataires qui acceptent la June, plus chacun peut couvrir ses besoins en monnaie libre, et moins il dépend de l'euro.
## Artisanat — Commerce — Entreprise
L'artisanat offre un terrain fertile pour la monnaie libre. Les artisans travaillent souvent en solo ou en petite équipe, ils sont proches de leurs clients, et leur production est locale par nature.
Menuisiers, couturiers, réparateurs, électriciens, plombiers, boulangers... autant de métiers qui peuvent intégrer la June dans leur activité. Le modèle le plus courant est le **double pricing** : une partie en euro (pour couvrir les charges incompressibles) et une partie en June.
Pour les commerces et les entreprises de taille plus importante, l'intégration de la June demande une réflexion comptable et organisationnelle plus poussée. Mais les exemples existent : des boutiques qui acceptent la June, des prestataires de services informatiques qui facturent en DU.
## Lycées — Écoles
L'éducation est un terrain d'expérimentation passionnant pour la monnaie libre. Apprendre aux jeunes comment fonctionne la monnaie — pas seulement comment la gagner et la dépenser, mais comment elle est créée, par qui, selon quelles règles — est un enjeu civique majeur.
Des initiatives existent : des ateliers sur la monnaie libre dans des lycées, des projets pédagogiques autour de la Ğ1, des simulations d'économie en monnaie libre avec des classes.
L'intérêt pédagogique est triple :
- **Économique** : comprendre la création monétaire, l'inflation, les systèmes monétaires
- **Mathématique** : manipuler les notions de croissance, de convergence, de symétrie
- **Civique** : réfléchir à la gouvernance des communs, à la démocratie économique
Les jeunes qui découvrent la monnaie libre réagissent souvent avec enthousiasme. L'idée qu'une autre monnaie est possible — et qu'elle existe déjà — ouvre des horizons que l'enseignement classique de l'économie tend à fermer.

View File

@@ -1,81 +0,0 @@
---
title: "Créer une économie ?"
description: "Passer de la théorie à la pratique : produire, greffer une économie du don sur le tissu local, inverser les flux."
order: 6
readingTime: "25 min"
---
Après la théorie, la pratique. Après avoir compris *pourquoi* une autre monnaie est nécessaire et *comment* elle fonctionne, la question qui brûle est : **comment construire concrètement une économie du don ?**
La réponse n'est pas de table rase. On ne détruit pas l'économie existante pour en construire une autre à la place. On **greffe**. On crée des passerelles. On développe des circuits parallèles qui, petit à petit, deviennent des alternatives crédibles.
## Produire
Toute économie commence par la **production**. Pas de production, pas d'échange. Pas d'échange, pas d'économie. La question première est donc : que produire en monnaie libre ?
La réponse est : tout ce dont la communauté a besoin. Des légumes, du pain, des vêtements, des réparations, des cours, des soins, des spectacles, des logiciels, des hébergements... La production en monnaie libre n'est pas cantonnée à un secteur : elle couvre potentiellement tous les besoins humains.
En pratique, les premiers producteurs en monnaie libre sont souvent des **artisans et des maraîchers** — des gens qui produisent à petite échelle, en circuit court, et qui sont proches de leur communauté. Mais on trouve aussi des informaticiens, des thérapeutes, des enseignants, des artistes.
Le point commun de ces producteurs, c'est qu'ils acceptent d'être rémunérés (en partie ou en totalité) en June. Ils font confiance à la communauté pour que cette monnaie ait de la valeur — c'est-à-dire pour que d'autres producteurs acceptent aussi la June, et qu'on puisse l'échanger contre des biens et services utiles.
## « Passer la seconde »
L'expression « passer la seconde » décrit le moment où une communauté monnaie-libre passe du stade de l'expérimentation au stade de l'**économie réelle**. C'est le moment où la June cesse d'être un jeu ou une curiosité pour devenir un outil économique fonctionnel.
Ce passage se caractérise par plusieurs marqueurs :
- Des producteurs **réguliers** (pas seulement occasionnels) acceptent la June
- Des **circuits d'échange** stables se forment entre producteurs et consommateurs
- La June commence à circuler « en boucle » : A paie B en June, B paie C, C paie A
- Les membres commencent à couvrir une partie significative de leurs besoins en June
« Passer la seconde » n'est pas un événement ponctuel. C'est un processus graduel, qui nécessite patience, persévérance et organisation collective.
## Économie de greffe
Le concept d'**économie de greffe** est central dans notre approche. Plutôt que de construire une économie alternative isolée, nous proposons de *greffer* l'économie en monnaie libre sur l'économie existante.
Concrètement, cela signifie que la plupart des producteurs en June acceptent aussi l'euro. Ils pratiquent un **double pricing** : un prix en euro et un prix en June (exprimé en DU). Le client choisit son moyen de paiement.
Cette approche présente plusieurs avantages :
- Elle ne demande pas aux producteurs de renoncer à l'euro du jour au lendemain
- Elle permet une transition progressive
- Elle expose de nouveaux publics à la monnaie libre
- Elle crée des ponts entre les deux économies
La greffe n'est pas un compromis ou une demi-mesure. C'est une **stratégie** de transition. L'objectif à long terme est que la part de l'économie en monnaie libre croisse naturellement, à mesure que la communauté grandit et que les circuits d'échange se multiplient.
## Connaître son bassin de vie
Pour greffer efficacement, il faut connaître son **bassin de vie** : les personnes, les activités, les ressources, les besoins du territoire. C'est un travail d'enquête et de cartographie qui peut sembler fastidieux, mais qui est indispensable.
Quels producteurs locaux pourraient accepter la June ? Quels services manquent ? Quels besoins ne sont pas satisfaits par l'économie classique ? Quelles compétences sont disponibles ? Où sont les forces vives ?
Cette connaissance du terrain permet de cibler les efforts : développer les circuits où la demande est forte, attirer les producteurs dont l'offre correspond aux besoins, organiser des événements qui rassemblent la communauté.
Les Groupes Locaux June (GLJ) jouent un rôle essentiel dans cette cartographie. Ce sont des collectifs informels de monnaie-libristes qui se réunissent régulièrement sur un territoire donné. Ils organisent des marchés, des rencontres, des ateliers. Ils sont les chevilles ouvrières de l'économie de greffe.
## Gestion « à l'anglaise »
La « gestion à l'anglaise » est une métaphore jardinière. En jardinage anglais, on ne cherche pas à tout contrôler comme dans un jardin à la française. On plante, on observe, on accompagne la croissance, on taille quand c'est nécessaire — mais on laisse la nature faire son travail.
L'économie du don fonctionne de la même manière. On ne peut pas la planifier de manière centralisée. On ne peut pas décider d'en haut qui doit produire quoi, qui doit échanger avec qui, à quel prix. Ce serait contradictoire avec l'esprit même de la liberté.
En revanche, on peut **créer les conditions** favorables :
- Organiser des marchés où les producteurs se rencontrent
- Faciliter la certification de nouveaux membres
- Animer la communauté (forums, événements, communication)
- Documenter et partager les bonnes pratiques
- Résoudre les problèmes techniques (outils informatiques, applications)
Le rôle des organisateurs n'est pas de diriger, mais de **faciliter**. Ils créent le cadre, et la communauté remplit le cadre selon ses propres dynamiques.
## Économie de flux — inversés
Dans l'économie classique, les flux vont du bas vers le haut : l'argent remonte des consommateurs vers les producteurs, puis vers les actionnaires, puis vers les marchés financiers. C'est une économie d'**extraction** : la valeur est extraite des territoires et concentrée dans les centres de pouvoir financier.
L'économie du don propose d'**inverser les flux**. La monnaie naît en bas — chez les individus, par le Dividende Universel — et circule horizontalement entre pairs. Il n'y a pas de centre d'accumulation. La richesse reste dans le territoire, circule entre les membres, fertilise l'économie locale.
Cette inversion des flux n'est pas une redistribution. La redistribution suppose qu'on prend aux riches pour donner aux pauvres — ce qui maintient la logique d'accumulation, simplement corrigée a posteriori. L'inversion des flux change la **source** même de la monnaie. On ne corrige pas les inégalités : on supprime le mécanisme qui les crée.
> Inverser les flux, ce n'est pas redistribuer la richesse. C'est changer l'endroit où la richesse naît.

View File

@@ -1,53 +0,0 @@
---
title: "De quel don parlons-nous ?"
description: "Exploration philosophique et sociologique du don, des asymétries communautaires aux expériences concrètes."
order: 2
readingTime: "20 min"
---
Le mot « don » est piégé. Il charrie des siècles de connotations religieuses, morales, sentimentales. Il évoque la charité, le sacrifice, la générosité — autant de notions nobles mais qui obscurcissent ce dont nous voulons parler. Alors, de quel don parlons-nous ?
Nous parlons d'un don **économique**. Un don qui circule, qui se mesure, qui s'organise. Pas un don qui s'oppose à l'économie, mais un don qui *est* l'économie — ou du moins qui pourrait en devenir le principe organisateur.
Pour comprendre cette proposition, il faut d'abord déconstruire quelques idées reçues. C'est l'objet de ce chapitre.
## Trois mots de philo et de socio
Trois penseurs sont incontournables quand on parle du don : **Marcel Mauss**, **Jacques Derrida** et **Alain Caillé**.
Marcel Mauss, dans son *Essai sur le don* (1925), a montré que dans les sociétés dites « archaïques », le don n'est jamais gratuit. Il obéit à une triple obligation : **donner, recevoir, rendre**. Le don crée du lien social. Il engage des relations de réciprocité. Il structure la communauté. Le don maussien n'est pas un acte isolé de générosité : c'est un système social complet.
Jacques Derrida, quant à lui, a posé une question vertigineuse : le don est-il seulement possible ? Pour qu'il y ait don véritable, il faudrait que le donateur n'attende rien en retour — pas même de la reconnaissance. Dès qu'on identifie un don comme tel, il cesse d'être un don pour devenir un échange. Le don pur serait donc impossible, ou du moins impensable. Cette aporie derridienne n'est pas un obstacle pour nous : elle est une boussole. Elle nous rappelle que le don n'est jamais simple, jamais acquis, jamais achevé.
Alain Caillé et le mouvement du MAUSS (Mouvement Anti-Utilitariste en Sciences Sociales) proposent une troisième voie. Pour eux, le don est un **paradigme** — une manière de penser les relations humaines qui ne se réduit ni à l'intérêt (utilitarisme) ni au devoir (moralisme). Le don est un acte libre, mais pas arbitraire. Il est conditionnel, mais pas calculé. Il crée de l'obligation, mais pas de la dette.
Ces trois perspectives éclairent notre propos :
- Avec Mauss, nous comprenons que le don est **structurant** : il crée de la société.
- Avec Derrida, nous comprenons que le don est **exigeant** : il résiste à la réduction.
- Avec Caillé, nous comprenons que le don est **possible** : il constitue un paradigme viable.
## Asymétries et Communautés
L'un des problèmes fondamentaux de toute économie est la question de l'**asymétrie**. Dans notre système actuel, les asymétries sont partout : asymétrie de pouvoir entre employeur et employé, asymétrie d'information entre producteur et consommateur, asymétrie de création monétaire entre banques et citoyens.
Ces asymétries ne sont pas des accidents. Elles sont **constitutives** du système. Le capitalisme ne fonctionne pas *malgré* les asymétries — il fonctionne *par* les asymétries. Le profit naît de la différence. L'accumulation naît de l'inégalité.
Dans une économie du don, les asymétries ne disparaissent pas — ce serait naïf de le prétendre. Mais elles changent de nature. L'asymétrie n'est plus un levier d'extraction, mais un moteur de circulation. Celui qui a plus donne plus. Celui qui sait transmet. Celui qui peut aide. Non pas par obligation morale, mais parce que le système rend cette circulation **naturelle et avantageuse pour tous**.
La notion de **communauté** est centrale ici. Le don ne fonctionne qu'au sein d'un groupe qui se reconnaît comme tel. Ce n'est pas nécessairement une communauté géographique ou ethnique — c'est une communauté de **confiance**. Les membres se font suffisamment confiance pour donner sans garantie immédiate de retour. Cette confiance n'est pas aveugle : elle est construite, entretenue, vérifiée par la pratique.
Dans la communauté June, cette confiance se construit par la **toile de confiance** (Web of Trust) : chaque nouveau membre est certifié par des membres existants qui le connaissent personnellement. Ce mécanisme garantit que chaque compte correspond à un être humain réel et vivant. Pas de bots, pas de comptes fictifs, pas de manipulation.
## Le cas emblématique « eco si nuestra »
L'Espagne nous offre un exemple remarquable avec le réseau **« eco si nuestra »** (notre éco). Ce mouvement, né dans le sillage de la crise de 2008, a développé des réseaux d'échange basés sur des monnaies locales et complémentaires.
Le principe est simple : des communautés créent leur propre monnaie pour faciliter les échanges locaux. Quand l'euro se fait rare — parce que le chômage explose, parce que les banques ne prêtent plus — ces monnaies locales permettent aux gens de continuer à échanger, à produire, à vivre.
Ce qui est remarquable dans l'expérience espagnole, c'est la vitesse à laquelle ces initiatives se sont développées face à la crise. Quand le système officiel faillit, les gens inventent spontanément des alternatives. Le don et l'échange non-monétaire ne sont pas des curiosités anthropologiques : ce sont des reflexes de survie et de solidarité.
## L'expérience « made in zion »
Plus proche de nous, l'expérience « made in zion » illustre une autre facette du don organisé. Ici, c'est la dimension **culturelle et identitaire** du don qui est mise en avant. Le don devient un acte de résistance, une affirmation d'autonomie face aux circuits économiques dominants.
Ces expériences montrent que le don n'est pas une abstraction théorique. Il se pratique, il s'organise, il se vit — dans des contextes très divers, face à des défis très concrets. Ce qui manque, ce n'est pas la volonté ni l'imagination. C'est un **cadre théorique solide** et des **outils techniques adaptés** pour passer à l'échelle. C'est précisément ce que proposent la TRM et la monnaie libre.

View File

@@ -1,69 +0,0 @@
---
title: "Échanger"
description: "Organiser les échanges dans une économie du don : filières, boucles, distribution et marchés Ğ1."
order: 7
readingTime: "18 min"
---
Produire ne suffit pas. Encore faut-il que la production **circule** — qu'elle atteigne ceux qui en ont besoin, au bon moment, au bon endroit. C'est la question de l'échange, qui est au cœur de toute économie.
Dans l'économie classique, l'échange est organisé par le marché et médié par le prix. Offre et demande se rencontrent, le prix s'ajuste, les biens circulent. Ce mécanisme est efficace, mais il a un coût : il exclut ceux qui n'ont pas de monnaie, il uniformise les valeurs, il favorise les intermédiaires.
Dans l'économie du don, l'échange prend d'autres formes. Il est plus direct, plus personnel, plus ancré dans la relation.
## Filières et boucles
L'un des objectifs de l'économie de greffe est de créer des **filières** et des **boucles** d'échange en monnaie libre.
Une **filière** est une chaîne de production-distribution : le maraîcher produit des légumes, le boulanger achète des légumes au maraîcher et vend du pain, le client achète du pain et des légumes. Si toute la filière fonctionne en June, la monnaie circule en interne sans avoir besoin d'être convertie en euro.
Une **boucle** va plus loin : c'est un circuit fermé où la monnaie revient à son point de départ. A paie B, B paie C, C paie A. La boucle est le Graal de l'économie locale : elle garantit que la monnaie reste dans le territoire et qu'elle profite à tous les participants.
Construire des filières et des boucles demande de la coordination. Il faut identifier les maillons manquants (quels producteurs manquent pour boucler la boucle ?) et les inciter à rejoindre l'aventure. C'est un travail de **tissage économique**, patient mais gratifiant.
En pratique, les premières boucles qui se forment sont souvent alimentaires : maraîcher → marché → consommateur → maraîcher. L'alimentation est le besoin le plus universel et le plus fréquent, ce qui en fait le meilleur point de départ pour construire des circuits en monnaie libre.
## Distribuer
La question de la **distribution** est souvent négligée dans les projets de monnaie alternative. On se concentre sur la production et l'échange, mais on oublie la logistique : comment acheminer les produits du producteur au consommateur ?
Dans l'économie classique, la distribution est assurée par un réseau dense et efficace de supermarchés, de plateformes de livraison, de grossistes. Concurrencer ce réseau est illusoire. Mais le compléter — offrir des alternatives pour ceux qui veulent consommer autrement — est tout à fait possible.
Les formes de distribution en monnaie libre sont variées :
- **Marchés physiques** : les marchés June sont des événements réguliers où producteurs et consommateurs se retrouvent
- **Vente directe** : de la ferme à l'assiette, sans intermédiaire
- **Groupements d'achat** : des consommateurs se regroupent pour commander ensemble
- **Plateformes en ligne** : des annonces de produits et services en June
Chaque forme a ses avantages et ses limites. Le marché physique crée du lien social mais demande de l'organisation. La vente directe est simple mais géographiquement limitée. La plateforme en ligne est accessible mais impersonnelle.
## Connecter avec l'existant
L'économie du don ne vit pas en vase clos. Elle coexiste avec l'économie classique, et elle doit **s'y connecter** intelligemment.
La connexion prend plusieurs formes :
- **Double pricing** : les producteurs affichent un prix en euro et un prix en DU
- **Paiement mixte** : le client paie une partie en June et une partie en euro
- **Passerelles comptables** : les entreprises qui acceptent la June tiennent une comptabilité qui intègre les deux monnaies
Cette connexion est pragmatique. Elle reconnaît que, pour l'instant, personne ne peut vivre à 100% en monnaie libre. Les charges fixes (loyer, impôts, assurances) se paient en euro. Le lait, le carburant, l'électricité aussi — du moins tant que ces filières ne sont pas développées en June.
Mais chaque nouveau producteur qui accepte la June, chaque nouvelle filière qui se crée, réduit la dépendance à l'euro. C'est une transition, pas une révolution. Et les transitions réussies sont celles qui avancent pas à pas, sans brûler les ponts.
## Ğ(marchés)
Les **Ğmarchés** (prononcer « Junmarchés ») sont les marchés physiques en monnaie libre. Ce sont des lieux de rencontre, d'échange et de convivialité où producteurs et consommateurs se retrouvent régulièrement.
Un Ğmarché typique rassemble une dizaine à une trentaine de stands : légumes, fruits, pain, miel, savons, vêtements, artisanat, services de massage, couture, informatique... Les prix sont affichés en DU. Les paiements se font via l'application Ğ1 sur smartphone ou par un système de bons papier pour les moins connectés.
Les Ğmarchés ne sont pas seulement des lieux d'échange économique. Ce sont des lieux de **vie communautaire**. On y vient aussi pour discuter, partager un repas, apprendre, s'entraider. L'aspect social est au moins aussi important que l'aspect économique.
La fréquence des Ğmarchés varie selon les territoires : hebdomadaire dans les zones les plus actives, mensuel ailleurs. Certains sont itinérants, d'autres ont un lieu fixe. Certains sont couplés à des événements culturels (concerts, conférences, ateliers).
Le succès d'un Ğmarché dépend de plusieurs facteurs :
- La **diversité de l'offre** : plus les stands sont variés, plus les visiteurs trouvent ce qu'ils cherchent
- La **régularité** : un marché prévisible fidélise les clients
- L'**ambiance** : un marché convivial attire et retient
- La **communication** : faire connaître le marché au-delà de la communauté existante
Les Ğmarchés sont la vitrine de l'économie du don. Ils montrent concrètement que cette économie fonctionne, qu'elle produit de la valeur, qu'elle crée du lien. Pour beaucoup de nouveaux venus, le premier Ğmarché est le déclic qui les convainc de rejoindre l'aventure.

View File

@@ -1,51 +0,0 @@
---
title: "Et maintenant ?… action ?"
description: "L'appel à l'action : comment rejoindre le mouvement, participer et construire ensemble une économie du don."
order: 10
readingTime: "8 min"
---
Vous avez lu ce livre — ou du moins une partie. Vous avez écouté les chansons — ou du moins certaines. Et maintenant ?
La question n'est pas rhétorique. Ce livre n'est pas un traité académique destiné à rester sur une étagère. C'est un **appel à l'action**. Un appel à rejoindre, à construire, à expérimenter.
## Par où commencer ?
Si vous êtes convaincu — ou même simplement curieux — voici quelques pistes concrètes :
**1. Créer son compte Ğ1.** C'est la première étape. Rejoindre la toile de confiance, recevoir sa certification, et commencer à co-créer de la monnaie. Le processus prend quelques semaines (le temps de rencontrer des membres existants et d'être certifié), mais il est gratuit et ouvert à tous.
**2. Rejoindre un Groupe Local June (GLJ).** Les GLJ sont les cellules vivantes de la communauté. Ils organisent des rencontres, des marchés, des ateliers. C'est le meilleur endroit pour rencontrer des monnaie-libristes, poser des questions, et découvrir l'économie en June de l'intérieur.
**3. Participer à un Ğmarché.** Même sans rien à vendre, allez voir un Ğmarché. Observez comment ça fonctionne. Goûtez un pain fait maison payé en DU. Discutez avec les producteurs. Sentez l'ambiance.
**4. Proposer un bien ou un service.** Vous savez faire quelque chose ? Proposez-le en June. Cours de guitare, réparation d'ordinateur, traduction, baby-sitting, confection de confitures... Toute compétence a de la valeur.
**5. Parler autour de vous.** Le bouche-à-oreille est le premier vecteur de développement de la communauté. Parlez de la monnaie libre à vos proches, à vos collègues, à vos voisins. Prêtez ce livre. Partagez les chansons.
## Les RML et événements
Les **Rencontres de la Monnaie Libre** (RML) sont des événements régionaux et nationaux qui rassemblent la communauté. Pendant plusieurs jours, les participants échangent, débattent, présentent leurs projets, et font vivre l'économie en June à grande échelle.
Les RML sont des moments forts de la vie communautaire. On y fait des rencontres, on y noue des liens, on y prend de l'énergie. C'est souvent lors d'une RML qu'on passe du statut de « curieux » à celui de « monnaie-libriste convaincu ».
D'autres événements ponctuent l'année : les Journées Inter-Nodales (JIN), les ateliers techniques, les formations, les fêtes locales avec Ğmarché intégré.
## Questions ouvertes
Ce livre ne prétend pas avoir toutes les réponses. Beaucoup de questions restent ouvertes :
- **L'échelle** : la monnaie libre peut-elle fonctionner à l'échelle d'un pays, d'un continent ? Ou est-elle condamnée à rester locale ?
- **La transition** : comment articuler la coexistence euro/June sur le long terme ? L'objectif est-il de remplacer l'euro ou de le compléter ?
- **La gouvernance** : comment prendre des décisions collectives dans une communauté décentralisée ? Comment éviter les dérives ?
- **La technologie** : les outils actuels (blockchain Duniter, applications Cesium/Tikka) sont-ils suffisamment robustes et accessibles ?
Ces questions ne sont pas des obstacles. Ce sont des **chantiers** — des invitations à la réflexion et à l'expérimentation collectives.
## Ce que nous pouvons ensemble
> Une économie du don — enfin concevable.
Le titre de ce livre est une promesse. Pas une promesse de résultat, mais une promesse de **possibilité**. Les outils existent. La théorie est solide. Les expériences sont encourageantes. Ce qui manque, c'est le nombre. Plus nous serons nombreux à expérimenter, à produire, à échanger, à imaginer — plus l'économie du don deviendra non seulement concevable, mais réelle.
Ce livre est un don. Les chansons sont un don. Faites-en ce que vous voulez. Partagez-les, discutez-les, critiquez-les, prolongez-les. C'est ainsi que le don circule.

View File

@@ -1,36 +0,0 @@
---
title: "Introduction"
description: "Un livre et des chansons pour rendre concevable une économie du don, portée par la communauté monnaie-libriste."
order: 1
readingTime: "15 min"
---
Ce livre est une façon de raconter ce que nous vivons, ce que nous expérimentons, et ce que nous pensons être possible. Il ne prétend pas être un manuel d'économie, encore moins un traité de philosophie. C'est un récit collectif, un témoignage d'expérience, un partage de convictions.
Nous sommes un petit groupe de personnes qui, depuis plusieurs années, explorons une idée simple mais radicale : **une économie fondée sur le don est non seulement souhaitable, mais concevable**. Et nous pensons que les outils pour la construire existent déjà.
Ce livre est accompagné de neuf chansons. Elles ne sont pas là pour décorer. Elles racontent le livre autrement, par la musique et la poésie. Chaque chanson éclaire un aspect du propos, ouvre une porte émotionnelle là où le texte reste analytique. Elles sont aussi un don — librement accessibles, librement partageables.
## Le projet
L'économie du don n'est pas une utopie lointaine. C'est une pratique quotidienne que nous connaissons tous : le repas préparé pour sa famille, le coup de main donné au voisin, le logiciel libre partagé sur Internet, le savoir transmis à un élève. Ce qui est nouveau, ce n'est pas le don. C'est l'idée qu'on puisse en faire le **fondement** d'un système économique entier.
Pour cela, il faut répondre à des questions difficiles. Comment mesurer sans dénaturer ? Comment échanger sans exploiter ? Comment produire sans détruire ? Comment organiser sans dominer ?
Ce livre propose des pistes. Il s'appuie sur des travaux théoriques rigoureux — notamment la Théorie Relative de la Monnaie (TRM) de Stéphane Laborde — et sur des expériences concrètes menées par des communautés en France et ailleurs.
## Monnaie-libristes
Nous nous appelons « monnaie-libristes ». Ce néologisme désigne les personnes qui utilisent, promeuvent ou contribuent au développement de monnaies libres — c'est-à-dire de monnaies dont la création est symétrique entre tous les membres, présents et futurs.
La monnaie libre la plus aboutie aujourd'hui est la **Ğ1** (prononcer « June »). Créée en 2017, elle est fondée sur les principes de la TRM. Chaque membre co-crée la même quantité de monnaie, chaque jour, par un Dividende Universel (DU). Personne ne contrôle la création monétaire. Personne n'en est exclu.
Les monnaie-libristes forment une communauté diverse : informaticiens, agriculteurs, artisans, enseignants, artistes, retraités. Ce qui les rassemble, c'est la conviction qu'une monnaie juste est le socle d'une économie juste. Et qu'une économie juste rend le don non seulement possible, mais naturel.
La communauté June compte aujourd'hui plusieurs milliers de membres en France et dans le monde francophone. Des marchés en June se tiennent régulièrement. Des producteurs vendent en June. Des services s'échangent en June. Une économie réelle, encore modeste mais vivante, se construit jour après jour.
Ce livre raconte cette aventure. Il tente d'en expliquer les fondements théoriques, d'en montrer les réalisations pratiques, et d'en dessiner les perspectives. Ce n'est pas un livre de certitudes. C'est un livre de convictions partagées, ouvert à la discussion et à la critique.
> « Une économie du don — enfin concevable » : le titre de ce livre est à la fois un constat et un programme. Le constat que les outils existent. Le programme qu'il reste à les déployer.
Bonne lecture. Et si le cœur vous en dit, bonne écoute.

Some files were not shown because too many files have changed in this diff Show More