Compare commits
21 Commits
develop
...
088333e4d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
088333e4d4 | ||
|
|
07449de187 | ||
|
|
9d92c4a5b3 | ||
|
|
8f548afb17 | ||
|
|
9caf11c8ab | ||
|
|
c564e7be5f | ||
|
|
3a5c40a886 | ||
|
|
fbc2867163 | ||
|
|
082a17d09b | ||
|
|
97ba6dd04c | ||
|
|
f0338cca5e | ||
|
|
c6b9abf2f3 | ||
|
|
df0409fec3 | ||
|
|
1af00cc64c | ||
|
|
8a38c86794 | ||
|
|
17f39e735d | ||
|
|
7ea19e2247 | ||
|
|
9525ed3953 | ||
|
|
b02368a15b | ||
| 1f47533c77 | |||
| 07bf07a942 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -18,6 +18,10 @@ 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 originales (PDF, JPG — pas servies par l'appli)
|
||||||
sources/
|
sources/
|
||||||
|
|
||||||
|
|||||||
60
CLAUDE.md
60
CLAUDE.md
@@ -14,26 +14,36 @@ Site vitrine du projet Le Librodrome — livre + chansons sur l'économie du don
|
|||||||
|
|
||||||
```
|
```
|
||||||
app/
|
app/
|
||||||
pages/ # Routes : index, lire/, ecouter/, autonomie, evenement, gratewizard, a-propos, admin/
|
pages/
|
||||||
components/ # admin, book, content, home, layout, player, song, ui
|
numerique/ # Autonomie numérique (index + [slug] détail)
|
||||||
composables/ # useAudioPlayer, useBookData, useGrateWizard, usePlaylist, usePageContent, useSiteContent...
|
economique/ # Autonomie économique (index, monnaie-libre, commande, productions-collectives)
|
||||||
assets/css/ # main.css (UnoCSS)
|
modele-eco/ # Livre : sommaire + chapitres [slug]
|
||||||
data/
|
citoyenne/ # Autonomie citoyenne (index + [slug] détail)
|
||||||
pages/ # Contenu YAML par page (home, lire, ecouter, autonomie, evenement...)
|
en-musique/ # Player audio
|
||||||
site.yml # Config globale du site
|
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/
|
server/
|
||||||
api/ # Endpoints API (admin, health)
|
api/content/pages/[...path].get.ts # GET pages YAML (chemins imbriqués)
|
||||||
middleware/ # Auth middleware
|
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/
|
docker/
|
||||||
Dockerfile # Build multi-stage (dev + prod)
|
Dockerfile, docker-compose.yml, docker-compose.dev.yml
|
||||||
docker-compose.yml # Production (Traefik)
|
|
||||||
docker-compose.dev.yml # Dev Docker
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ports dev (CRITIQUE)
|
## Ports dev (CRITIQUE)
|
||||||
|
|
||||||
Chaque projet a un port fixe pour éviter les conflits Nuxt auto-increment :
|
|
||||||
|
|
||||||
| Projet | Port | Config |
|
| Projet | Port | Config |
|
||||||
|--------|------|--------|
|
|--------|------|--------|
|
||||||
| **librodrome** | **3000** | `nuxt.config.ts` → `devServer.port: 3000` |
|
| **librodrome** | **3000** | `nuxt.config.ts` → `devServer.port: 3000` |
|
||||||
@@ -41,32 +51,32 @@ Chaque projet a un port fixe pour éviter les conflits Nuxt auto-increment :
|
|||||||
| **SejeteralO frontend** | **3009** | `frontend/nuxt.config.ts` → `devServer.port: 3009` |
|
| **SejeteralO frontend** | **3009** | `frontend/nuxt.config.ts` → `devServer.port: 3009` |
|
||||||
| **SejeteralO backend** | **8000** | Makefile → `uvicorn --port 8000` |
|
| **SejeteralO backend** | **8000** | Makefile → `uvicorn --port 8000` |
|
||||||
|
|
||||||
Script de gestion : `/home/yvv/Documents/PROD/DEV/dev-ports.sh` (status/kill/clean/start)
|
**Ne jamais changer ces ports.**
|
||||||
|
|
||||||
**Ne jamais changer ces ports.** Si un port est occupé, tuer le process parasite plutôt que laisser Nuxt auto-incrémenter.
|
|
||||||
|
|
||||||
## Intégration GrateWizard
|
## Intégration GrateWizard
|
||||||
|
|
||||||
- URL dev configurée dans `app/app.config.ts` → `localhost:3001`
|
- URL dev : `app/app.config.ts` → `localhost:3001`
|
||||||
- URL prod : `https://gratewizard.ml`
|
- URL prod : `https://gratewizard.axiom-team.fr`
|
||||||
- Ouverture en popup via `composables/useGrateWizard.ts`
|
- Bloc GrateWizard dans la section économique de la home
|
||||||
- GrateWizard est un projet Next.js séparé (`/home/yvv/Documents/PROD/DEV/GrateWizard`)
|
|
||||||
|
|
||||||
## Contenu
|
## Contenu administrable
|
||||||
|
|
||||||
Le contenu des pages est dans `data/pages/*.yml` et chargé via `composables/usePageContent.ts`. Le contenu riche (articles) utilise Nuxt Content avec des fichiers Markdown.
|
- 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
|
## Commandes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Dev server sur :3000
|
pnpm dev # Dev server sur :3000
|
||||||
pnpm build # Build production
|
pnpm build # Build production
|
||||||
pnpm generate # Génération statique
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Langue du site : français
|
- Langue du site : français
|
||||||
- Commits en français, style concis
|
- Commits en français, style concis
|
||||||
- CSS via UnoCSS (utility-first), pas de framework CSS externe
|
- CSS via UnoCSS (utility-first) + variables CSS palettes
|
||||||
- Composants Vue SFC avec `<script setup lang="ts">`
|
- Composants Vue SFC avec `<script setup lang="ts">`
|
||||||
|
- Shadoks SVG inline thématiques sur chaque page (hidden mobile)
|
||||||
|
|||||||
101
CONTRIBUTING.md
Normal file
101
CONTRIBUTING.md
Normal 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
124
README.md
@@ -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.
|
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ export default defineAppConfig({
|
|||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
height: '4rem',
|
height: '4rem',
|
||||||
nav: [
|
|
||||||
{ label: 'Autonomie', to: '/autonomie' },
|
|
||||||
{ label: 'Modèle éco', to: '/modele-eco' },
|
|
||||||
{ label: 'En musique', to: '/en-musique' },
|
|
||||||
{ label: 'Évènement', to: '/evenement' },
|
|
||||||
{ label: 'À propos', to: '/a-propos' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
credits: '© 2026 Le Librodrome — Productions collectives',
|
credits: '© 2026 Le Librodrome — Productions collectives',
|
||||||
@@ -21,10 +14,16 @@ export default defineAppConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
gratewizard: {
|
gratewizard: {
|
||||||
url: import.meta.dev ? 'http://localhost:3001' : 'https://gratewizard.ml',
|
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',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
12
app/app.vue
12
app/app.vue
@@ -17,6 +17,18 @@
|
|||||||
const paletteStore = usePaletteStore()
|
const paletteStore = usePaletteStore()
|
||||||
onMounted(() => paletteStore.applyToDOM())
|
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) => {
|
||||||
return title ? `${title} — Le Librodrome` : 'Le librodrome'
|
return title ? `${title} — Le Librodrome` : 'Le librodrome'
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
/* This file provides fallback and utility classes */
|
/* This file provides fallback and utility classes */
|
||||||
|
|
||||||
.font-display {
|
.font-display {
|
||||||
font-family: 'Outfit', system-ui, sans-serif;
|
font-family: 'Syne', system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-sans {
|
.font-sans {
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-mono {
|
.font-mono {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-primary: 18 80% 45%;
|
--color-primary: 18 80% 45%;
|
||||||
--color-accent: 32 85% 50%;
|
--color-accent: 32 85% 50%;
|
||||||
--color-bg: 20 10% 7%;
|
--color-bg: 215 8% 22%;
|
||||||
--color-surface: 20 10% 12%;
|
--color-surface: 213 7% 27%;
|
||||||
--color-surface-light: 20 8% 17%;
|
--color-surface-light: 210 6% 32%;
|
||||||
--color-text: 0 0% 100%;
|
--color-text: 0 0% 100%;
|
||||||
--color-text-muted: 0 0% 65%;
|
--color-text-muted: 0 0% 65%;
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
--player-height: 0rem;
|
--player-height: 0rem;
|
||||||
--sidebar-width: 280px;
|
--sidebar-width: 280px;
|
||||||
|
|
||||||
--font-display: 'Outfit', sans-serif;
|
--font-display: 'Syne', sans-serif;
|
||||||
--font-sans: 'Inter', sans-serif;
|
--font-sans: 'Space Grotesk', sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', monospace;
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
|
||||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
@@ -104,6 +104,7 @@ a {
|
|||||||
.palette-light .text-white\/45 { color: hsl(var(--color-text) / 0.52) !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\/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\/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\/70 { color: hsl(var(--color-text) / 0.78) !important; }
|
||||||
.palette-light .text-white\/80 { color: hsl(var(--color-text) / 0.88) !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\/85 { color: hsl(var(--color-text) / 0.92) !important; }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const renderedHtml = computed(() => {
|
|||||||
|
|
||||||
<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;
|
||||||
}
|
}
|
||||||
@@ -107,8 +107,8 @@ const renderedHtml = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background: hsl(20 8% 6%);
|
background: hsl(var(--color-bg));
|
||||||
border-bottom: 1px solid hsl(20 8% 14%);
|
border-bottom: 1px solid hsl(var(--color-surface-light));
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-tabs {
|
.md-tabs {
|
||||||
@@ -122,24 +122,24 @@ const renderedHtml = computed(() => {
|
|||||||
padding: 0.5rem 0.875rem;
|
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 {
|
.md-fullscreen {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
color: hsl(20 8% 40%);
|
color: hsl(var(--color-text-muted));
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
.md-fullscreen:hover,
|
.md-fullscreen:hover,
|
||||||
.md-fullscreen--active { color: white; }
|
.md-fullscreen--active { color: hsl(var(--color-text)); }
|
||||||
|
|
||||||
.md-body {
|
.md-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -151,14 +151,14 @@ const renderedHtml = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.md-body--split .md-preview {
|
.md-body--split .md-preview {
|
||||||
border-left: 1px solid hsl(20 8% 14%);
|
border-left: 1px solid hsl(var(--color-surface-light));
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-body--fullscreen {
|
.md-body--fullscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background: hsl(20 8% 4%);
|
background: hsl(var(--color-bg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-body--fullscreen .md-textarea,
|
.md-body--fullscreen .md-textarea,
|
||||||
@@ -170,8 +170,8 @@ const renderedHtml = computed(() => {
|
|||||||
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;
|
||||||
@@ -189,6 +189,6 @@ const renderedHtml = computed(() => {
|
|||||||
min-height: 24rem;
|
min-height: 24rem;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: hsl(20 8% 4%);
|
background: hsl(var(--color-bg));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -23,13 +23,25 @@
|
|||||||
</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/modele-eco" 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" />
|
||||||
Modèle éco
|
Numérique
|
||||||
|
</NuxtLink>
|
||||||
|
<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>
|
||||||
<NuxtLink to="/admin/pages/en-musique" class="sidebar-link" active-class="sidebar-link--active">
|
<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" />
|
||||||
@@ -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) {
|
||||||
|
|||||||
33
app/components/book/Actions.vue
Normal file
33
app/components/book/Actions.vue
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ 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;
|
||||||
@@ -538,7 +538,7 @@ onUnmounted(() => {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +582,7 @@ onUnmounted(() => {
|
|||||||
width: 2.25rem; height: 2.25rem;
|
width: 2.25rem; height: 2.25rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: hsl(20 8% 45%);
|
color: hsl(var(--color-text-muted));
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
@@ -617,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,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;
|
||||||
@@ -646,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;
|
||||||
@@ -658,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;
|
||||||
@@ -667,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 {
|
||||||
@@ -683,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 {
|
||||||
@@ -713,7 +713,7 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden auto;
|
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);
|
||||||
}
|
}
|
||||||
@@ -809,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;
|
||||||
}
|
}
|
||||||
@@ -833,7 +833,7 @@ onUnmounted(() => {
|
|||||||
width: 2.5rem; height: 2.5rem;
|
width: 2.5rem; height: 2.5rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: hsl(20 8% 45%);
|
color: hsl(var(--color-text-muted));
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
@@ -861,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 {
|
||||||
@@ -878,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;
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -59,4 +72,57 @@ function playSong(song: Song) {
|
|||||||
padding-bottom: 0.75rem;
|
padding-bottom: 0.75rem;
|
||||||
border-bottom: 2px solid hsl(var(--color-primary) / 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>
|
||||||
|
|||||||
@@ -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="`/modele-eco/${chapter.stem?.split('/').pop()}`"
|
: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"
|
||||||
>
|
>
|
||||||
|
|||||||
159
app/components/book/ChapterToc.vue
Normal file
159
app/components/book/ChapterToc.vue
Normal 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>
|
||||||
353
app/components/home/AxisBlock.vue
Normal file
353
app/components/home/AxisBlock.vue
Normal 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>
|
||||||
237
app/components/home/AxisGrid.vue
Normal file
237
app/components/home/AxisGrid.vue
Normal 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>
|
||||||
@@ -5,23 +5,6 @@
|
|||||||
<!-- Book cover -->
|
<!-- Book cover -->
|
||||||
<UiScrollReveal>
|
<UiScrollReveal>
|
||||||
<div class="book-cover-wrapper relative">
|
<div class="book-cover-wrapper relative">
|
||||||
<!-- Shadok pumper -->
|
|
||||||
<svg class="shadok-pumper" viewBox="0 0 200 240" fill="none" aria-hidden="true">
|
|
||||||
<ellipse cx="100" cy="130" rx="55" ry="65" fill="currentColor" opacity="0.9"/>
|
|
||||||
<ellipse cx="100" cy="60" rx="30" ry="28" fill="currentColor" opacity="0.85"/>
|
|
||||||
<circle cx="88" cy="54" r="6" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="112" cy="54" r="6" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="90" cy="53" r="2.5" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="114" cy="53" r="2.5" fill="currentColor" opacity="0.5"/>
|
|
||||||
<polygon points="100,68 115,78 85,78" fill="currentColor" opacity="0.6"/>
|
|
||||||
<line x1="80" y1="192" x2="70" y2="230" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
|
|
||||||
<line x1="120" y1="192" x2="130" y2="230" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
|
|
||||||
<line x1="70" y1="230" x2="55" y2="232" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
|
|
||||||
<line x1="130" y1="230" x2="145" y2="232" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.5"/>
|
|
||||||
<line x1="155" y1="110" x2="190" y2="90" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="190" y1="90" x2="190" y2="120" stroke="currentColor" stroke-width="4" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<rect x="180" y="118" width="18" height="40" rx="3" fill="currentColor" opacity="0.4"/>
|
|
||||||
</svg>
|
|
||||||
<div class="book-cover-3d">
|
<div class="book-cover-3d">
|
||||||
<img
|
<img
|
||||||
:src="content?.book.coverImage"
|
:src="content?.book.coverImage"
|
||||||
@@ -48,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>
|
||||||
@@ -66,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': []
|
||||||
@@ -111,25 +95,4 @@ 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-pumper {
|
|
||||||
position: absolute;
|
|
||||||
right: 3%;
|
|
||||||
bottom: 8%;
|
|
||||||
width: clamp(90px, 12vw, 180px);
|
|
||||||
opacity: 0.28;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-primary));
|
|
||||||
animation: shadok-float 10s ease-in-out infinite;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-float {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-10px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.shadok-pumper { display: none; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="section-padding">
|
|
||||||
<div class="container-content">
|
|
||||||
<UiScrollReveal>
|
|
||||||
<div class="gw-card relative overflow-hidden">
|
|
||||||
<!-- Shadok blob -->
|
|
||||||
<svg class="shadok-blob" viewBox="0 0 200 180" fill="none" aria-hidden="true">
|
|
||||||
<path d="M60 90 Q30 50 70 30 Q110 10 140 40 Q180 60 170 100 Q165 140 130 155 Q90 170 55 145 Q25 125 60 90Z" fill="currentColor" opacity="0.12"/>
|
|
||||||
<path d="M60 90 Q30 50 70 30 Q110 10 140 40 Q180 60 170 100 Q165 140 130 155 Q90 170 55 145 Q25 125 60 90Z" stroke="currentColor" stroke-width="1.5" opacity="0.2"/>
|
|
||||||
<circle cx="100" cy="80" r="8" fill="currentColor" opacity="0.08"/>
|
|
||||||
<circle cx="120" cy="110" r="6" fill="currentColor" opacity="0.06"/>
|
|
||||||
<circle cx="80" cy="105" r="5" fill="currentColor" opacity="0.07"/>
|
|
||||||
<circle cx="95" cy="72" r="3" fill="currentColor" opacity="0.3"/>
|
|
||||||
<circle cx="108" cy="70" r="3" fill="currentColor" opacity="0.3"/>
|
|
||||||
<circle cx="96" cy="71" r="1.2" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="109" cy="69" r="1.2" fill="currentColor" opacity="0.5"/>
|
|
||||||
</svg>
|
|
||||||
<div class="flex flex-col items-center text-center gap-4 md:flex-row md:text-left md:gap-8 relative z-1">
|
|
||||||
<!-- 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 :href="url" target="_blank" @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 { url, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadok-blob {
|
|
||||||
position: absolute;
|
|
||||||
right: -2%;
|
|
||||||
top: -20%;
|
|
||||||
width: clamp(120px, 16vw, 220px);
|
|
||||||
opacity: 0.35;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-accent));
|
|
||||||
animation: shadok-drift 12s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-drift {
|
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
|
||||||
50% { transform: translateY(-8px) rotate(3deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.shadok-blob { display: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<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 -->
|
<!-- Shadok bird decoration -->
|
||||||
<svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true">
|
<svg class="shadok-bird" viewBox="0 0 180 260" fill="none" aria-hidden="true">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<path d="M48 105 Q25 102 12 100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3" fill="none"/>
|
<path d="M48 105 Q25 102 12 100" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.3" fill="none"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Shadok boulanger: character with oven and bread -->
|
<!-- Shadok boulanger -->
|
||||||
<svg class="shadok-boulanger" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
<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"/>
|
<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"/>
|
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
|
||||||
@@ -44,41 +44,11 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@@ -86,11 +56,27 @@
|
|||||||
|
|
||||||
<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 {
|
.shadok-bird {
|
||||||
|
|||||||
@@ -1,51 +1,48 @@
|
|||||||
<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 text-white mb-4">Laisser un message</h3>
|
||||||
|
|
||||||
<form v-if="!submitted" class="space-y-3" @submit.prevent="send">
|
<form v-if="!submitted" class="space-y-3" @submit.prevent="send">
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
<input
|
||||||
<input
|
v-model="form.author"
|
||||||
v-model="form.author"
|
type="text"
|
||||||
type="text"
|
placeholder="Votre nom"
|
||||||
placeholder="Votre nom *"
|
class="msg-input"
|
||||||
required
|
/>
|
||||||
class="msg-input"
|
<p class="text-white/30 text-xs -mt-1 px-1">Pour recevoir une réponse, laissez votre e-mail dans le message.</p>
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-model="form.email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Email (optionnel)"
|
|
||||||
class="msg-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<textarea
|
<textarea
|
||||||
v-model="form.text"
|
v-model="form.text"
|
||||||
placeholder="Votre message *"
|
placeholder="Votre message"
|
||||||
required
|
|
||||||
rows="3"
|
rows="3"
|
||||||
class="msg-input resize-none"
|
class="msg-input resize-none"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button type="submit" class="btn-primary text-sm" :disabled="sending">
|
<button type="submit" class="btn-primary text-sm" :disabled="sending || !canSend">
|
||||||
<div v-if="sending" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" />
|
<div v-if="sending" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" />
|
||||||
Envoyer
|
Envoyer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div v-else class="text-center py-4">
|
<div v-else class="text-center py-6">
|
||||||
<div class="i-lucide-check-circle h-8 w-8 text-green-400 mx-auto mb-2" />
|
<div class="i-lucide-heart-handshake h-10 w-10 text-primary mx-auto mb-3" />
|
||||||
<p class="text-white/80">Merci pour votre message !</p>
|
<p class="text-white/90 font-display text-lg font-semibold">Merci pour votre message !</p>
|
||||||
<p class="text-white/40 text-sm mt-1">Il sera visible après modération.</p>
|
<p class="text-white/55 text-sm mt-2 max-w-md mx-auto leading-relaxed">
|
||||||
|
Il sera lu et traité dans un délai... humainement raisonnable.
|
||||||
|
</p>
|
||||||
|
<p class="text-primary/60 text-xs mt-4 italic">
|
||||||
|
Chaque message est un premier pas dans l'aventure.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UiScrollReveal>
|
</UiScrollReveal>
|
||||||
|
|
||||||
<!-- 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 text-white/80 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">
|
||||||
@@ -64,17 +61,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</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: '' })
|
||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
const submitted = ref(false)
|
const submitted = ref(false)
|
||||||
|
|
||||||
|
const canSend = computed(() => form.text.trim().length > 0)
|
||||||
|
|
||||||
async function send() {
|
async function send() {
|
||||||
|
if (!canSend.value) return
|
||||||
sending.value = true
|
sending.value = true
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/messages', { method: 'POST', body: form })
|
await $fetch('/api/messages', { method: 'POST', body: form })
|
||||||
|
|||||||
279
app/components/home/TypewriterText.vue
Normal file
279
app/components/home/TypewriterText.vue
Normal 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>
|
||||||
@@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
<!-- 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 :</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"
|
||||||
@@ -25,6 +27,19 @@
|
|||||||
>
|
>
|
||||||
{{ 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 />
|
<UiPaletteSelector />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -39,13 +54,17 @@
|
|||||||
</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>
|
<style scoped>
|
||||||
@@ -65,9 +84,34 @@ const isMobileMenuOpen = ref(false)
|
|||||||
|
|
||||||
.logo-text {
|
.logo-text {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-weight: 600;
|
font-weight: 400;
|
||||||
font-size: 1.15rem;
|
font-size: 1.25rem;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.04em;
|
||||||
color: hsl(var(--color-primary));
|
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>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -13,9 +13,11 @@ async function loadConfig(): Promise<BookConfig> {
|
|||||||
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[],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,14 @@ export function useBookData() {
|
|||||||
return link?.chapterSlug
|
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 {
|
||||||
@@ -98,5 +108,7 @@ export function useBookData() {
|
|||||||
getChapterForSong,
|
getChapterForSong,
|
||||||
getPlaylistOrder,
|
getPlaylistOrder,
|
||||||
getBookMeta,
|
getBookMeta,
|
||||||
|
getChapterPage,
|
||||||
|
getPdfUrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
export function useGrateWizard() {
|
export function useGrateWizard() {
|
||||||
const appConfig = useAppConfig()
|
const appConfig = useAppConfig()
|
||||||
const { url, popup } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } }
|
const { url } = appConfig.gratewizard as { url: string; popup: { width: number; height: number } }
|
||||||
|
|
||||||
function launch(e?: Event) {
|
function launch(e?: Event) {
|
||||||
const left = Math.round((window.screen.width - popup.width) / 2)
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
const top = Math.round((window.screen.height - popup.height) / 2)
|
e?.preventDefault()
|
||||||
const win = window.open(
|
|
||||||
url,
|
|
||||||
'grateWizard',
|
|
||||||
`width=${popup.width},height=${popup.height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes`,
|
|
||||||
)
|
|
||||||
if (win) e?.preventDefault()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { url, launch }
|
return { url, launch }
|
||||||
|
|||||||
20
app/composables/useTracking.ts
Normal file
20
app/composables/useTracking.ts
Normal 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 }
|
||||||
|
}
|
||||||
@@ -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>
|
<main class="app-main">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
|
<!-- 益 Yì (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>
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ async function save() {
|
|||||||
.field-label {
|
.field-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: hsl(20 8% 50%);
|
color: hsl(var(--color-text-muted));
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,15 +217,15 @@ async function save() {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem 0.625rem;
|
padding: 0.5rem 0.625rem;
|
||||||
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.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Song tags ── */
|
/* ── Song tags ── */
|
||||||
@@ -233,45 +233,45 @@ async function save() {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border: 1px solid hsl(20 8% 22%);
|
border: 1px solid hsl(var(--color-surface-light));
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag:hover {
|
.song-tag:hover {
|
||||||
border-color: hsl(12 76% 48% / 0.4);
|
border-color: hsl(var(--color-primary) / 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag--active {
|
.song-tag--active {
|
||||||
border-color: hsl(12 76% 48% / 0.6);
|
border-color: hsl(var(--color-primary) / 0.6);
|
||||||
background: hsl(12 76% 48% / 0.08);
|
background: hsl(var(--color-primary) / 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag--primary {
|
.song-tag--primary {
|
||||||
border-color: hsl(45 90% 55%);
|
border-color: hsl(var(--color-accent));
|
||||||
background: hsl(45 90% 55% / 0.08);
|
background: hsl(var(--color-accent) / 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag-label {
|
.song-tag-label {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: 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: color 0.15s;
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag--active .song-tag-label {
|
.song-tag--active .song-tag-label {
|
||||||
color: hsl(12 76% 68%);
|
color: hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag--primary .song-tag-label {
|
.song-tag--primary .song-tag-label {
|
||||||
color: hsl(45 90% 65%);
|
color: hsl(var(--color-accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-tag-label:hover {
|
.song-tag-label:hover {
|
||||||
color: white;
|
color: hsl(var(--color-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-star {
|
.song-star {
|
||||||
@@ -281,16 +281,16 @@ async function save() {
|
|||||||
padding: 0.375rem 0 0.375rem 0.625rem;
|
padding: 0.375rem 0 0.375rem 0.625rem;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: hsl(20 8% 30%);
|
color: hsl(var(--color-text-muted));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-star:hover {
|
.song-star:hover {
|
||||||
color: hsl(45 90% 55%);
|
color: hsl(var(--color-accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-star--active {
|
.song-star--active {
|
||||||
color: hsl(45 90% 55%);
|
color: hsl(var(--color-accent));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="font-display text-2xl font-bold text-white">Chapitres</h1>
|
<h1 class="font-display text-2xl font-bold text-white">Livre & chapitres</h1>
|
||||||
<AdminSaveButton :saving="saving" :saved="saved" @save="saveOrder" />
|
<AdminSaveButton :saving="saving" :saved="saved" @save="saveOrder" />
|
||||||
</div>
|
</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">
|
||||||
<div
|
<div
|
||||||
v-for="(chapter, i) in chapters"
|
v-for="(chapter, i) in chapters"
|
||||||
@@ -35,6 +59,20 @@
|
|||||||
>{{ name }}</span>
|
>{{ name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<button
|
||||||
class="delete-btn"
|
class="delete-btn"
|
||||||
@click="removeChapter(chapter.slug)"
|
@click="removeChapter(chapter.slug)"
|
||||||
@@ -91,6 +129,58 @@ const saved = ref(false)
|
|||||||
const newTitle = ref('')
|
const newTitle = ref('')
|
||||||
const newSlug = 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
|
// Drag & drop state
|
||||||
const dragIdx = ref<number | null>(null)
|
const dragIdx = ref<number | null>(null)
|
||||||
const dropIdx = ref<number | null>(null)
|
const dropIdx = ref<number | null>(null)
|
||||||
@@ -156,19 +246,52 @@ async function removeChapter(slug: string) {
|
|||||||
</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;
|
||||||
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 {
|
.chapter-item--dragging {
|
||||||
@@ -176,13 +299,13 @@ async function removeChapter(slug: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chapter-item--over {
|
.chapter-item--over {
|
||||||
border-top: 2px solid hsl(12 76% 48%);
|
border-top: 2px solid hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
color: hsl(20 8% 35%);
|
color: hsl(var(--color-text-muted));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +316,7 @@ async function removeChapter(slug: string) {
|
|||||||
.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;
|
||||||
}
|
}
|
||||||
@@ -205,13 +328,13 @@ async function removeChapter(slug: string) {
|
|||||||
|
|
||||||
.chapter-title {
|
.chapter-title {
|
||||||
display: block;
|
display: block;
|
||||||
color: white;
|
color: hsl(var(--color-text));
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-title:hover {
|
.chapter-title:hover {
|
||||||
color: hsl(12 76% 68%);
|
color: hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-songs {
|
.chapter-songs {
|
||||||
@@ -225,9 +348,26 @@ async function removeChapter(slug: string) {
|
|||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: hsl(12 76% 48% / 0.1);
|
background: hsl(var(--color-primary) / 0.1);
|
||||||
color: hsl(12 76% 60%);
|
color: hsl(var(--color-primary));
|
||||||
border: 1px solid hsl(12 76% 48% / 0.2);
|
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 {
|
.delete-btn {
|
||||||
@@ -248,15 +388,15 @@ async function removeChapter(slug: string) {
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
.add-btn {
|
||||||
@@ -265,9 +405,9 @@ async function removeChapter(slug: string) {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px solid hsl(20 8% 25%);
|
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.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
@@ -275,8 +415,8 @@ async function removeChapter(slug: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:hover:not(:disabled) {
|
.add-btn:hover:not(:disabled) {
|
||||||
border-color: hsl(12 76% 48% / 0.5);
|
border-color: hsl(var(--color-primary) / 0.5);
|
||||||
color: hsl(12 76% 68%);
|
color: hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:disabled {
|
.add-btn:disabled {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -72,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 {
|
||||||
@@ -97,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,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 {
|
||||||
@@ -126,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;
|
||||||
@@ -135,7 +135,7 @@ 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 {
|
||||||
@@ -147,12 +147,12 @@ async function login() {
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: hsl(20 8% 40%);
|
color: hsl(var(--color-text-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.dev-hint code {
|
.dev-hint code {
|
||||||
color: hsl(12 76% 60%);
|
color: hsl(var(--color-primary));
|
||||||
background: hsl(20 8% 10%);
|
background: hsl(var(--color-surface));
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,14 +130,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 +155,22 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 +180,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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
97
app/pages/admin/pages/index.vue
Normal file
97
app/pages/admin/pages/index.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ async function save() {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem 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;
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,13 +172,13 @@ async function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.song-row--over {
|
.song-row--over {
|
||||||
border-top: 2px solid hsl(12 76% 48%);
|
border-top: 2px solid hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
color: hsl(20 8% 35%);
|
color: hsl(var(--color-text-muted));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ async function save() {
|
|||||||
.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;
|
flex-shrink: 0;
|
||||||
@@ -200,16 +200,16 @@ 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;
|
||||||
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 {
|
.lyrics-textarea {
|
||||||
@@ -240,16 +240,16 @@ async function save() {
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px dashed hsl(20 8% 25%);
|
border: 1px dashed hsl(var(--color-surface-light));
|
||||||
background: none;
|
background: none;
|
||||||
color: hsl(20 8% 55%);
|
color: hsl(var(--color-text-muted));
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:hover {
|
.add-btn:hover {
|
||||||
border-color: hsl(12 76% 48% / 0.5);
|
border-color: hsl(var(--color-primary) / 0.5);
|
||||||
color: hsl(12 76% 68%);
|
color: hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative overflow-hidden section-padding">
|
|
||||||
<!-- Shadok jardinier: character with watering can and plant -->
|
|
||||||
<svg class="shadok-jardinier" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
|
||||||
<!-- Body -->
|
|
||||||
<ellipse cx="110" cy="160" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
|
|
||||||
<!-- Head -->
|
|
||||||
<circle cx="110" cy="96" r="25" fill="currentColor" opacity="0.8"/>
|
|
||||||
<!-- Straw hat -->
|
|
||||||
<ellipse cx="110" cy="78" rx="35" ry="8" fill="currentColor" opacity="0.4"/>
|
|
||||||
<path d="M85 78 Q110 60 135 78" fill="currentColor" opacity="0.35"/>
|
|
||||||
<!-- Eyes (focused, looking down at plant) -->
|
|
||||||
<circle cx="102" cy="94" r="4" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="120" cy="94" r="4" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="103" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="121" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<!-- Smile -->
|
|
||||||
<path d="M103 106 Q110 111 118 106" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
|
||||||
<!-- Arm holding watering can -->
|
|
||||||
<line x1="70" y1="150" x2="40" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Watering can -->
|
|
||||||
<rect x="20" y="165" width="30" height="20" rx="3" fill="currentColor" opacity="0.4"/>
|
|
||||||
<line x1="20" y1="168" x2="10" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
<!-- Water drops -->
|
|
||||||
<circle cx="12" cy="165" r="1.5" fill="currentColor" opacity="0.25"/>
|
|
||||||
<circle cx="8" cy="170" r="1.5" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="15" cy="172" r="1.5" fill="currentColor" opacity="0.2"/>
|
|
||||||
<!-- Other arm -->
|
|
||||||
<line x1="150" y1="150" x2="170" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Legs -->
|
|
||||||
<line x1="95" y1="205" x2="85" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="125" y1="205" x2="135" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Plant -->
|
|
||||||
<line x1="180" y1="220" x2="180" y2="180" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
<path d="M180 195 Q195 185 190 175" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
|
|
||||||
<path d="M180 205 Q165 195 168 185" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
|
|
||||||
<path d="M180 185 Q192 172 188 165" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.3"/>
|
|
||||||
<!-- Pot -->
|
|
||||||
<path d="M170 220 L175 240 L185 240 L190 220 Z" fill="currentColor" opacity="0.35"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Shadok bâtisseur: character with trowel building a wall -->
|
|
||||||
<svg class="shadok-batisseur" viewBox="0 0 260 300" fill="none" aria-hidden="true">
|
|
||||||
<!-- Body -->
|
|
||||||
<ellipse cx="130" cy="150" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
|
|
||||||
<!-- Head -->
|
|
||||||
<circle cx="130" cy="86" r="25" fill="currentColor" opacity="0.8"/>
|
|
||||||
<!-- Hard hat -->
|
|
||||||
<ellipse cx="130" cy="68" rx="28" ry="6" fill="currentColor" opacity="0.4"/>
|
|
||||||
<rect x="108" y="60" width="44" height="10" rx="3" fill="currentColor" opacity="0.35"/>
|
|
||||||
<!-- Eyes (determined) -->
|
|
||||||
<circle cx="122" cy="84" r="4" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="140" cy="84" r="4" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="123" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="141" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<!-- Grin -->
|
|
||||||
<path d="M123 96 Q130 101 138 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
|
||||||
<!-- Arm with trowel -->
|
|
||||||
<line x1="170" y1="140" x2="210" y2="120" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Trowel -->
|
|
||||||
<polygon points="210,115 230,110 225,120 210,122" fill="currentColor" opacity="0.45"/>
|
|
||||||
<line x1="210" y1="118" x2="200" y2="125" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
<!-- Other arm -->
|
|
||||||
<line x1="90" y1="145" x2="65" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Legs -->
|
|
||||||
<line x1="115" y1="195" x2="105" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="145" y1="195" x2="155" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Wall (bricks) -->
|
|
||||||
<rect x="40" y="200" width="50" height="16" rx="1" fill="currentColor" opacity="0.3"/>
|
|
||||||
<rect x="45" y="183" width="40" height="16" rx="1" fill="currentColor" opacity="0.28"/>
|
|
||||||
<rect x="50" y="166" width="30" height="16" rx="1" fill="currentColor" opacity="0.25"/>
|
|
||||||
<!-- Brick lines -->
|
|
||||||
<line x1="65" y1="200" x2="65" y2="216" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
|
||||||
<line x1="55" y1="183" x2="55" y2="199" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
|
||||||
</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 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 flex flex-col gap-6">
|
|
||||||
<div
|
|
||||||
v-for="(extract, i) in content?.extracts"
|
|
||||||
:key="i"
|
|
||||||
class="card-surface"
|
|
||||||
>
|
|
||||||
<p class="mb-2 font-mono text-xs tracking-widest text-accent uppercase">
|
|
||||||
{{ extract.chapter }}
|
|
||||||
</p>
|
|
||||||
<blockquote class="border-l-2 border-primary/30 pl-4 text-white/70 italic leading-relaxed whitespace-pre-line">
|
|
||||||
{{ extract.text }}
|
|
||||||
</blockquote>
|
|
||||||
<div class="mt-4">
|
|
||||||
<NuxtLink
|
|
||||||
:to="`/modele-eco/${extract.chapterSlug}`"
|
|
||||||
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
|
|
||||||
>
|
|
||||||
Lire le chapitre
|
|
||||||
<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('autonomie')
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: content.value?.meta?.title ?? 'Autonomie',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page-title {
|
|
||||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadok-jardinier {
|
|
||||||
position: absolute;
|
|
||||||
left: 2%;
|
|
||||||
top: 5%;
|
|
||||||
width: clamp(100px, 14vw, 200px);
|
|
||||||
opacity: 0.28;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-primary));
|
|
||||||
animation: shadok-float-jardinier 10s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadok-batisseur {
|
|
||||||
position: absolute;
|
|
||||||
right: 2%;
|
|
||||||
bottom: 5%;
|
|
||||||
width: clamp(110px, 15vw, 210px);
|
|
||||||
opacity: 0.3;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-accent));
|
|
||||||
animation: shadok-float-batisseur 11s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-float-jardinier {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-10px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-float-batisseur {
|
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
|
||||||
50% { transform: translateY(-8px) rotate(1deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.shadok-jardinier { display: none; }
|
|
||||||
.shadok-batisseur { display: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
159
app/pages/citoyenne/[slug].vue
Normal file
159
app/pages/citoyenne/[slug].vue
Normal 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>
|
||||||
631
app/pages/citoyenne/index.vue
Normal file
631
app/pages/citoyenne/index.vue
Normal 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>
|
||||||
453
app/pages/economique/commande.vue
Normal file
453
app/pages/economique/commande.vue
Normal 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>
|
||||||
647
app/pages/economique/index.vue
Normal file
647
app/pages/economique/index.vue
Normal 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>
|
||||||
@@ -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="`/modele-eco/${prevChapter.stem?.split('/').pop()}`"
|
: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="`/modele-eco/${nextChapter.stem?.split('/').pop()}`"
|
: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>
|
||||||
562
app/pages/economique/modele-eco/index.vue
Normal file
562
app/pages/economique/modele-eco/index.vue
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
<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-16"
|
||||||
|
:show-chapters="false"
|
||||||
|
@open-player="showBookPlayer = true"
|
||||||
|
@open-pdf="showPdfReader = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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" 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">
|
||||||
|
<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?.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(2rem, 5vw, 2.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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>
|
||||||
119
app/pages/economique/monnaie-libre.vue
Normal file
119
app/pages/economique/monnaie-libre.vue
Normal 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>
|
||||||
85
app/pages/economique/productions-collectives.vue
Normal file
85
app/pages/economique/productions-collectives.vue
Normal 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>
|
||||||
@@ -1,152 +1,340 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative overflow-hidden section-padding min-h-[70vh] flex items-center justify-center">
|
<div class="relative overflow-hidden section-padding min-h-[70vh] flex items-center justify-center">
|
||||||
<!-- Shadok jongleur: juggling coins (top-left) -->
|
<!-- 1. Shadok funambule: walking on tightrope (top-left) -->
|
||||||
<svg class="shadok-juggler" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
<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">♪</text>
|
||||||
|
<text x="145" y="68" fill="currentColor" opacity="0.2" font-size="11">♫</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 -->
|
<!-- Body -->
|
||||||
<ellipse cx="120" cy="160" rx="38" ry="46" fill="currentColor" opacity="0.85"/>
|
<ellipse cx="80" cy="120" rx="21" ry="27" fill="currentColor" opacity="0.25"/>
|
||||||
<!-- Head -->
|
<!-- Head -->
|
||||||
<circle cx="120" cy="98" r="24" fill="currentColor" opacity="0.8"/>
|
<circle cx="80" cy="82" r="15" fill="currentColor" opacity="0.3"/>
|
||||||
<!-- Eyes (looking up at coins) -->
|
<!-- Eyes (looking up, wide) -->
|
||||||
<circle cx="112" cy="92" r="3.5" fill="currentColor" opacity="0.2"/>
|
<circle cx="74" cy="78" r="2" fill="currentColor" opacity="0.5"/>
|
||||||
<circle cx="130" cy="92" r="3.5" fill="currentColor" opacity="0.2"/>
|
<circle cx="86" cy="78" r="2" fill="currentColor" opacity="0.5"/>
|
||||||
<circle cx="113" cy="91" r="1.5" fill="currentColor" opacity="0.5"/>
|
<!-- Mouth open (concentration) -->
|
||||||
<circle cx="131" cy="91" r="1.5" fill="currentColor" opacity="0.5"/>
|
<ellipse cx="80" cy="92" rx="4" ry="3" fill="currentColor" opacity="0.2"/>
|
||||||
<!-- Smile -->
|
<!-- Beak (small, front view) -->
|
||||||
<path d="M112 108 Q120 114 128 108" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.35"/>
|
<polygon points="80,86 87,89 80,92" fill="currentColor" opacity="0.3"/>
|
||||||
<!-- Arms up (juggling) -->
|
<!-- Arms up wide -->
|
||||||
<line x1="85" y1="145" x2="55" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
<line x1="60" y1="110" x2="30" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
|
||||||
<line x1="155" y1="145" x2="185" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
<line x1="100" y1="110" x2="130" y2="75" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
|
||||||
<!-- Hands -->
|
<!-- 4 juggling balls in arc -->
|
||||||
<circle cx="55" cy="103" r="4" fill="currentColor" opacity="0.4"/>
|
<circle cx="40" cy="35" r="8" fill="currentColor" opacity="0.3"/>
|
||||||
<circle cx="185" cy="103" r="4" fill="currentColor" opacity="0.4"/>
|
<circle cx="70" cy="18" r="8" fill="currentColor" opacity="0.25"/>
|
||||||
<!-- Juggling coins -->
|
<circle cx="100" cy="15" r="8" fill="currentColor" opacity="0.3"/>
|
||||||
<circle cx="90" cy="55" r="8" fill="currentColor" opacity="0.35"/>
|
<circle cx="128" cy="30" r="8" fill="currentColor" opacity="0.25"/>
|
||||||
<text x="86" y="59" fill="currentColor" opacity="0.5" font-size="10" font-weight="bold">$</text>
|
<!-- Long legs -->
|
||||||
<circle cx="120" cy="40" r="8" fill="currentColor" opacity="0.3"/>
|
<line x1="72" y1="145" x2="60" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
|
||||||
<text x="116" y="44" fill="currentColor" opacity="0.45" font-size="10" font-weight="bold">$</text>
|
<ellipse cx="60" cy="202" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
|
||||||
<circle cx="150" cy="50" r="8" fill="currentColor" opacity="0.32"/>
|
<line x1="88" y1="145" x2="100" y2="200" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
|
||||||
<text x="146" y="54" fill="currentColor" opacity="0.48" font-size="10" font-weight="bold">$</text>
|
<ellipse cx="100" cy="202" rx="8" ry="3" fill="currentColor" opacity="0.3"/>
|
||||||
<!-- Legs -->
|
|
||||||
<line x1="105" y1="203" x2="95" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="135" y1="203" x2="145" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Shadok échelle: on a wobbly ladder (top-right) -->
|
<!-- 4. Shadok cracheur de feu: head tilted back, flame from mouth -->
|
||||||
<svg class="shadok-ladder" viewBox="0 0 220 320" fill="none" aria-hidden="true">
|
<svg class="shadok-cracheur" viewBox="0 0 170 220" fill="none" aria-hidden="true">
|
||||||
<!-- Ladder (tilting) -->
|
|
||||||
<line x1="80" y1="50" x2="70" y2="300" stroke="currentColor" stroke-width="3" opacity="0.35"/>
|
|
||||||
<line x1="150" y1="50" x2="140" y2="300" stroke="currentColor" stroke-width="3" opacity="0.35"/>
|
|
||||||
<!-- Rungs -->
|
|
||||||
<line x1="82" y1="80" x2="148" y2="80" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
|
||||||
<line x1="83" y1="120" x2="147" y2="120" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
|
||||||
<line x1="84" y1="160" x2="146" y2="160" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
|
||||||
<line x1="85" y1="200" x2="145" y2="200" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
|
||||||
<line x1="86" y1="240" x2="144" y2="240" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
|
||||||
<!-- Shadok on top (arms out for balance) -->
|
|
||||||
<ellipse cx="115" cy="68" rx="18" ry="14" fill="currentColor" opacity="0.85"/>
|
|
||||||
<circle cx="115" cy="46" r="14" fill="currentColor" opacity="0.8"/>
|
|
||||||
<!-- Eyes (worried) -->
|
|
||||||
<circle cx="110" cy="43" r="3" fill="currentColor" opacity="0.25"/>
|
|
||||||
<circle cx="122" cy="43" r="3" fill="currentColor" opacity="0.25"/>
|
|
||||||
<circle cx="110" cy="44" r="1.2" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="122" cy="44" r="1.2" fill="currentColor" opacity="0.5"/>
|
|
||||||
<!-- Worried mouth -->
|
|
||||||
<path d="M108 52 Q115 49 122 52" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
|
||||||
<!-- Arms out (balancing) -->
|
|
||||||
<line x1="97" y1="62" x2="60" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="133" y1="62" x2="170" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Shadok acrobate: doing a cartwheel (center) -->
|
|
||||||
<svg class="shadok-acrobat" viewBox="0 0 260 240" fill="none" aria-hidden="true">
|
|
||||||
<!-- Body (sideways, mid-cartwheel) -->
|
|
||||||
<ellipse cx="130" cy="120" rx="30" ry="38" fill="currentColor" opacity="0.85" transform="rotate(45 130 120)"/>
|
|
||||||
<!-- Head -->
|
|
||||||
<circle cx="155" cy="82" r="20" fill="currentColor" opacity="0.8"/>
|
|
||||||
<!-- Eyes (dizzy/happy) -->
|
|
||||||
<path d="M148 78 Q152 74 156 78" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
|
|
||||||
<path d="M160 78 Q164 74 168 78" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
|
|
||||||
<!-- Smile -->
|
|
||||||
<path d="M150 90 Q158 95 165 90" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
|
||||||
<!-- Arms (one touching ground, one up) -->
|
|
||||||
<line x1="110" y1="100" x2="80" y2="130" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="150" y1="105" x2="185" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Hand on ground -->
|
|
||||||
<circle cx="78" cy="132" r="4" fill="currentColor" opacity="0.4"/>
|
|
||||||
<!-- Legs (splayed in cartwheel) -->
|
|
||||||
<line x1="125" y1="155" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="140" y1="150" x2="175" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Motion lines -->
|
|
||||||
<path d="M70 110 Q60 105 55 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
|
||||||
<path d="M190 60 Q200 55 205 65" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Shadok dormeur: sleeping on a cloud (bottom-left) -->
|
|
||||||
<svg class="shadok-sleeper" viewBox="0 0 260 220" fill="none" aria-hidden="true">
|
|
||||||
<!-- Cloud -->
|
|
||||||
<ellipse cx="130" cy="150" rx="80" ry="25" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="80" cy="140" r="25" fill="currentColor" opacity="0.18"/>
|
|
||||||
<circle cx="120" cy="130" r="30" fill="currentColor" opacity="0.2"/>
|
|
||||||
<circle cx="165" cy="135" r="22" fill="currentColor" opacity="0.18"/>
|
|
||||||
<circle cx="190" cy="142" r="18" fill="currentColor" opacity="0.15"/>
|
|
||||||
<!-- Shadok body (lying down) -->
|
|
||||||
<ellipse cx="130" cy="125" rx="35" ry="18" fill="currentColor" opacity="0.85"/>
|
|
||||||
<!-- Head (on cloud, sideways) -->
|
|
||||||
<ellipse cx="85" cy="118" rx="18" ry="16" fill="currentColor" opacity="0.8"/>
|
|
||||||
<!-- Closed eyes (sleeping) -->
|
|
||||||
<path d="M76 115 Q80 112 84 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
|
|
||||||
<path d="M88 115 Q92 112 96 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
|
|
||||||
<!-- Snooze bubbles -->
|
|
||||||
<text x="70" y="100" fill="currentColor" opacity="0.3" font-size="12" font-weight="bold">z</text>
|
|
||||||
<text x="60" y="85" fill="currentColor" opacity="0.25" font-size="16" font-weight="bold">z</text>
|
|
||||||
<text x="48" y="68" fill="currentColor" opacity="0.2" font-size="20" font-weight="bold">z</text>
|
|
||||||
<!-- Legs (curled) -->
|
|
||||||
<path d="M165 125 Q180 130 175 140" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.5"/>
|
|
||||||
<path d="M160 130 Q172 138 168 148" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.5"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Shadok cuisinier: cooking in a cauldron (bottom-right) -->
|
|
||||||
<svg class="shadok-cook" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<ellipse cx="120" cy="145" rx="38" ry="45" fill="currentColor" opacity="0.85"/>
|
<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 -->
|
<!-- Head -->
|
||||||
<circle cx="120" cy="85" r="24" fill="currentColor" opacity="0.8"/>
|
<circle cx="90" cy="70" r="16" fill="currentColor" opacity="0.3"/>
|
||||||
<!-- Chef hat -->
|
<!-- Eyes (intense, looking at drums) -->
|
||||||
<ellipse cx="120" cy="62" rx="22" ry="18" fill="currentColor" opacity="0.35"/>
|
<circle cx="84" cy="68" r="2" fill="currentColor" opacity="0.5"/>
|
||||||
<rect x="105" y="68" width="30" height="6" rx="1" fill="currentColor" opacity="0.4"/>
|
<circle cx="96" cy="69" r="2" fill="currentColor" opacity="0.5"/>
|
||||||
<!-- Eyes (focused on cooking) -->
|
<!-- Beak (front, small) -->
|
||||||
<circle cx="112" cy="82" r="3.5" fill="currentColor" opacity="0.2"/>
|
<polygon points="90,76 97,80 90,82" fill="currentColor" opacity="0.3"/>
|
||||||
<circle cx="130" cy="82" r="3.5" fill="currentColor" opacity="0.2"/>
|
<!-- Arms raised with drumsticks -->
|
||||||
<circle cx="113" cy="83" r="1.5" fill="currentColor" opacity="0.5"/>
|
<line x1="70" y1="100" x2="40" y2="65" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
|
||||||
<circle cx="131" cy="83" r="1.5" fill="currentColor" opacity="0.5"/>
|
<line x1="40" y1="65" x2="25" y2="50" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
|
||||||
<!-- Tongue out (concentrating) -->
|
<line x1="110" y1="100" x2="140" y2="60" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.4"/>
|
||||||
<path d="M115 96 Q120 100 125 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
<line x1="140" y1="60" x2="158" y2="48" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.35"/>
|
||||||
<!-- Arm with ladle -->
|
<!-- Motion lines on sticks -->
|
||||||
<line x1="155" y1="135" x2="185" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
<path d="M22 52 Q18 48 20 44" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||||
<!-- Ladle -->
|
<path d="M160 50 Q164 46 162 42" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||||
<line x1="185" y1="175" x2="175" y2="200" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
|
<!-- Legs (tucked behind kit) -->
|
||||||
<ellipse cx="175" cy="205" rx="8" ry="5" fill="currentColor" opacity="0.35"/>
|
<line x1="80" y1="136" x2="70" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
|
||||||
<!-- Other arm -->
|
<ellipse cx="70" cy="197" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
|
||||||
<line x1="85" y1="140" x2="60" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
<line x1="100" y1="136" x2="110" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.35"/>
|
||||||
<!-- Legs -->
|
<ellipse cx="110" cy="197" rx="7" ry="3" fill="currentColor" opacity="0.25"/>
|
||||||
<line x1="105" y1="188" x2="95" y2="250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
</svg>
|
||||||
<line x1="135" y1="188" x2="145" y2="250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Cauldron -->
|
<!-- 7. Shadok marionnettiste: holding puppet strings -->
|
||||||
<path d="M55 220 Q55 260 120 260 Q185 260 185 220" fill="currentColor" opacity="0.3"/>
|
<svg class="shadok-marionnettiste" viewBox="0 0 160 220" fill="none" aria-hidden="true">
|
||||||
<ellipse cx="120" cy="220" rx="65" ry="12" fill="currentColor" opacity="0.25"/>
|
<!-- Body -->
|
||||||
<ellipse cx="120" cy="220" rx="65" ry="12" stroke="currentColor" stroke-width="2" fill="none" opacity="0.35"/>
|
<ellipse cx="80" cy="75" rx="20" ry="26" fill="currentColor" opacity="0.25"/>
|
||||||
<!-- Steam -->
|
<!-- Head -->
|
||||||
<path d="M95 210 Q90 195 95 185" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
<circle cx="80" cy="40" r="15" fill="currentColor" opacity="0.3"/>
|
||||||
<path d="M120 208 Q118 190 122 180" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
<!-- Eyes (looking down at puppet) -->
|
||||||
<path d="M145 210 Q148 195 143 185" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
<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>
|
</svg>
|
||||||
|
|
||||||
<div class="container-content relative z-10 text-center">
|
<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>
|
<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 text-white">
|
<h1 class="page-title font-display font-extrabold tracking-tight" style="color: hsl(var(--color-text))">
|
||||||
{{ content?.title }}
|
{{ content?.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-4 text-lg text-white/50">
|
<p class="mt-4 text-lg" style="color: hsl(var(--color-text-muted))">
|
||||||
{{ content?.description }}
|
{{ content?.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,98 +358,201 @@ useHead({
|
|||||||
font-size: clamp(2.5rem, 6vw, 3.5rem);
|
font-size: clamp(2.5rem, 6vw, 3.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadok-juggler {
|
/* 1. Funambule — top-left */
|
||||||
|
.shadok-funambule {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 4%;
|
left: 4%;
|
||||||
top: 5%;
|
top: 5%;
|
||||||
width: clamp(100px, 14vw, 190px);
|
width: clamp(70px, 10vw, 140px);
|
||||||
opacity: 0.3;
|
opacity: 0.24;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
animation: shadok-bounce-juggler 4s ease-in-out infinite;
|
animation: shadok-sway-funambule 9s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadok-ladder {
|
/* 2. Accordeoniste — top-right */
|
||||||
|
.shadok-accordeoniste {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4%;
|
right: 3%;
|
||||||
top: 3%;
|
top: 4%;
|
||||||
width: clamp(90px, 12vw, 170px);
|
width: clamp(70px, 10vw, 140px);
|
||||||
opacity: 0.28;
|
opacity: 0.22;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: hsl(var(--color-accent));
|
color: hsl(var(--color-accent));
|
||||||
animation: shadok-wobble-ladder 5s ease-in-out infinite;
|
animation: shadok-bounce-accordeon 8s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadok-acrobat {
|
/* 3. Jongleur — top-center */
|
||||||
|
.shadok-jongleur {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 55%;
|
top: 2%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: clamp(100px, 13vw, 180px);
|
width: clamp(70px, 10vw, 130px);
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
animation: shadok-spin-acrobat 6s ease-in-out infinite;
|
animation: shadok-float-jongleur 7s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadok-sleeper {
|
/* 4. Cracheur de feu — left 5%, 40% */
|
||||||
|
.shadok-cracheur {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 3%;
|
left: 5%;
|
||||||
bottom: 5%;
|
top: 40%;
|
||||||
width: clamp(110px, 15vw, 210px);
|
width: clamp(70px, 10vw, 140px);
|
||||||
opacity: 0.25;
|
opacity: 0.22;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: hsl(var(--color-accent));
|
color: hsl(var(--color-accent));
|
||||||
animation: shadok-float-sleeper 8s ease-in-out infinite;
|
animation: shadok-flicker-cracheur 8s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadok-cook {
|
/* 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;
|
position: absolute;
|
||||||
right: 3%;
|
right: 3%;
|
||||||
bottom: 4%;
|
bottom: 4%;
|
||||||
width: clamp(100px, 14vw, 200px);
|
width: clamp(70px, 10vw, 140px);
|
||||||
opacity: 0.3;
|
opacity: 0.22;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-accent));
|
||||||
animation: shadok-bounce-cook 5s ease-in-out infinite;
|
animation: shadok-rush-regisseur 7s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shadok-bounce-juggler {
|
@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); }
|
0%, 100% { transform: translateY(0); }
|
||||||
30% { transform: translateY(-12px); }
|
40% { transform: translateY(-8px); }
|
||||||
60% { transform: translateY(-6px); }
|
70% { transform: translateY(-3px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shadok-wobble-ladder {
|
@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); }
|
0%, 100% { transform: rotate(0deg); }
|
||||||
25% { transform: rotate(3deg); }
|
25% { transform: rotate(4deg); }
|
||||||
75% { transform: rotate(-3deg); }
|
75% { transform: rotate(-4deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shadok-spin-acrobat {
|
@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); }
|
0% { transform: translateX(-50%) rotate(0deg); }
|
||||||
25% { transform: translateX(-50%) rotate(15deg); }
|
50% { transform: translateX(-50%) rotate(10deg); }
|
||||||
50% { transform: translateX(-50%) rotate(0deg); }
|
|
||||||
75% { transform: translateX(-50%) rotate(-15deg); }
|
|
||||||
100% { transform: translateX(-50%) rotate(0deg); }
|
100% { transform: translateX(-50%) rotate(0deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shadok-float-sleeper {
|
@keyframes shadok-rush-regisseur {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateX(0) translateY(0); }
|
||||||
50% { transform: translateY(-6px); }
|
25% { transform: translateX(3px) translateY(-5px); }
|
||||||
}
|
50% { transform: translateX(-2px) translateY(-2px); }
|
||||||
|
75% { transform: translateX(2px) translateY(-6px); }
|
||||||
@keyframes shadok-bounce-cook {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
40% { transform: translateY(-10px); }
|
|
||||||
70% { transform: translateY(-4px); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.shadok-juggler { display: none; }
|
.shadok-funambule,
|
||||||
.shadok-ladder { display: none; }
|
.shadok-accordeoniste,
|
||||||
.shadok-acrobat { display: none; }
|
.shadok-jongleur,
|
||||||
.shadok-sleeper { display: none; }
|
.shadok-cracheur,
|
||||||
.shadok-cook { display: none; }
|
.shadok-trapeziste,
|
||||||
|
.shadok-batteur,
|
||||||
|
.shadok-marionnettiste,
|
||||||
|
.shadok-clown,
|
||||||
|
.shadok-acrobate,
|
||||||
|
.shadok-regisseur {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative overflow-hidden section-padding">
|
|
||||||
<!-- Shadok reader: character with big glasses reading a book -->
|
|
||||||
<svg class="shadok-reader" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
|
||||||
<!-- Body -->
|
|
||||||
<ellipse cx="120" cy="170" rx="45" ry="55" fill="currentColor" opacity="0.85"/>
|
|
||||||
<!-- Head -->
|
|
||||||
<circle cx="120" cy="100" r="28" fill="currentColor" opacity="0.8"/>
|
|
||||||
<!-- Big round glasses -->
|
|
||||||
<circle cx="107" cy="94" r="11" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.5"/>
|
|
||||||
<circle cx="133" cy="94" r="11" stroke="currentColor" stroke-width="2.5" fill="none" opacity="0.5"/>
|
|
||||||
<line x1="118" y1="94" x2="122" y2="94" stroke="currentColor" stroke-width="2" opacity="0.5"/>
|
|
||||||
<!-- Eyes behind glasses -->
|
|
||||||
<circle cx="108" cy="93" r="2.5" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="134" cy="93" r="2.5" fill="currentColor" opacity="0.5"/>
|
|
||||||
<!-- Arms holding book -->
|
|
||||||
<line x1="78" y1="155" x2="60" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="162" y1="155" x2="180" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<!-- Book (open) -->
|
|
||||||
<rect x="55" y="175" width="55" height="40" rx="2" fill="currentColor" opacity="0.35"/>
|
|
||||||
<rect x="110" y="175" width="55" height="40" rx="2" fill="currentColor" opacity="0.3"/>
|
|
||||||
<line x1="110" y1="175" x2="110" y2="215" stroke="currentColor" stroke-width="2" opacity="0.5"/>
|
|
||||||
<!-- Book lines (text) -->
|
|
||||||
<line x1="65" y1="188" x2="100" y2="188" stroke="currentColor" stroke-width="1" opacity="0.2"/>
|
|
||||||
<line x1="65" y1="195" x2="95" y2="195" stroke="currentColor" stroke-width="1" opacity="0.2"/>
|
|
||||||
<line x1="65" y1="202" x2="98" y2="202" stroke="currentColor" stroke-width="1" opacity="0.2"/>
|
|
||||||
<!-- Legs -->
|
|
||||||
<line x1="105" y1="222" x2="95" y2="270" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="135" y1="222" x2="145" y2="270" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Shadok stack: pile of books tilting -->
|
|
||||||
<svg class="shadok-stack" viewBox="0 0 160 220" fill="none" aria-hidden="true">
|
|
||||||
<!-- Bottom book -->
|
|
||||||
<rect x="20" y="170" width="120" height="22" rx="3" fill="currentColor" opacity="0.5" transform="rotate(-2 80 181)"/>
|
|
||||||
<!-- Second book -->
|
|
||||||
<rect x="30" y="145" width="100" height="20" rx="3" fill="currentColor" opacity="0.45" transform="rotate(3 80 155)"/>
|
|
||||||
<!-- Third book -->
|
|
||||||
<rect x="25" y="120" width="110" height="18" rx="3" fill="currentColor" opacity="0.4" transform="rotate(-4 80 129)"/>
|
|
||||||
<!-- Fourth book -->
|
|
||||||
<rect x="35" y="97" width="90" height="18" rx="3" fill="currentColor" opacity="0.35" transform="rotate(5 80 106)"/>
|
|
||||||
<!-- Fifth book (tilting more) -->
|
|
||||||
<rect x="40" y="74" width="80" height="17" rx="3" fill="currentColor" opacity="0.3" transform="rotate(-7 80 82)"/>
|
|
||||||
<!-- Top book (really tilting) -->
|
|
||||||
<rect x="45" y="52" width="70" height="16" rx="3" fill="currentColor" opacity="0.25" transform="rotate(10 80 60)"/>
|
|
||||||
<!-- Tiny Shadok sitting on top -->
|
|
||||||
<ellipse cx="85" cy="42" rx="10" ry="8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="85" cy="30" r="6" fill="currentColor" opacity="0.45"/>
|
|
||||||
<circle cx="87" cy="29" r="1.5" fill="currentColor" opacity="0.3"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Shadok scribe: character with quill and inkwell -->
|
|
||||||
<svg class="shadok-scribe" viewBox="0 0 240 280" 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="72" rx="20" ry="8" fill="currentColor" opacity="0.35"/>
|
|
||||||
<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="91" r="1.8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<circle cx="131" cy="91" r="1.8" fill="currentColor" opacity="0.5"/>
|
|
||||||
<path d="M118 104 Q120 108 122 104" fill="currentColor" opacity="0.3"/>
|
|
||||||
<line x1="160" y1="140" x2="190" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<line x1="190" y1="170" x2="210" y2="145" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
|
|
||||||
<path d="M210 145 Q215 140 212 135 Q208 138 210 145" fill="currentColor" opacity="0.3"/>
|
|
||||||
<line x1="80" y1="148" x2="55" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<path d="M100 200 Q90 230 95 250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
|
|
||||||
<path d="M140 200 Q150 230 145 250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
|
|
||||||
<rect x="45" y="185" width="20" height="18" rx="3" fill="currentColor" opacity="0.35"/>
|
|
||||||
<ellipse cx="55" cy="185" rx="12" ry="4" fill="currentColor" opacity="0.3"/>
|
|
||||||
<rect x="170" y="175" width="40" height="50" rx="2" fill="currentColor" opacity="0.2"/>
|
|
||||||
<line x1="178" y1="188" x2="202" y2="188" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
|
||||||
<line x1="178" y1="195" x2="200" y2="195" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
|
||||||
<line x1="178" y1="202" x2="198" y2="202" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
|
||||||
</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 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="`/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">
|
|
||||||
<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?.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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
definePageMeta({
|
|
||||||
layout: 'default',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: content } = await usePageContent('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(),
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page-title {
|
|
||||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadok-reader {
|
|
||||||
position: absolute;
|
|
||||||
right: 2%;
|
|
||||||
top: 3%;
|
|
||||||
width: clamp(110px, 15vw, 220px);
|
|
||||||
opacity: 0.3;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-primary));
|
|
||||||
animation: shadok-float-reader 9s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadok-stack {
|
|
||||||
position: absolute;
|
|
||||||
left: 2%;
|
|
||||||
bottom: 5%;
|
|
||||||
width: clamp(100px, 13vw, 180px);
|
|
||||||
opacity: 0.25;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-accent));
|
|
||||||
animation: shadok-float-stack 11s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-float-reader {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-10px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-float-stack {
|
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
|
||||||
50% { transform: translateY(-8px) rotate(-2deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadok-scribe {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
bottom: 3%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: clamp(100px, 13vw, 180px);
|
|
||||||
opacity: 0.25;
|
|
||||||
pointer-events: none;
|
|
||||||
color: hsl(var(--color-primary));
|
|
||||||
animation: shadok-float-scribe 10s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shadok-float-scribe {
|
|
||||||
0%, 100% { transform: translateX(-50%) translateY(0); }
|
|
||||||
50% { transform: translateX(-50%) translateY(-8px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.shadok-reader { display: none; }
|
|
||||||
.shadok-stack { display: none; }
|
|
||||||
.shadok-scribe { display: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
1902
app/pages/numerique/[slug].vue
Normal file
1902
app/pages/numerique/[slug].vue
Normal file
File diff suppressed because it is too large
Load Diff
586
app/pages/numerique/index.vue
Normal file
586
app/pages/numerique/index.vue
Normal 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"></></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>
|
||||||
@@ -77,7 +77,7 @@ const palettes: Record<PaletteName, PaletteColors> = {
|
|||||||
|
|
||||||
export const usePaletteStore = defineStore('palette', () => {
|
export const usePaletteStore = defineStore('palette', () => {
|
||||||
const currentPalette = ref<PaletteName>(
|
const currentPalette = ref<PaletteName>(
|
||||||
(import.meta.client && localStorage.getItem('palette') as PaletteName) || 'automne',
|
(import.meta.client && localStorage.getItem('palette') as PaletteName) || 'ete',
|
||||||
)
|
)
|
||||||
|
|
||||||
const colors = computed(() => palettes[currentPalette.value])
|
const colors = computed(() => palettes[currentPalette.value])
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Ce chapitre est une **invitation à lire la Théorie Relative de la Monnaie** (T
|
|||||||
|
|
||||||
## Le flux monétaire
|
## Le flux monétaire
|
||||||
|
|
||||||
En monnaie libre, le flux monétaire est **corrélé à l'existence physique** de chaque être humain. Tant que tu vis, tu co-crées de la monnaie. Le Dividende Universel (DU) est cette part de création, identique pour chaque membre, chaque jour.
|
En monnaie libre, le flux monétaire est **corrélé à l'existence physique** de chaque être humain. Tant que tu vis, tu crées de la monnaie. Le Dividende Universel (DU) est cette part de création, identique pour chaque membre, chaque jour.
|
||||||
|
|
||||||
## La symétrie spatio-temporelle
|
## La symétrie spatio-temporelle
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ readingTime: "25 min"
|
|||||||
|
|
||||||
### La June est une crypto
|
### La June est une crypto
|
||||||
|
|
||||||
Oui, la June (Ğ1) est une **cryptomonnaie** au sens technique : elle repose sur un réseau décentralisé, des algorithmes cryptographiques et une blockchain. Mais elle est fondamentalement différente des cryptos spéculatives par sa **co-création symétrique** (le DU) et l'absence de minage compétitif.
|
Oui, la June (Ğ1) est une **cryptomonnaie** au sens technique : elle repose sur un réseau décentralisé, des algorithmes cryptographiques et une blockchain. Mais elle est fondamentalement différente des cryptos spéculatives par sa **création symétrique** (le DU) et l'absence de minage compétitif.
|
||||||
|
|
||||||
### DeX vs CeX
|
### DeX vs CeX
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ FROM node:${NODE_VERSION} AS base
|
|||||||
|
|
||||||
ARG PORT=3000
|
ARG PORT=3000
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /app
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
@@ -19,6 +19,7 @@ RUN pnpm rebuild sharp
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN sh scripts/copy-pdfjs.sh
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
# Production
|
# Production
|
||||||
@@ -27,10 +28,11 @@ FROM base AS production
|
|||||||
ENV PORT=${PORT}
|
ENV PORT=${PORT}
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
COPY --from=build /src/.output /src/.output
|
RUN apt-get update && apt-get -fy install curl git && rm -rf /var/cache/apt/*
|
||||||
COPY --from=build /src/site /src/site
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get -fy install curl && rm -rf /var/cache/apt/*
|
COPY --from=build /app/.output /app/.output
|
||||||
|
COPY --from=build /app/site /app/site
|
||||||
|
COPY --from=build /app/content /app/content
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD curl -f http://localhost:${PORT}/api/health || exit 1
|
CMD curl -f http://localhost:${PORT}/api/health || exit 1
|
||||||
|
|||||||
52
docker/docker-compose.umami.yml
Normal file
52
docker/docker-compose.umami.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
## Umami analytics — déployer avec le compose principal
|
||||||
|
## Usage : docker compose -f docker-compose.yml -f docker-compose.umami.yml up -d
|
||||||
|
##
|
||||||
|
## Variables à définir dans .env :
|
||||||
|
## UMAMI_DB_PASSWORD — mot de passe PostgreSQL Umami (générer avec openssl rand -hex 32)
|
||||||
|
## UMAMI_APP_SECRET — secret applicatif (générer avec openssl rand -hex 32)
|
||||||
|
## UMAMI_DOMAIN — ex: stats.librodrome.org
|
||||||
|
|
||||||
|
name: librodrome
|
||||||
|
|
||||||
|
services:
|
||||||
|
umami:
|
||||||
|
image: ghcr.io/umami-software/umami:postgresql-latest
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@umami-db:5432/umami
|
||||||
|
DATABASE_TYPE: postgresql
|
||||||
|
APP_SECRET: ${UMAMI_APP_SECRET}
|
||||||
|
depends_on:
|
||||||
|
umami-db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.umami.rule=Host(`${UMAMI_DOMAIN:-stats.librodrome.org}`)"
|
||||||
|
- "traefik.http.routers.umami.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.umami.loadbalancer.server.port=3000"
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- traefik
|
||||||
|
|
||||||
|
umami-db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: umami
|
||||||
|
POSTGRES_USER: umami
|
||||||
|
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- umami-db-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U umami"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
umami-db-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-https://librodrome.org}
|
NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-https://librodrome.org}
|
||||||
NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD}
|
NUXT_ADMIN_PASSWORD: ${NUXT_ADMIN_PASSWORD}
|
||||||
NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET}
|
NUXT_ADMIN_SECRET: ${NUXT_ADMIN_SECRET}
|
||||||
|
ADMIN_GIT_SYNC: ${ADMIN_GIT_SYNC:-false}
|
||||||
ports:
|
ports:
|
||||||
- 3000
|
- 3000
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -16,6 +16,28 @@ export default defineNuxtConfig({
|
|||||||
'@nuxt/image',
|
'@nuxt/image',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
unocss: {
|
||||||
|
safelist: [
|
||||||
|
// Axis block icons (dynamic from YAML)
|
||||||
|
'i-lucide-monitor', 'i-lucide-coins', 'i-lucide-landmark',
|
||||||
|
'i-lucide-code-2', 'i-lucide-share-2', 'i-lucide-cloud',
|
||||||
|
'i-lucide-scale', 'i-lucide-gavel', 'i-lucide-users',
|
||||||
|
'i-lucide-droplets', 'i-lucide-calendar-heart',
|
||||||
|
// Action icons
|
||||||
|
'i-lucide-play', 'i-lucide-book-open', 'i-lucide-sparkles',
|
||||||
|
'i-lucide-heart-handshake', 'i-lucide-arrow-up-right',
|
||||||
|
'i-lucide-rocket', 'i-lucide-flask-conical', 'i-lucide-arrow-right',
|
||||||
|
// Decision page
|
||||||
|
'i-lucide-vote', 'i-lucide-scroll-text', 'i-lucide-git-branch',
|
||||||
|
// sweethomeCloud + territoire
|
||||||
|
'i-lucide-home', 'i-lucide-hard-drive', 'i-lucide-video',
|
||||||
|
'i-lucide-message-circle', 'i-lucide-bot', 'i-lucide-globe-2',
|
||||||
|
'i-lucide-map-pin', 'i-lucide-euro', 'i-lucide-package',
|
||||||
|
'i-lucide-settings', 'i-lucide-layout-grid', 'i-lucide-server',
|
||||||
|
'i-lucide-chevron-down', 'i-lucide-info',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
htmlAttrs: { lang: 'fr' },
|
htmlAttrs: { lang: 'fr' },
|
||||||
@@ -40,8 +62,21 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
adminPassword: '',
|
adminPassword: '',
|
||||||
adminSecret: '',
|
adminSecret: '',
|
||||||
|
umamiApiKey: '',
|
||||||
public: {
|
public: {
|
||||||
siteUrl: 'https://librodrome.org',
|
siteUrl: 'https://librodrome.org',
|
||||||
|
tenantId: 'librodrome',
|
||||||
|
umamiUrl: '',
|
||||||
|
umamiWebsiteId: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nitro: {
|
||||||
|
externals: {
|
||||||
|
traceInclude: [
|
||||||
|
'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build && node scripts/fix-esm-imports.mjs",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare && (test -f scripts/copy-pdfjs.sh && sh scripts/copy-pdfjs.sh || true)"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "^3.11.2",
|
"@nuxt/content": "^3.11.2",
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"@vueuse/nuxt": "^14.2.1",
|
"@vueuse/nuxt": "^14.2.1",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
|
"pdfjs-dist": "^5.4.624",
|
||||||
"vue": "^3.5.28",
|
"vue": "^3.5.28",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
"yaml": "^2.8.2"
|
"yaml": "^2.8.2"
|
||||||
|
|||||||
141
pnpm-lock.yaml
generated
141
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^4.3.1
|
specifier: ^4.3.1
|
||||||
version: 4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2)
|
version: 4.3.1(@parcel/watcher@2.5.6)(@types/node@25.2.3)(@vue/compiler-sfc@3.5.28)(better-sqlite3@12.6.2)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.6.2))(ioredis@5.9.3)(magicast@0.5.2)(rollup@4.57.1)(terser@5.46.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||||
|
pdfjs-dist:
|
||||||
|
specifier: ^5.4.624
|
||||||
|
version: 5.4.624
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.28
|
specifier: ^3.5.28
|
||||||
version: 3.5.28(typescript@5.9.3)
|
version: 3.5.28(typescript@5.9.3)
|
||||||
@@ -598,6 +601,81 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||||
|
resolution: {integrity: sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||||
|
resolution: {integrity: sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||||
|
resolution: {integrity: sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||||
|
resolution: {integrity: sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||||
|
resolution: {integrity: sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||||
|
resolution: {integrity: sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||||
|
resolution: {integrity: sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||||
|
resolution: {integrity: sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||||
|
resolution: {integrity: sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||||
|
resolution: {integrity: sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||||
|
resolution: {integrity: sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.95':
|
||||||
|
resolution: {integrity: sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.1':
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||||
|
|
||||||
@@ -3336,6 +3414,9 @@ packages:
|
|||||||
node-mock-http@1.0.4:
|
node-mock-http@1.0.4:
|
||||||
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
|
resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||||
|
|
||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
@@ -3496,6 +3577,10 @@ packages:
|
|||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
pdfjs-dist@5.4.624:
|
||||||
|
resolution: {integrity: sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==}
|
||||||
|
engines: {node: '>=20.16.0 || >=22.3.0'}
|
||||||
|
|
||||||
perfect-debounce@1.0.0:
|
perfect-debounce@1.0.0:
|
||||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||||
|
|
||||||
@@ -5214,6 +5299,54 @@ snapshots:
|
|||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@napi-rs/canvas@0.1.95':
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas-android-arm64': 0.1.95
|
||||||
|
'@napi-rs/canvas-darwin-arm64': 0.1.95
|
||||||
|
'@napi-rs/canvas-darwin-x64': 0.1.95
|
||||||
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.95
|
||||||
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.95
|
||||||
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.95
|
||||||
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.95
|
||||||
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.95
|
||||||
|
'@napi-rs/canvas-linux-x64-musl': 0.1.95
|
||||||
|
'@napi-rs/canvas-win32-arm64-msvc': 0.1.95
|
||||||
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.95
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.1':
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.8.1
|
'@emnapi/core': 1.8.1
|
||||||
@@ -8512,6 +8645,9 @@ snapshots:
|
|||||||
|
|
||||||
node-mock-http@1.0.4: {}
|
node-mock-http@1.0.4: {}
|
||||||
|
|
||||||
|
node-readable-to-web-readable-stream@0.4.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
nopt@8.1.0:
|
nopt@8.1.0:
|
||||||
@@ -8855,6 +8991,11 @@ snapshots:
|
|||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
|
pdfjs-dist@5.4.624:
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas': 0.1.95
|
||||||
|
node-readable-to-web-readable-stream: 0.4.2
|
||||||
|
|
||||||
perfect-debounce@1.0.0: {}
|
perfect-debounce@1.0.0: {}
|
||||||
|
|
||||||
perfect-debounce@2.1.0: {}
|
perfect-debounce@2.1.0: {}
|
||||||
|
|||||||
607
public/pdfjs/viewer.html
Normal file
607
public/pdfjs/viewer.html
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>PDF Viewer</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outline panel */
|
||||||
|
.outline-panel {
|
||||||
|
width: 260px;
|
||||||
|
min-width: 260px;
|
||||||
|
background: #111;
|
||||||
|
border-right: 1px solid #2a2a2a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.25s, min-width 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-panel.collapsed {
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-actions { display: flex; gap: 0.25rem; }
|
||||||
|
|
||||||
|
.outline-btn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #777;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-btn:hover { border-color: #555; color: #ccc; }
|
||||||
|
|
||||||
|
.outline-tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-tree::-webkit-scrollbar { width: 4px; }
|
||||||
|
.outline-tree::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
|
||||||
|
|
||||||
|
.outline-node { user-select: none; }
|
||||||
|
|
||||||
|
.outline-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.3rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-item:hover { background: #1e1e1e; color: #fff; }
|
||||||
|
.outline-item.active { background: #1a1207; color: #e8a040; }
|
||||||
|
|
||||||
|
.outline-item .toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 14px; height: 14px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 0.6rem; color: #555; cursor: pointer;
|
||||||
|
transition: transform 0.15s; margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-item .toggle.open { transform: rotate(90deg); }
|
||||||
|
.outline-item .toggle.empty { visibility: hidden; }
|
||||||
|
.outline-item .label { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
|
.outline-children { padding-left: 0.85rem; overflow: hidden; }
|
||||||
|
.outline-children.hidden { display: none; }
|
||||||
|
|
||||||
|
.outline-empty {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #444;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main area */
|
||||||
|
.main-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Book spread */
|
||||||
|
.book-spread {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #2a2a2a;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-spread canvas {
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
||||||
|
background: white;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slot.empty {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page turn transition */
|
||||||
|
.page-slot canvas {
|
||||||
|
transition: opacity 1s ease-out, transform 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slot canvas.entering {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(var(--enter-dir, 20px));
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slot canvas.leaving {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(var(--leave-dir, -20px));
|
||||||
|
position: absolute;
|
||||||
|
transition: opacity 1.2s ease-in, transform 1.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation bar */
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
background: #141414;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:hover:not(:disabled) { background: #2a2a2a; color: #fff; border-color: #555; }
|
||||||
|
.nav-btn:disabled { opacity: 0.25; cursor: default; }
|
||||||
|
|
||||||
|
.nav-info {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
min-width: 6rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0; bottom: 0;
|
||||||
|
width: 2rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn:hover { background: rgba(255,255,255,0.03); color: #aaa; }
|
||||||
|
.sidebar-btn.hidden { display: none; }
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.outline-panel { width: 200px; min-width: 200px; }
|
||||||
|
.book-spread { padding: 0.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.outline-panel { position: absolute; z-index: 20; height: 100%; }
|
||||||
|
.outline-panel.collapsed { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="viewer-layout">
|
||||||
|
<div class="outline-panel" id="outlinePanel">
|
||||||
|
<div class="outline-header">
|
||||||
|
<span class="outline-title">Sommaire</span>
|
||||||
|
<div class="outline-actions">
|
||||||
|
<button class="outline-btn" onclick="expandAll()">Déplier</button>
|
||||||
|
<button class="outline-btn" onclick="collapseAll()">Replier</button>
|
||||||
|
<button class="outline-btn" onclick="toggleSidebar()">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="outline-tree" id="outlineTree">
|
||||||
|
<div class="loading">Chargement…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-area">
|
||||||
|
<div class="book-spread" id="bookSpread">
|
||||||
|
<button class="sidebar-btn hidden" id="sidebarBtn" onclick="toggleSidebar()">☰</button>
|
||||||
|
<div class="loading" id="loadingMsg">Chargement du PDF…</div>
|
||||||
|
<div class="page-slot" id="slotLeft"></div>
|
||||||
|
<div class="page-slot" id="slotRight"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-bar">
|
||||||
|
<button class="nav-btn" id="prevBtn" onclick="prevSpread()" disabled>◀ Précédent</button>
|
||||||
|
<span class="nav-info" id="pageInfo"></span>
|
||||||
|
<button class="nav-btn" id="nextBtn" onclick="nextSpread()" disabled>Suivant ▶</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import * as pdfjsLib from '/pdfjs/pdf.min.mjs';
|
||||||
|
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.min.mjs';
|
||||||
|
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const hash = location.hash.slice(1);
|
||||||
|
const hashParams = new URLSearchParams(hash);
|
||||||
|
|
||||||
|
const fileUrl = params.get('file') || '/pdf/une-economie-du-don.pdf';
|
||||||
|
const targetPage = parseInt(hashParams.get('page') || params.get('page') || '1', 10);
|
||||||
|
|
||||||
|
let pdfDoc = null;
|
||||||
|
let currentSpread = 0; // index into spreads[]
|
||||||
|
let spreads = []; // [[1], [2,3], [4,5], ...] — page 1 alone (cover), then pairs
|
||||||
|
let pageCanvasCache = new Map();
|
||||||
|
let outlinePageMap = []; // [{item, pageNum}] for highlighting
|
||||||
|
let isAnimating = false;
|
||||||
|
|
||||||
|
function buildSpreads(numPages) {
|
||||||
|
spreads = [];
|
||||||
|
// Page 1 = couverture seule
|
||||||
|
spreads.push([1]);
|
||||||
|
// Ensuite par paires : 2-3, 4-5, etc.
|
||||||
|
for (let i = 2; i <= numPages; i += 2) {
|
||||||
|
if (i + 1 <= numPages) {
|
||||||
|
spreads.push([i, i + 1]);
|
||||||
|
} else {
|
||||||
|
spreads.push([i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpreadForPage(pageNum) {
|
||||||
|
return spreads.findIndex(s => s.includes(pageNum));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPageCanvas(pageNum) {
|
||||||
|
if (pageCanvasCache.has(pageNum)) return pageCanvasCache.get(pageNum);
|
||||||
|
|
||||||
|
const page = await pdfDoc.getPage(pageNum);
|
||||||
|
const spread = document.getElementById('bookSpread');
|
||||||
|
const availH = spread.clientHeight - 32;
|
||||||
|
const availW = (spread.clientWidth - 40) / 2;
|
||||||
|
|
||||||
|
const rawViewport = page.getViewport({ scale: 1 });
|
||||||
|
const scaleH = availH / rawViewport.height;
|
||||||
|
const scaleW = availW / rawViewport.width;
|
||||||
|
const scale = Math.min(scaleH, scaleW, 2.5);
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.dataset.page = pageNum;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||||
|
|
||||||
|
pageCanvasCache.set(pageNum, canvas);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showSpread(index, animate = true) {
|
||||||
|
if (index < 0 || index >= spreads.length) return;
|
||||||
|
if (isAnimating) return;
|
||||||
|
|
||||||
|
const prevIndex = currentSpread;
|
||||||
|
const direction = index > prevIndex ? 1 : -1; // 1 = forward, -1 = back
|
||||||
|
currentSpread = index;
|
||||||
|
|
||||||
|
const pages = spreads[index];
|
||||||
|
const slotLeft = document.getElementById('slotLeft');
|
||||||
|
const slotRight = document.getElementById('slotRight');
|
||||||
|
|
||||||
|
// Collect old canvases for fade-out
|
||||||
|
const oldCanvases = [...slotLeft.querySelectorAll('canvas'), ...slotRight.querySelectorAll('canvas')];
|
||||||
|
const shouldAnimate = animate && oldCanvases.length > 0;
|
||||||
|
|
||||||
|
if (shouldAnimate) {
|
||||||
|
isAnimating = true;
|
||||||
|
// Fade out old canvases with directional slide
|
||||||
|
oldCanvases.forEach(c => {
|
||||||
|
c.style.setProperty('--leave-dir', `${-direction * 20}px`);
|
||||||
|
c.classList.add('leaving');
|
||||||
|
});
|
||||||
|
// Wait for fade-out to mostly complete
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
slotLeft.innerHTML = '';
|
||||||
|
slotRight.innerHTML = '';
|
||||||
|
slotLeft.className = 'page-slot';
|
||||||
|
slotRight.className = 'page-slot';
|
||||||
|
|
||||||
|
if (pages.length === 1) {
|
||||||
|
const canvas = await renderPageCanvas(pages[0]);
|
||||||
|
if (shouldAnimate) {
|
||||||
|
canvas.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||||
|
canvas.classList.add('entering');
|
||||||
|
}
|
||||||
|
slotLeft.appendChild(canvas);
|
||||||
|
slotRight.className = 'page-slot empty';
|
||||||
|
if (shouldAnimate) {
|
||||||
|
// Double rAF ensures the browser has painted the initial state before transitioning
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => canvas.classList.remove('entering')));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const [left, right] = await Promise.all([
|
||||||
|
renderPageCanvas(pages[0]),
|
||||||
|
renderPageCanvas(pages[1]),
|
||||||
|
]);
|
||||||
|
if (shouldAnimate) {
|
||||||
|
left.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||||
|
right.style.setProperty('--enter-dir', `${direction * 20}px`);
|
||||||
|
left.classList.add('entering');
|
||||||
|
right.classList.add('entering');
|
||||||
|
}
|
||||||
|
slotLeft.appendChild(left);
|
||||||
|
slotRight.appendChild(right);
|
||||||
|
if (shouldAnimate) {
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||||
|
left.classList.remove('entering');
|
||||||
|
right.classList.remove('entering');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAnimate) setTimeout(() => { isAnimating = false; }, 1100);
|
||||||
|
else isAnimating = false;
|
||||||
|
|
||||||
|
updateNav();
|
||||||
|
highlightOutline();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNav() {
|
||||||
|
const pages = spreads[currentSpread];
|
||||||
|
const info = document.getElementById('pageInfo');
|
||||||
|
const prevBtn = document.getElementById('prevBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
|
||||||
|
if (pages.length === 1) {
|
||||||
|
info.textContent = `Page ${pages[0]} / ${pdfDoc.numPages}`;
|
||||||
|
} else {
|
||||||
|
info.textContent = `Pages ${pages[0]}–${pages[1]} / ${pdfDoc.numPages}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevBtn.disabled = currentSpread <= 0;
|
||||||
|
nextBtn.disabled = currentSpread >= spreads.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevSpread() { showSpread(currentSpread - 1); }
|
||||||
|
function nextSpread() { showSpread(currentSpread + 1); }
|
||||||
|
|
||||||
|
function goToPage(pageNum) {
|
||||||
|
const idx = getSpreadForPage(pageNum);
|
||||||
|
if (idx >= 0) showSpread(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
prevSpread();
|
||||||
|
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
nextSpread();
|
||||||
|
} else if (e.key === 'Home') {
|
||||||
|
e.preventDefault();
|
||||||
|
showSpread(0);
|
||||||
|
} else if (e.key === 'End') {
|
||||||
|
e.preventDefault();
|
||||||
|
showSpread(spreads.length - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resize handling
|
||||||
|
let resizeTimer;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
pageCanvasCache.clear();
|
||||||
|
showSpread(currentSpread, false);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Outline
|
||||||
|
async function renderOutline() {
|
||||||
|
const outlineTree = document.getElementById('outlineTree');
|
||||||
|
const outline = await pdfDoc.getOutline();
|
||||||
|
|
||||||
|
if (!outline || outline.length === 0) {
|
||||||
|
outlineTree.innerHTML = '<div class="outline-empty">Aucun signet dans ce PDF</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outlineTree.innerHTML = '';
|
||||||
|
outlinePageMap = [];
|
||||||
|
const tree = await buildOutlineTree(outline);
|
||||||
|
outlineTree.appendChild(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildOutlineTree(items) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
node.className = 'outline-node';
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'outline-item';
|
||||||
|
|
||||||
|
// Resolve page number for this item
|
||||||
|
let pageNum = null;
|
||||||
|
try {
|
||||||
|
let dest = item.dest;
|
||||||
|
if (typeof dest === 'string') dest = await pdfDoc.getDestination(dest);
|
||||||
|
if (dest) {
|
||||||
|
const ref = dest[0];
|
||||||
|
pageNum = (await pdfDoc.getPageIndex(ref)) + 1;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const toggle = document.createElement('span');
|
||||||
|
toggle.className = 'toggle' + (item.items && item.items.length > 0 ? ' open' : ' empty');
|
||||||
|
toggle.textContent = '▶';
|
||||||
|
if (item.items && item.items.length > 0) {
|
||||||
|
toggle.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const children = node.querySelector('.outline-children');
|
||||||
|
if (children) {
|
||||||
|
const isHidden = children.classList.toggle('hidden');
|
||||||
|
toggle.classList.toggle('open', !isHidden);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
row.appendChild(toggle);
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'label';
|
||||||
|
label.textContent = item.title;
|
||||||
|
row.appendChild(label);
|
||||||
|
|
||||||
|
if (pageNum !== null) {
|
||||||
|
const pg = pageNum;
|
||||||
|
row.addEventListener('click', () => goToPage(pg));
|
||||||
|
outlinePageMap.push({ row, pageNum: pg });
|
||||||
|
}
|
||||||
|
|
||||||
|
node.appendChild(row);
|
||||||
|
|
||||||
|
if (item.items && item.items.length > 0) {
|
||||||
|
const children = document.createElement('div');
|
||||||
|
children.className = 'outline-children';
|
||||||
|
children.appendChild(await buildOutlineTree(item.items));
|
||||||
|
node.appendChild(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
frag.appendChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightOutline() {
|
||||||
|
const pages = spreads[currentSpread];
|
||||||
|
for (const { row, pageNum } of outlinePageMap) {
|
||||||
|
row.classList.toggle('active', pages.includes(pageNum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load
|
||||||
|
async function init() {
|
||||||
|
const loadingTask = pdfjsLib.getDocument(fileUrl);
|
||||||
|
pdfDoc = await loadingTask.promise;
|
||||||
|
|
||||||
|
buildSpreads(pdfDoc.numPages);
|
||||||
|
await renderOutline();
|
||||||
|
|
||||||
|
document.getElementById('loadingMsg').remove();
|
||||||
|
|
||||||
|
const startSpread = getSpreadForPage(targetPage);
|
||||||
|
await showSpread(startSpread >= 0 ? startSpread : 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
init().catch(err => {
|
||||||
|
document.getElementById('loadingMsg').textContent = `Erreur : ${err.message}`;
|
||||||
|
document.getElementById('loadingMsg').style.color = '#c44';
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose globals
|
||||||
|
window.prevSpread = prevSpread;
|
||||||
|
window.nextSpread = nextSpread;
|
||||||
|
|
||||||
|
window.expandAll = function() {
|
||||||
|
const tree = document.getElementById('outlineTree');
|
||||||
|
tree.querySelectorAll('.outline-children.hidden').forEach(el => el.classList.remove('hidden'));
|
||||||
|
tree.querySelectorAll('.toggle:not(.empty)').forEach(el => el.classList.add('open'));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.collapseAll = function() {
|
||||||
|
const tree = document.getElementById('outlineTree');
|
||||||
|
tree.querySelectorAll('.outline-children').forEach(el => el.classList.add('hidden'));
|
||||||
|
tree.querySelectorAll('.toggle:not(.empty)').forEach(el => el.classList.remove('open'));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleSidebar = function() {
|
||||||
|
const panel = document.getElementById('outlinePanel');
|
||||||
|
const btn = document.getElementById('sidebarBtn');
|
||||||
|
panel.classList.toggle('collapsed');
|
||||||
|
btn.classList.toggle('hidden', !panel.classList.contains('collapsed'));
|
||||||
|
// Recalc on sidebar toggle
|
||||||
|
setTimeout(() => {
|
||||||
|
pageCanvasCache.clear();
|
||||||
|
showSpread(currentSpread, false);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
scripts/copy-pdfjs.sh
Executable file
12
scripts/copy-pdfjs.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Copie les fichiers PDF.js nécessaires depuis node_modules vers public/pdfjs/
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEST="public/pdfjs"
|
||||||
|
SRC="node_modules/pdfjs-dist/build"
|
||||||
|
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
cp "$SRC/pdf.min.mjs" "$DEST/pdf.min.mjs"
|
||||||
|
cp "$SRC/pdf.worker.min.mjs" "$DEST/pdf.worker.min.mjs"
|
||||||
|
|
||||||
|
echo "[copy-pdfjs] OK"
|
||||||
35
scripts/fix-esm-imports.mjs
Normal file
35
scripts/fix-esm-imports.mjs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Post-build fix: Pinia CJS (pinia.prod.cjs) fait require('vue').
|
||||||
|
* Rollup convertit ça en `import require$$0 from 'vue'` (default import).
|
||||||
|
* Vue 3 n'a pas de default export en ESM → crash Node 22+.
|
||||||
|
*
|
||||||
|
* Ce script remplace le default import par un namespace import valide.
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
||||||
|
|
||||||
|
const file = '.output/server/chunks/build/server.mjs'
|
||||||
|
|
||||||
|
if (!existsSync(file)) {
|
||||||
|
console.log('[fix-esm] server.mjs not found, skipping')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = readFileSync(file, 'utf-8')
|
||||||
|
|
||||||
|
// Pattern: import require$$0, { defineComponent, inject, ... } from 'vue'
|
||||||
|
// → import * as require$$0 from 'vue'; import { defineComponent, ... } from 'vue'
|
||||||
|
const re = /import\s+(require\$\$\d+)\s*,\s*\{([^}]+)\}\s*from\s*'vue'/g
|
||||||
|
let patched = false
|
||||||
|
|
||||||
|
code = code.replace(re, (match, varName, namedStr) => {
|
||||||
|
patched = true
|
||||||
|
const names = namedStr.split(',').map(n => n.trim()).filter(Boolean)
|
||||||
|
return `import * as ${varName} from 'vue'; import { ${names.join(', ')} } from 'vue'`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (patched) {
|
||||||
|
writeFileSync(file, code)
|
||||||
|
console.log('[fix-esm] Patched default import from vue in server.mjs')
|
||||||
|
} else {
|
||||||
|
console.log('[fix-esm] No default import found, nothing to patch')
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const filePath = join(process.cwd(), 'content', 'book', `${slug}.md`)
|
const filePath = join(process.cwd(), 'content', 'book', `${slug}.md`)
|
||||||
|
|
||||||
await unlink(filePath)
|
await unlink(filePath)
|
||||||
|
gitSyncContent(`Suppression chapitre ${slug}`, [`content/book/${slug}.md`])
|
||||||
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const content = `---\n${body.frontmatter.trim()}\n---\n${body.body}`
|
const content = `---\n${body.frontmatter.trim()}\n---\n${body.body}`
|
||||||
|
|
||||||
await writeFile(filePath, content, 'utf-8')
|
await writeFile(filePath, content, 'utf-8')
|
||||||
|
gitSyncContent(`Mise à jour chapitre ${slug}`, [`content/book/${slug}.md`])
|
||||||
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ readingTime: "5 min"
|
|||||||
`
|
`
|
||||||
|
|
||||||
await writeFile(filePath, content, 'utf-8')
|
await writeFile(filePath, content, 'utf-8')
|
||||||
|
gitSyncContent(`Nouveau chapitre ${body.slug}`, [`content/book/${body.slug}.md`])
|
||||||
|
|
||||||
return { ok: true, slug: body.slug }
|
return { ok: true, slug: body.slug }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,5 +25,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
gitSyncContent('Réorganisation chapitres', ['content/book/'])
|
||||||
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
await writeYaml('bookplayer.config.yml', body)
|
await writeYaml('bookplayer.config.yml', body)
|
||||||
|
gitSyncContent('Mise à jour config livre/player', ['site/bookplayer.config.yml'])
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
39
server/api/admin/content/pages.get.ts
Normal file
39
server/api/admin/content/pages.get.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { readdir, stat } from 'node:fs/promises'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
interface PageEntry {
|
||||||
|
path: string
|
||||||
|
label: string
|
||||||
|
section?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listYamlFiles(dir: string, prefix = ''): Promise<PageEntry[]> {
|
||||||
|
const entries: PageEntry[] = []
|
||||||
|
const items = await readdir(dir)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = join(dir, item)
|
||||||
|
const s = await stat(fullPath)
|
||||||
|
|
||||||
|
if (s.isDirectory()) {
|
||||||
|
const subEntries = await listYamlFiles(fullPath, prefix ? `${prefix}/${item}` : item)
|
||||||
|
entries.push(...subEntries)
|
||||||
|
}
|
||||||
|
else if (item.endsWith('.yml')) {
|
||||||
|
const name = item.replace('.yml', '')
|
||||||
|
const path = prefix ? `${prefix}/${name}` : name
|
||||||
|
entries.push({
|
||||||
|
path,
|
||||||
|
label: name,
|
||||||
|
section: prefix || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
const pagesDir = join(process.cwd(), 'site', 'pages')
|
||||||
|
return await listYamlFiles(pagesDir)
|
||||||
|
})
|
||||||
21
server/api/admin/content/pages/[...path].put.ts
Normal file
21
server/api/admin/content/pages/[...path].put.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { mkdir } from 'node:fs/promises'
|
||||||
|
import { join, dirname } from 'node:path'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const path = getRouterParam(event, 'path')
|
||||||
|
|
||||||
|
if (!path || !/^[a-z0-9-/]+$/.test(path)) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid page path' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
const relativePath = `pages/${path}.yml`
|
||||||
|
|
||||||
|
// Ensure subdirectory exists
|
||||||
|
const fullPath = join(process.cwd(), 'site', relativePath)
|
||||||
|
await mkdir(dirname(fullPath), { recursive: true })
|
||||||
|
|
||||||
|
await writeYaml(relativePath, body)
|
||||||
|
gitSyncContent(`Mise à jour page ${path}`, [`site/${relativePath}`])
|
||||||
|
return { ok: true }
|
||||||
|
})
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const name = getRouterParam(event, 'name')
|
|
||||||
|
|
||||||
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Invalid page name' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await readBody(event)
|
|
||||||
await writeYaml(`pages/${name}.yml`, body)
|
|
||||||
return { ok: true }
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
await writeYaml('site.yml', body)
|
await writeYaml('site.yml', body)
|
||||||
|
gitSyncContent('Mise à jour config site', ['site/site.yml'])
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await unlink(filePath)
|
await unlink(filePath)
|
||||||
|
gitSyncContent(`Suppression média ${path}`, [`public/${path}`])
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@@ -33,5 +33,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
uploaded.push(`/${subdir}/${safeName}`)
|
uploaded.push(`/${subdir}/${safeName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uploaded.length > 0) {
|
||||||
|
gitSyncContent(`Upload média : ${uploaded.join(', ')}`, uploaded.map(f => `public${f}`))
|
||||||
|
}
|
||||||
|
|
||||||
return { ok: true, files: uploaded }
|
return { ok: true, files: uploaded }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
data.messages.splice(index, 1)
|
data.messages.splice(index, 1)
|
||||||
await writeYaml('messages.yml', data)
|
await writeYaml('messages.yml', data)
|
||||||
|
gitSyncContent(`Suppression message #${id}`, ['site/messages.yml'])
|
||||||
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (body.author !== undefined) message.author = body.author
|
if (body.author !== undefined) message.author = body.author
|
||||||
|
|
||||||
await writeYaml('messages.yml', data)
|
await writeYaml('messages.yml', data)
|
||||||
|
gitSyncContent(`Mise à jour message #${id}`, ['site/messages.yml'])
|
||||||
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
})
|
})
|
||||||
|
|||||||
86
server/api/admin/pdf-outline.get.ts
Normal file
86
server/api/admin/pdf-outline.get.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { join } from 'node:path'
|
||||||
|
import { readFileSync, existsSync } from 'node:fs'
|
||||||
|
|
||||||
|
// Polyfills nécessaires pour pdfjs-dist en Node.js pur (pas de DOM)
|
||||||
|
// On n'a besoin que du parsing, pas du rendu
|
||||||
|
if (typeof globalThis.DOMMatrix === 'undefined') {
|
||||||
|
// @ts-expect-error polyfill minimal pour pdfjs
|
||||||
|
globalThis.DOMMatrix = class DOMMatrix {
|
||||||
|
constructor() { return Object.assign(this, { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }) }
|
||||||
|
isIdentity = true
|
||||||
|
translate() { return new DOMMatrix() }
|
||||||
|
scale() { return new DOMMatrix() }
|
||||||
|
inverse() { return new DOMMatrix() }
|
||||||
|
multiply() { return new DOMMatrix() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof globalThis.Path2D === 'undefined') {
|
||||||
|
// @ts-expect-error polyfill stub
|
||||||
|
globalThis.Path2D = class Path2D { constructor() {} }
|
||||||
|
}
|
||||||
|
if (typeof globalThis.ImageData === 'undefined') {
|
||||||
|
// @ts-expect-error polyfill stub
|
||||||
|
globalThis.ImageData = class ImageData { constructor(w: number, h: number) { this.width = w; this.height = h; this.data = new Uint8ClampedArray(w * h * 4) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
const config = await readYaml('bookplayer.config.yml')
|
||||||
|
const pdfFile = config?.book?.pdfFile || '/pdf/une-economie-du-don.pdf'
|
||||||
|
|
||||||
|
// Résolution du chemin PDF : dev (public/) et prod (.output/public/)
|
||||||
|
const cwd = process.cwd()
|
||||||
|
const candidates = [
|
||||||
|
join(cwd, 'public', pdfFile),
|
||||||
|
join(cwd, '.output', 'public', pdfFile),
|
||||||
|
]
|
||||||
|
const pdfPath = candidates.find(p => existsSync(p))
|
||||||
|
|
||||||
|
if (!pdfPath) {
|
||||||
|
console.warn('[pdf-outline] PDF non trouvé. cwd:', cwd, 'candidats:', candidates)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Uint8Array
|
||||||
|
try {
|
||||||
|
data = new Uint8Array(readFileSync(pdfPath))
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[pdf-outline] Erreur lecture PDF:', err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfjsLib = await import('pdfjs-dist/legacy/build/pdf.mjs')
|
||||||
|
|
||||||
|
let doc
|
||||||
|
try {
|
||||||
|
doc = await pdfjsLib.getDocument({ data, useSystemFonts: true }).promise
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[pdf-outline] Erreur getDocument:', err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const outline = await doc.getOutline()
|
||||||
|
|
||||||
|
if (!outline || outline.length === 0) {
|
||||||
|
doc.destroy()
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: Array<{ title: string; page: number; level: number }> = []
|
||||||
|
|
||||||
|
async function extract(items: any[], level: number) {
|
||||||
|
for (const item of items) {
|
||||||
|
let page: number | null = null
|
||||||
|
try {
|
||||||
|
let dest = item.dest
|
||||||
|
if (typeof dest === 'string') dest = await doc.getDestination(dest)
|
||||||
|
if (dest) page = (await doc.getPageIndex(dest[0])) + 1
|
||||||
|
} catch {}
|
||||||
|
if (page !== null) entries.push({ title: item.title, page, level })
|
||||||
|
if (item.items?.length) await extract(item.items, level + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await extract(outline, 0)
|
||||||
|
doc.destroy()
|
||||||
|
return entries
|
||||||
|
})
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const name = getRouterParam(event, 'name')
|
const path = getRouterParam(event, 'path')
|
||||||
|
|
||||||
if (!name || !/^[a-z0-9-]+$/.test(name)) {
|
if (!path || !/^[a-z0-9-/]+$/.test(path)) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Invalid page name' })
|
throw createError({ statusCode: 400, statusMessage: 'Invalid page path' })
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await readYaml(`pages/${name}.yml`)
|
return await readYaml(`pages/${path}.yml`)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
throw createError({ statusCode: 404, statusMessage: `Page "${name}" not found` })
|
throw createError({ statusCode: 404, statusMessage: `Page "${path}" not found` })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody<{ author: string; email?: string; text: string }>(event)
|
const body = await readBody<{ author: string; email?: string; text: string }>(event)
|
||||||
|
|
||||||
if (!body.author?.trim() || !body.text?.trim()) {
|
if (!body.text?.trim()) {
|
||||||
throw createError({ statusCode: 400, statusMessage: 'Nom et message requis' })
|
throw createError({ statusCode: 400, statusMessage: 'Message requis' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await readYaml<{ messages: any[] }>('messages.yml')
|
const data = await readYaml<{ messages: any[] }>('messages.yml')
|
||||||
|
|||||||
43
server/api/stats/index.get.ts
Normal file
43
server/api/stats/index.get.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* GET /api/stats
|
||||||
|
* Public stats endpoint — proxies Umami for cross-instance federation / observatoires.
|
||||||
|
* Each librodrome instance exposes its own metrics here.
|
||||||
|
* Observatoires call this endpoint on each instance and aggregate.
|
||||||
|
*
|
||||||
|
* Env vars required (private, server-side):
|
||||||
|
* NUXT_UMAMI_API_KEY — Umami API key (read-only)
|
||||||
|
* NUXT_UMAMI_WEBSITE_ID — Umami website ID (internal, server-side)
|
||||||
|
* NUXT_PUBLIC_UMAMI_URL — Umami base URL
|
||||||
|
* NUXT_PUBLIC_TENANT_ID — e.g. "librodrome" or "librodrome-bordeaux"
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const { umamiApiKey } = config
|
||||||
|
const { umamiUrl, umamiWebsiteId, tenantId } = config.public
|
||||||
|
|
||||||
|
if (!umamiApiKey || !umamiUrl || !umamiWebsiteId) {
|
||||||
|
return { tenant: tenantId, configured: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const endAt = Date.now()
|
||||||
|
const startAt = endAt - 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||||
|
|
||||||
|
const [stats, pageviews] = await Promise.all([
|
||||||
|
$fetch<Record<string, unknown>>(`${umamiUrl}/api/websites/${umamiWebsiteId}/stats`, {
|
||||||
|
headers: { 'x-umami-api-key': umamiApiKey },
|
||||||
|
query: { startAt, endAt },
|
||||||
|
}).catch(() => null),
|
||||||
|
$fetch<Record<string, unknown>>(`${umamiUrl}/api/websites/${umamiWebsiteId}/pageviews`, {
|
||||||
|
headers: { 'x-umami-api-key': umamiApiKey },
|
||||||
|
query: { startAt, endAt, unit: 'day', timezone: 'Europe/Paris' },
|
||||||
|
}).catch(() => null),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenant: tenantId,
|
||||||
|
configured: true,
|
||||||
|
period: '30d',
|
||||||
|
stats,
|
||||||
|
pageviews,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,13 +1,45 @@
|
|||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler((event) => {
|
||||||
const path = getRequestURL(event).pathname
|
const path = getRequestURL(event).pathname
|
||||||
|
|
||||||
|
// /lire → /economique/modele-eco
|
||||||
if (path.startsWith('/lire')) {
|
if (path.startsWith('/lire')) {
|
||||||
const rest = path.slice(5) // remove '/lire'
|
const rest = path.slice(5) // remove '/lire'
|
||||||
return sendRedirect(event, `/modele-eco${rest || '/'}`, 301)
|
return sendRedirect(event, `/economique/modele-eco${rest || '/'}`, 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.startsWith('/ecouter')) {
|
if (path.startsWith('/ecouter')) {
|
||||||
const rest = path.slice(8) // remove '/ecouter'
|
const rest = path.slice(8) // remove '/ecouter'
|
||||||
return sendRedirect(event, `/en-musique${rest || '/'}`, 301)
|
return sendRedirect(event, `/en-musique${rest || '/'}`, 301)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (path === '/autonomie' || path === '/autonomie/') {
|
||||||
|
return sendRedirect(event, '/numerique', 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/decision' || path === '/decision/') {
|
||||||
|
return sendRedirect(event, '/citoyenne/decision', 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /modele-eco → /economique/modele-eco
|
||||||
|
if (path.startsWith('/modele-eco')) {
|
||||||
|
const rest = path.slice(11) // remove '/modele-eco'
|
||||||
|
return sendRedirect(event, `/economique/modele-eco${rest || '/'}`, 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect old /gestation/* routes to proper sections
|
||||||
|
if (path.startsWith('/gestation/')) {
|
||||||
|
const slug = path.slice(11).replace(/\/$/, '')
|
||||||
|
const numeriquePages = ['logiciel-libre', 'authentification-wot', 'cloud-libre']
|
||||||
|
if (numeriquePages.includes(slug)) {
|
||||||
|
return sendRedirect(event, `/numerique/${slug}`, 301)
|
||||||
|
}
|
||||||
|
if (slug === 'productions-collectives') {
|
||||||
|
return sendRedirect(event, '/economique/productions-collectives', 301)
|
||||||
|
}
|
||||||
|
if (slug === 'tarifs-eau') {
|
||||||
|
return sendRedirect(event, '/citoyenne/tarifs-eau', 301)
|
||||||
|
}
|
||||||
|
// Fallback
|
||||||
|
return sendRedirect(event, '/', 301)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
44
server/utils/gitSync.ts
Normal file
44
server/utils/gitSync.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { execFile } from 'node:child_process'
|
||||||
|
import { promisify } from 'node:util'
|
||||||
|
|
||||||
|
const exec = promisify(execFile)
|
||||||
|
|
||||||
|
const enabled = process.env.ADMIN_GIT_SYNC === 'true'
|
||||||
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
async function git(...args: string[]): Promise<string> {
|
||||||
|
const { stdout } = await exec('git', args, { cwd })
|
||||||
|
return stdout.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit et push les modifications admin vers le dépôt git.
|
||||||
|
* Activé uniquement si ADMIN_GIT_SYNC=true (prod).
|
||||||
|
* Ne bloque jamais la réponse HTTP — exécution fire-and-forget.
|
||||||
|
*/
|
||||||
|
export function gitSyncContent(description: string, paths: string[] = ['.']) {
|
||||||
|
if (!enabled) return
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
try {
|
||||||
|
for (const p of paths) {
|
||||||
|
await git('add', p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier s'il y a vraiment des changements stagés
|
||||||
|
const status = await git('diff', '--cached', '--name-only')
|
||||||
|
if (!status) return
|
||||||
|
|
||||||
|
await git('commit', '-m', `[admin] ${description}`)
|
||||||
|
await git('push')
|
||||||
|
|
||||||
|
console.log(`[gitSync] pushed: ${description}`)
|
||||||
|
}
|
||||||
|
catch (err: any) {
|
||||||
|
console.error(`[gitSync] error:`, err.message ?? err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire-and-forget : ne pas bloquer la réponse HTTP
|
||||||
|
run()
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
book:
|
book:
|
||||||
title: Une économie du don — enfin concevable
|
title: Une économie du don — enfin concevable
|
||||||
author: Yvv
|
author: Yvv
|
||||||
|
pdfFile: /pdf/une-economie-du-don.pdf
|
||||||
description: Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don.
|
description: Un livre et 9 chansons pour explorer ensemble les fondements d'une économie fondée sur le don.
|
||||||
coverImage: /images/book-cover.jpg
|
coverImage: /images/book-cover.jpg
|
||||||
license: CC-BY-NC
|
license: CC-BY-NC
|
||||||
@@ -560,6 +561,29 @@ chapterSongs:
|
|||||||
- chapterSlug: 11-annexes
|
- chapterSlug: 11-annexes
|
||||||
songId: coder-la-liberte
|
songId: coder-la-liberte
|
||||||
primary: true
|
primary: true
|
||||||
|
chapterPages:
|
||||||
|
- chapterSlug: 01-introduction
|
||||||
|
page: 9
|
||||||
|
- chapterSlug: 02-don
|
||||||
|
page: 23
|
||||||
|
- chapterSlug: 03-mesure
|
||||||
|
page: 43
|
||||||
|
- chapterSlug: 04-monnaie
|
||||||
|
page: 49
|
||||||
|
- chapterSlug: 05-trm
|
||||||
|
page: 83
|
||||||
|
- chapterSlug: 06-economie
|
||||||
|
page: 121
|
||||||
|
- chapterSlug: 07-echange
|
||||||
|
page: 147
|
||||||
|
- chapterSlug: 08-institution
|
||||||
|
page: 163
|
||||||
|
- chapterSlug: 09-greffes
|
||||||
|
page: 181
|
||||||
|
- chapterSlug: 10-maintenant
|
||||||
|
page: 193
|
||||||
|
- chapterSlug: 11-annexes
|
||||||
|
page: 199
|
||||||
defaultPlaylistOrder:
|
defaultPlaylistOrder:
|
||||||
- ce-livre-est-une-facon
|
- ce-livre-est-une-facon
|
||||||
- de-quel-don-nous-parlons
|
- de-quel-don-nous-parlons
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
kicker: Pourquoi l'autonomie
|
|
||||||
title: Autonomie
|
|
||||||
description: Des passages du livre qui éclairent la démarche d'autonomie collective — le fil rouge du projet.
|
|
||||||
meta:
|
|
||||||
title: Autonomie
|
|
||||||
extracts:
|
|
||||||
- chapter: Introduction
|
|
||||||
chapterSlug: 01-introduction
|
|
||||||
text: >
|
|
||||||
Ben. Pour l'autonomie.
|
|
||||||
...C'est tout — sauf un repli.
|
|
||||||
Balkanisation ? que nenni !
|
|
||||||
Réfuter l'autonomie... c'est fallacieux,
|
|
||||||
c'est nous bannir en tant qu'adultes, bien vivants,
|
|
||||||
restez là sans mot dire,
|
|
||||||
"restez des enfants !"...
|
|
||||||
mmh, suspect et sans avenir.
|
|
||||||
- chapter: Introduction
|
|
||||||
chapterSlug: 01-introduction
|
|
||||||
text: >
|
|
||||||
Ne plus subir les agendas. Créer les nôtr'.
|
|
||||||
On manque de repères ?... Entre autres, ...
|
|
||||||
Faut les trouver,...
|
|
||||||
En produisant, les inventer.
|
|
||||||
- chapter: "Raison d'être d'une monnaie"
|
|
||||||
chapterSlug: 04-monnaie
|
|
||||||
text: >
|
|
||||||
Même accès pour tous.
|
|
||||||
Même pouvoir de création.
|
|
||||||
Ce n'est plus "Que la dette soit".
|
|
||||||
C'est "Que l'équilibre soit".
|
|
||||||
- chapter: "Créer une économie ?"
|
|
||||||
chapterSlug: 06-economie
|
|
||||||
text: >
|
|
||||||
Le D.U, c'est une mesure.
|
|
||||||
Pour ne plus obliger. Pour ne plus devoir.
|
|
||||||
Je donne à mon économie, j'alimente le réservoir.
|
|
||||||
Je compte sur les autres, sur mon économie,
|
|
||||||
Pour y trouver ma pleine mesure.
|
|
||||||
- chapter: "Et maintenant ?… action ?"
|
|
||||||
chapterSlug: 10-maintenant
|
|
||||||
text: >
|
|
||||||
Mais la monnaie n'est pas la richesse.
|
|
||||||
C'est juste le mètre... pas le tissu.
|
|
||||||
C'est le baromètre... pas le climat.
|
|
||||||
Ne confondons pas la carte et le territoire.
|
|
||||||
22
site/pages/citoyenne.yml
Normal file
22
site/pages/citoyenne.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
meta:
|
||||||
|
title: Autonomie citoyenne
|
||||||
|
|
||||||
|
kicker: Autonomie citoyenne
|
||||||
|
title: La décision
|
||||||
|
description: >
|
||||||
|
Se donner les moyens de la décision collective. Reprendre la main
|
||||||
|
sur les processus qui structurent notre vie commune — à l'échelle
|
||||||
|
des bassins de vie.
|
||||||
|
|
||||||
|
items:
|
||||||
|
- id: decision
|
||||||
|
label: Décision collective
|
||||||
|
icon: gavel
|
||||||
|
description: Se donner les moyens de la décision collective.
|
||||||
|
to: /citoyenne/decision
|
||||||
|
- id: tarifs-eau
|
||||||
|
label: Tarifs de l'eau
|
||||||
|
icon: droplets
|
||||||
|
description: Application pour obtenir justice sociale et incitation dynamique à la réduction. Permet de confier la décision à la population des communes.
|
||||||
|
to: /citoyenne/tarifs-eau
|
||||||
|
gestation: true
|
||||||
21
site/pages/citoyenne/decision.yml
Normal file
21
site/pages/citoyenne/decision.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
meta:
|
||||||
|
title: Décision collective — Autonomie citoyenne
|
||||||
|
|
||||||
|
kicker: Autonomie citoyenne
|
||||||
|
title: Plateforme Décision
|
||||||
|
icon: gavel
|
||||||
|
description: Se donner les moyens de la décision collective.
|
||||||
|
|
||||||
|
features:
|
||||||
|
- icon: vote
|
||||||
|
title: Décisions on-chain
|
||||||
|
text: Des décisions transparentes et vérifiables, inscrites sur la blockchain.
|
||||||
|
- icon: scroll-text
|
||||||
|
title: Les Mandats
|
||||||
|
text: Formaliser et suivre les mandats confiés aux personnes désignées.
|
||||||
|
- icon: scroll-text
|
||||||
|
title: Documents de référence
|
||||||
|
text: Les textes fondateurs et documents qui encadrent la prise de décision.
|
||||||
|
- icon: git-branch
|
||||||
|
title: Les Protocoles
|
||||||
|
text: Les règles et processus qui structurent la décision collective.
|
||||||
20
site/pages/citoyenne/tarifs-eau.yml
Normal file
20
site/pages/citoyenne/tarifs-eau.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
meta:
|
||||||
|
title: Tarifs de l'eau — Autonomie citoyenne
|
||||||
|
|
||||||
|
kicker: Autonomie citoyenne
|
||||||
|
title: Tarifs de l'eau
|
||||||
|
icon: droplets
|
||||||
|
gestation: true
|
||||||
|
|
||||||
|
description: >
|
||||||
|
Application pour obtenir justice sociale et incitation dynamique
|
||||||
|
à la réduction. Permet de confier la décision à la population des communes.
|
||||||
|
|
||||||
|
project:
|
||||||
|
name: SejeteralO
|
||||||
|
text: Simulateur de tarification de l'eau — justice sociale et écologie.
|
||||||
|
|
||||||
|
content: >
|
||||||
|
La tarification de l'eau est un levier essentiel de justice sociale
|
||||||
|
et d'incitation à la réduction de la consommation. SejeteralO permet
|
||||||
|
aux communes de simuler et comparer différentes grilles tarifaires.
|
||||||
29
site/pages/economique.yml
Normal file
29
site/pages/economique.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
meta:
|
||||||
|
title: Autonomie économique
|
||||||
|
|
||||||
|
kicker: Autonomie économique
|
||||||
|
title: La création monétaire
|
||||||
|
description: >
|
||||||
|
Pour fonctionner autrement, il faut créer une économie qui le permette.
|
||||||
|
Au fondement de toute économie se trouve la création monétaire — son code,
|
||||||
|
ses règles, ses bénéficiaires. Tant que ce code est écrit par d'autres,
|
||||||
|
pour d'autres, aucune autonomie réelle n'est possible.
|
||||||
|
La monnaie libre propose un autre code : symétrique, transparent, partagé.
|
||||||
|
|
||||||
|
items:
|
||||||
|
- id: monnaie-libre
|
||||||
|
label: Monnaie libre
|
||||||
|
icon: g1
|
||||||
|
description: "La Ğ1 (June) : chaque membre crée la même part de monnaie, chaque jour. Sans dette ni intérêt — le dividende universel."
|
||||||
|
to: /economique/monnaie-libre
|
||||||
|
- id: modele-eco
|
||||||
|
label: Économie du don
|
||||||
|
icon: scale
|
||||||
|
description: Un livre et des chansons pour une proposition de modèle économique fondé sur le don.
|
||||||
|
to: /economique/modele-eco
|
||||||
|
- id: productions-collectives
|
||||||
|
label: Productions collectives
|
||||||
|
icon: users
|
||||||
|
description: Une plateforme pour faciliter la création d'équipes et la réalisation de productions à l'échelle des bassins de vie.
|
||||||
|
to: /economique/productions-collectives
|
||||||
|
gestation: true
|
||||||
23
site/pages/economique/commande.yml
Normal file
23
site/pages/economique/commande.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
meta:
|
||||||
|
title: Commandez le livre papier
|
||||||
|
|
||||||
|
kicker: Le livre
|
||||||
|
title: Commandez le livre papier
|
||||||
|
icon: shopping-bag
|
||||||
|
|
||||||
|
description: >
|
||||||
|
« Une économie du don — enfin concevable » est disponible en version papier.
|
||||||
|
Vous pouvez le commander en ligne ou le demander en librairie.
|
||||||
|
|
||||||
|
bookelis:
|
||||||
|
label: Commander sur Bookelis
|
||||||
|
url: https://www.bookelis.com/economie/56818-Une-economie-du-don-enfin-concevable.html
|
||||||
|
|
||||||
|
librairie:
|
||||||
|
title: En librairie
|
||||||
|
text: >
|
||||||
|
Le livre est disponible à la commande dans toute librairie jusqu'au printemps 2027.
|
||||||
|
Cette disponibilité dépend d'un abonnement de référencement Hachette,
|
||||||
|
qui permet aux libraires de le trouver dans leur catalogue et de le commander
|
||||||
|
auprès de leur distributeur. Au-delà de cette date, la commande en ligne
|
||||||
|
sur Bookelis reste disponible.
|
||||||
59
site/pages/economique/monnaie-libre.yml
Normal file
59
site/pages/economique/monnaie-libre.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
meta:
|
||||||
|
title: Monnaie libre — Autonomie économique
|
||||||
|
|
||||||
|
kicker: Autonomie économique
|
||||||
|
title: Monnaie libre
|
||||||
|
icon: g1
|
||||||
|
|
||||||
|
description: >
|
||||||
|
Toute économie repose sur un code monétaire.
|
||||||
|
Le nôtre programme la cavalerie et la machine à faillites.
|
||||||
|
La monnaie libre recode : chaque membre crée la même part relative de monnaie,
|
||||||
|
chaque jour. Symétrique dans l'espace, symétrique dans le temps.
|
||||||
|
|
||||||
|
content: >
|
||||||
|
La Théorie Relative de la Monnaie (TRM), formulée par Stéphane Laborde, pose
|
||||||
|
la question autrement : à quelle condition une monnaie ne privilégie-t-elle
|
||||||
|
personne dans sa création ? Ni aucun individu parmi ses contemporains,
|
||||||
|
ni aucune génération par rapport aux suivantes ?
|
||||||
|
|
||||||
|
La réponse est la double symétrie.
|
||||||
|
Symétrie spatiale : chaque membre crée la même part de monnaie que ses voisins.
|
||||||
|
Symétrie temporelle : les membres d'aujourd'hui ne privent pas ceux de demain.
|
||||||
|
Le Dividende Universel (DU) est la traduction pratique de ces deux symétries —
|
||||||
|
une part égale de création monétaire, pour chaque membre certifié, chaque jour.
|
||||||
|
Ni plus, ni moins.
|
||||||
|
|
||||||
|
Ce n'est pas une monnaie complémentaire indexée sur l'euro.
|
||||||
|
Ce n'est pas une monnaie locale. Ce n'est pas une spéculation.
|
||||||
|
La Ğ1 (June) est la première implémentation de la TRM — lancée le 8 mars 2017,
|
||||||
|
plus de 6 000 membres, des milliers d'offres et de demandes,
|
||||||
|
une conférence biannuelle. Une économie vivante.
|
||||||
|
|
||||||
|
L'entrée se fait par la toile de confiance (WoT) : chaque nouveau membre
|
||||||
|
est certifié par des membres existants, dans un graphe de confiance humain.
|
||||||
|
Pas de banque, pas d'État, pas de biométrie.
|
||||||
|
C'est aussi ce qui lie monnaie libre et identité décentralisée —
|
||||||
|
la même clé ouvre les deux portes.
|
||||||
|
|
||||||
|
Le DU comme unité de mesure change la posture.
|
||||||
|
On ne vend plus — on donne. On n'achète plus — on reçoit.
|
||||||
|
On ne paye plus — on mesure.
|
||||||
|
La monnaie libre mesure la gratitude. Pas le prix.
|
||||||
|
|
||||||
|
links:
|
||||||
|
- label: monnaie-libre.fr
|
||||||
|
href: https://monnaie-libre.fr
|
||||||
|
icon: external-link
|
||||||
|
- label: Théorie Relative de la Monnaie
|
||||||
|
href: https://trm.creationmonetaire.info
|
||||||
|
icon: book-open
|
||||||
|
- label: duniter.org — le logiciel
|
||||||
|
href: https://duniter.org
|
||||||
|
icon: code-2
|
||||||
|
- label: Forum Duniter
|
||||||
|
href: https://forum.duniter.org
|
||||||
|
icon: message-square
|
||||||
|
- label: Forum Monnaie Libre
|
||||||
|
href: https://forum.monnaie-libre.fr
|
||||||
|
icon: users
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user