Boîtes à outils enrichies : ContextMapper, SocioElection, WorkflowMilestones
- ContextMapper : 4 questions contexte → méthode de décision optimale (advice process Laloux, vote inertiel WoT, consentement sociocratique, Smith…) - SocioElection : guide élection sociocratique 6 étapes + advice process + clarté de rôle - WorkflowMilestones : 11 jalons de protocole (7 essentiels), durées recommandées, principes Ostrom - WorkspaceSelector : sélecteur de collectif multi-site dans le header - SectionLayout : toolbox en USlideover droit sur mobile, sidebar sticky desktop - Décisions : ContextMapper intégré + guide consentement - Mandats : SocioElection intégré + cycle de mandat - Documents : guide inertie 4 niveaux + structure + IPFS - Protocoles : WorkflowMilestones + protocole élection sociocratique ajouté - Renommage projet Glibredecision → libreDecision (dossier + sources) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Glibredecision
|
||||
# libreDecision
|
||||
|
||||
Plateforme de decisions collectives pour la communaute Duniter/G1.
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
title: Documentation technique
|
||||
description: Architecture, API et reference technique de Glibredecision
|
||||
description: Architecture, API et reference technique de libreDecision
|
||||
---
|
||||
|
||||
# Documentation technique
|
||||
|
||||
Bienvenue dans la documentation technique de Glibredecision, la plateforme de decisions collectives pour la communaute Duniter/G1.
|
||||
Bienvenue dans la documentation technique de libreDecision, la plateforme de decisions collectives pour la communaute Duniter/G1.
|
||||
|
||||
## Presentation
|
||||
|
||||
Glibredecision est une plateforme de gouvernance decentralisee qui permet aux membres de la Toile de Confiance (WoT) Duniter V2 de gerer des documents de reference modulaires sous vote permanent, prendre des decisions collectives multi-etapes, attribuer des mandats et archiver de maniere immuable les resultats via IPFS et la blockchain Duniter.
|
||||
libreDecision est une plateforme de gouvernance decentralisee qui permet aux membres de la Toile de Confiance (WoT) Duniter V2 de gerer des documents de reference modulaires sous vote permanent, prendre des decisions collectives multi-etapes, attribuer des mandats et archiver de maniere immuable les resultats via IPFS et la blockchain Duniter.
|
||||
|
||||
## Stack technique
|
||||
|
||||
@@ -37,7 +37,7 @@ Glibredecision est une plateforme de gouvernance decentralisee qui permet aux me
|
||||
|
||||
- **Version** : 1.0.0-rc
|
||||
- **Statut** : Release candidate -- Sprint 5 (documentation et stabilisation)
|
||||
- **Depot** : [git.duniter.org/tools/glibredecision](https://git.duniter.org/tools/glibredecision)
|
||||
- **Depot** : [git.duniter.org/tools/libredecision](https://git.duniter.org/tools/libredecision)
|
||||
|
||||
## Sections
|
||||
|
||||
|
||||
1101
docs/content/dev/11.governance-modalities.md
Normal file
1101
docs/content/dev/11.governance-modalities.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,16 @@
|
||||
---
|
||||
title: Architecture
|
||||
description: Vue d'ensemble de l'architecture technique de Glibredecision
|
||||
description: Vue d'ensemble de l'architecture technique de libreDecision
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Glibredecision est organise en monorepo avec trois composants principaux :
|
||||
libreDecision est organise en monorepo avec trois composants principaux :
|
||||
|
||||
```
|
||||
Glibredecision/
|
||||
libreDecision/
|
||||
backend/ # API Python FastAPI (port 8002)
|
||||
frontend/ # Application Nuxt 4 (port 3002)
|
||||
docker/ # Fichiers Docker et orchestration
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Reference API
|
||||
description: Liste des endpoints de l'API Glibredecision
|
||||
description: Liste des endpoints de l'API libreDecision
|
||||
---
|
||||
|
||||
# Reference API
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Tables et relations de la base de donnees PostgreSQL
|
||||
|
||||
# Schema de base de donnees
|
||||
|
||||
Glibredecision utilise PostgreSQL 16 avec SQLAlchemy 2.0 en mode asynchrone (asyncpg). Toutes les cles primaires sont des UUID v4.
|
||||
libreDecision utilise PostgreSQL 16 avec SQLAlchemy 2.0 en mode asynchrone (asyncpg). Toutes les cles primaires sont des UUID v4.
|
||||
|
||||
## Tables
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Formules mathematiques de seuil WoT, criteres Smith et TechComm, si
|
||||
|
||||
# Formules de seuil
|
||||
|
||||
Glibredecision utilise un systeme de formules mathematiques pour determiner les seuils d'adoption des votes. Le mecanisme central est la **formule d'inertie WoT** qui impose une quasi-unanimite en cas de faible participation et converge vers une majorite simple a participation elevee.
|
||||
libreDecision utilise un systeme de formules mathematiques pour determiner les seuils d'adoption des votes. Le mecanisme central est la **formule d'inertie WoT** qui impose une quasi-unanimite en cas de faible participation et converge vers une majorite simple a participation elevee.
|
||||
|
||||
## Formule principale -- Seuil WoT
|
||||
|
||||
@@ -174,7 +174,7 @@ Les parametres de formule sont encodes dans une chaine compacte pour faciliter l
|
||||
|
||||
## Vote nuance
|
||||
|
||||
En plus du vote binaire (pour/contre), Glibredecision supporte un vote nuance a 6 niveaux :
|
||||
En plus du vote binaire (pour/contre), libreDecision supporte un vote nuance a 6 niveaux :
|
||||
|
||||
| Niveau | Label | Valeur normalisee |
|
||||
| ------ | ------------- | ----------------: |
|
||||
@@ -211,7 +211,7 @@ L'API expose un endpoint de simulation qui permet de tester le comportement de l
|
||||
**Exemple de requete** :
|
||||
|
||||
```bash
|
||||
curl -X POST https://glibredecision.example.org/api/v1/protocols/simulate \
|
||||
curl -X POST https://libredecision.example.org/api/v1/protocols/simulate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"wot_size": 7224,
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Integration Duniter V2, IPFS et ancrage on-chain
|
||||
|
||||
# Integration blockchain
|
||||
|
||||
Glibredecision s'integre a la blockchain Duniter V2 pour trois fonctions essentielles :
|
||||
libreDecision s'integre a la blockchain Duniter V2 pour trois fonctions essentielles :
|
||||
|
||||
1. **Authentification** -- Verification de l'identite des membres via signature Ed25519
|
||||
2. **Donnees WoT** -- Recuperation des tailles WoT, Smith et TechComm pour le calcul des seuils
|
||||
@@ -100,7 +100,7 @@ L'ancrage on-chain consiste a soumettre un extrinsic `system.remark` contenant l
|
||||
### Format du remark
|
||||
|
||||
```
|
||||
glibredecision:sanctuary:{content_hash_sha256}
|
||||
libredecision:sanctuary:{content_hash_sha256}
|
||||
```
|
||||
|
||||
### Soumission
|
||||
@@ -113,7 +113,7 @@ substrate = SubstrateInterface(url="wss://gdev.p2p.legal/ws")
|
||||
call = substrate.compose_call(
|
||||
call_module="System",
|
||||
call_function="remark",
|
||||
call_params={"remark": f"glibredecision:sanctuary:{content_hash}"},
|
||||
call_params={"remark": f"libredecision:sanctuary:{content_hash}"},
|
||||
)
|
||||
|
||||
extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Contribution
|
||||
description: Guide de contribution au projet Glibredecision
|
||||
description: Guide de contribution au projet libreDecision
|
||||
---
|
||||
|
||||
# Guide de contribution
|
||||
|
||||
Merci de votre interet pour contribuer a Glibredecision. Ce guide explique comment configurer l'environnement de developpement, les conventions a respecter et le processus de contribution.
|
||||
Merci de votre interet pour contribuer a libreDecision. Ce guide explique comment configurer l'environnement de developpement, les conventions a respecter et le processus de contribution.
|
||||
|
||||
## Prerequis
|
||||
|
||||
@@ -21,8 +21,8 @@ Merci de votre interet pour contribuer a Glibredecision. Ce guide explique comme
|
||||
|
||||
```bash
|
||||
# Cloner le depot
|
||||
git clone https://git.duniter.org/tools/glibredecision.git
|
||||
cd glibredecision
|
||||
git clone https://git.duniter.org/tools/libredecision.git
|
||||
cd libredecision
|
||||
|
||||
# Copier le fichier d'environnement
|
||||
cp .env.example .env
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Deploiement
|
||||
description: Guide de deploiement en production de Glibredecision
|
||||
description: Guide de deploiement en production de libreDecision
|
||||
---
|
||||
|
||||
# Deploiement
|
||||
|
||||
Ce guide couvre le deploiement complet de Glibredecision en production avec Docker, Traefik, PostgreSQL et IPFS.
|
||||
Ce guide couvre le deploiement complet de libreDecision en production avec Docker, Traefik, PostgreSQL et IPFS.
|
||||
|
||||
## Prerequis
|
||||
|
||||
@@ -13,7 +13,7 @@ Ce guide couvre le deploiement complet de Glibredecision en production avec Dock
|
||||
| --------- | ---------------- | ----------- |
|
||||
| Docker | 24+ | Moteur de conteneurs |
|
||||
| Docker Compose | 2.20+ | Orchestration multi-conteneurs |
|
||||
| Nom de domaine | -- | Domaine pointe vers le serveur (ex: `glibredecision.org`) |
|
||||
| Nom de domaine | -- | Domaine pointe vers le serveur (ex: `libredecision.org`) |
|
||||
| Certificat TLS | -- | Genere automatiquement par Traefik via Let's Encrypt |
|
||||
| Traefik | 2.10+ | Reverse proxy avec terminaison TLS (reseau externe `traefik`) |
|
||||
| Serveur | 2 vCPU, 4 Go RAM, 40 Go SSD | Ressources recommandees |
|
||||
@@ -40,18 +40,18 @@ cp .env.example .env
|
||||
|
||||
| Variable | Description | Valeur par defaut | Production |
|
||||
| -------- | ----------- | ----------------- | ---------- |
|
||||
| `POSTGRES_DB` | Nom de la base de donnees | `glibredecision` | `glibredecision` |
|
||||
| `POSTGRES_USER` | Utilisateur PostgreSQL | `glibredecision` | `glibredecision` |
|
||||
| `POSTGRES_DB` | Nom de la base de donnees | `libredecision` | `libredecision` |
|
||||
| `POSTGRES_USER` | Utilisateur PostgreSQL | `libredecision` | `libredecision` |
|
||||
| `POSTGRES_PASSWORD` | Mot de passe PostgreSQL | `change-me-in-production` | **Generer un mot de passe fort** (32+ caracteres) |
|
||||
| `DATABASE_URL` | URL de connexion asyncpg | `postgresql+asyncpg://...@localhost:5432/...` | Construite automatiquement dans docker-compose |
|
||||
| `SECRET_KEY` | Cle secrete pour les tokens de session | `change-me-in-production-with-a-real-secret-key` | **Generer une cle aleatoire** (`openssl rand -hex 64`) |
|
||||
| `DEBUG` | Mode debug | `true` | **`false`** |
|
||||
| `CORS_ORIGINS` | Origines CORS autorisees | `["http://localhost:3002"]` | `["https://glibredecision.org"]` |
|
||||
| `CORS_ORIGINS` | Origines CORS autorisees | `["http://localhost:3002"]` | `["https://libredecision.org"]` |
|
||||
| `DUNITER_RPC_URL` | URL du noeud Duniter V2 RPC | `wss://gdev.p2p.legal/ws` | URL du noeud de production |
|
||||
| `IPFS_API_URL` | URL de l'API IPFS (kubo) | `http://localhost:5001` | `http://ipfs:5001` (interne Docker) |
|
||||
| `IPFS_GATEWAY_URL` | URL de la passerelle IPFS | `http://localhost:8080` | `http://ipfs:8080` (interne Docker) |
|
||||
| `NUXT_PUBLIC_API_BASE` | URL publique de l'API pour le frontend | `http://localhost:8002/api/v1` | `https://glibredecision.org/api/v1` |
|
||||
| `DOMAIN` | Nom de domaine | `glibredecision.org` | Votre domaine |
|
||||
| `NUXT_PUBLIC_API_BASE` | URL publique de l'API pour le frontend | `http://localhost:8002/api/v1` | `https://libredecision.org/api/v1` |
|
||||
| `DOMAIN` | Nom de domaine | `libredecision.org` | Votre domaine |
|
||||
|
||||
### Generer les secrets
|
||||
|
||||
@@ -73,10 +73,10 @@ Ne commitez jamais le fichier `.env` contenant les secrets de production. Ajoute
|
||||
|
||||
```bash
|
||||
# Se placer dans le repertoire du projet
|
||||
cd /opt/glibredecision
|
||||
cd /opt/libredecision
|
||||
|
||||
# Cloner le depot
|
||||
git clone https://git.duniter.org/tools/glibredecision.git .
|
||||
git clone https://git.duniter.org/tools/libredecision.git .
|
||||
|
||||
# Configurer l'environnement
|
||||
cp .env.example .env
|
||||
@@ -108,12 +108,12 @@ docker compose -f docker/docker-compose.yml ps
|
||||
docker compose -f docker/docker-compose.yml logs -f backend
|
||||
|
||||
# Health check de l'API
|
||||
curl -s https://glibredecision.org/api/health | jq .
|
||||
curl -s https://libredecision.org/api/health | jq .
|
||||
```
|
||||
|
||||
## Migration de base de donnees (Alembic)
|
||||
|
||||
Glibredecision utilise Alembic pour les migrations de schema PostgreSQL.
|
||||
libreDecision utilise Alembic pour les migrations de schema PostgreSQL.
|
||||
|
||||
### Appliquer les migrations
|
||||
|
||||
@@ -182,7 +182,7 @@ services:
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=admin@glibredecision.org"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=admin@libredecision.org"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- "80:80"
|
||||
@@ -207,10 +207,10 @@ docker compose -f docker-compose.traefik.yml up -d
|
||||
|
||||
### Routage
|
||||
|
||||
Le `docker-compose.yml` de Glibredecision configure automatiquement les labels Traefik :
|
||||
Le `docker-compose.yml` de libreDecision configure automatiquement les labels Traefik :
|
||||
|
||||
- **Frontend** : `Host(glibredecision.org)` sur le port 3000
|
||||
- **Backend** : `Host(glibredecision.org) && PathPrefix(/api)` sur le port 8002
|
||||
- **Frontend** : `Host(libredecision.org)` sur le port 3000
|
||||
- **Backend** : `Host(libredecision.org) && PathPrefix(/api)` sur le port 8002
|
||||
- Les deux utilisent le certificat Let's Encrypt (`certresolver=letsencrypt`)
|
||||
- Redirection HTTP vers HTTPS automatique
|
||||
|
||||
@@ -230,7 +230,7 @@ Le service PostgreSQL dispose d'un health check integre (`pg_isready`). Le backe
|
||||
|
||||
```bash
|
||||
# Health check de l'API
|
||||
curl -s https://glibredecision.org/api/health
|
||||
curl -s https://libredecision.org/api/health
|
||||
# Reponse attendue : {"status": "healthy"}
|
||||
```
|
||||
|
||||
@@ -257,8 +257,8 @@ Surveillez les indicateurs suivants :
|
||||
| ---------- | -------- | --------------- |
|
||||
| CPU/RAM conteneurs | `docker stats` | > 80% RAM |
|
||||
| Espace disque | `df -h` | > 85% |
|
||||
| Connexions PostgreSQL | `docker exec postgres psql -U glibredecision -c "SELECT count(*) FROM pg_stat_activity;"` | > 80 |
|
||||
| Taille base de donnees | `docker exec postgres psql -U glibredecision -c "SELECT pg_size_pretty(pg_database_size('glibredecision'));"` | Information |
|
||||
| Connexions PostgreSQL | `docker exec postgres psql -U libredecision -c "SELECT count(*) FROM pg_stat_activity;"` | > 80 |
|
||||
| Taille base de donnees | `docker exec postgres psql -U libredecision -c "SELECT pg_size_pretty(pg_database_size('libredecision'));"` | Information |
|
||||
| Statut IPFS | `docker exec ipfs ipfs id` | Erreur |
|
||||
|
||||
## Sauvegarde PostgreSQL
|
||||
@@ -268,7 +268,7 @@ Surveillez les indicateurs suivants :
|
||||
```bash
|
||||
# Dump complet de la base
|
||||
docker compose -f docker/docker-compose.yml exec postgres \
|
||||
pg_dump -U glibredecision -Fc glibredecision > backup_$(date +%Y%m%d_%H%M%S).dump
|
||||
pg_dump -U libredecision -Fc libredecision > backup_$(date +%Y%m%d_%H%M%S).dump
|
||||
```
|
||||
|
||||
### Restauration
|
||||
@@ -276,7 +276,7 @@ docker compose -f docker/docker-compose.yml exec postgres \
|
||||
```bash
|
||||
# Restaurer un dump
|
||||
docker compose -f docker/docker-compose.yml exec -T postgres \
|
||||
pg_restore -U glibredecision -d glibredecision --clean < backup_20260228_120000.dump
|
||||
pg_restore -U libredecision -d libredecision --clean < backup_20260228_120000.dump
|
||||
```
|
||||
|
||||
### Sauvegarde automatique (cron)
|
||||
@@ -288,7 +288,7 @@ Ajoutez un crontab pour des sauvegardes quotidiennes :
|
||||
crontab -e
|
||||
|
||||
# Ajouter une sauvegarde quotidienne a 3h du matin
|
||||
0 3 * * * cd /opt/glibredecision && docker compose -f docker/docker-compose.yml exec -T postgres pg_dump -U glibredecision -Fc glibredecision > /opt/backups/glibredecision_$(date +\%Y\%m\%d).dump && find /opt/backups -name "glibredecision_*.dump" -mtime +30 -delete
|
||||
0 3 * * * cd /opt/libredecision && docker compose -f docker/docker-compose.yml exec -T postgres pg_dump -U libredecision -Fc libredecision > /opt/backups/libredecision_$(date +\%Y\%m\%d).dump && find /opt/backups -name "libredecision_*.dump" -mtime +30 -delete
|
||||
```
|
||||
|
||||
Cette commande :
|
||||
@@ -301,7 +301,7 @@ Cette commande :
|
||||
### Procedure standard
|
||||
|
||||
```bash
|
||||
cd /opt/glibredecision
|
||||
cd /opt/libredecision
|
||||
|
||||
# 1. Tirer les dernieres images
|
||||
docker compose -f docker/docker-compose.yml pull
|
||||
@@ -317,7 +317,7 @@ docker image prune -f
|
||||
|
||||
# 5. Verifier le deploiement
|
||||
docker compose -f docker/docker-compose.yml ps
|
||||
curl -s https://glibredecision.org/api/health
|
||||
curl -s https://libredecision.org/api/health
|
||||
```
|
||||
|
||||
### Pipeline CI/CD (Woodpecker)
|
||||
@@ -377,7 +377,7 @@ docker compose -f docker/docker-compose.yml up -d # recree avec le nouveau m
|
||||
|
||||
**Symptome** : Le site est inaccessible en HTTPS ou affiche un certificat invalide.
|
||||
|
||||
1. Verifier que le DNS A/AAAA pointe vers le serveur : `dig glibredecision.org`
|
||||
1. Verifier que le DNS A/AAAA pointe vers le serveur : `dig libredecision.org`
|
||||
2. Verifier que les ports 80 et 443 sont ouverts : `ss -tlnp | grep -E '80|443'`
|
||||
3. Consulter les logs Traefik : `docker logs traefik 2>&1 | grep -i acme`
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
---
|
||||
title: Securite
|
||||
description: Politique de securite et mesures de protection de Glibredecision
|
||||
description: Politique de securite et mesures de protection de libreDecision
|
||||
---
|
||||
|
||||
# Securite
|
||||
|
||||
Ce document decrit les mesures de securite implementees dans Glibredecision pour proteger l'integrite de la plateforme, des votes et des donnees des utilisateurs.
|
||||
Ce document decrit les mesures de securite implementees dans libreDecision pour proteger l'integrite de la plateforme, des votes et des donnees des utilisateurs.
|
||||
|
||||
## Authentification Duniter V2 (Ed25519 challenge-response)
|
||||
|
||||
### Principe
|
||||
|
||||
Glibredecision n'utilise ni mot de passe ni systeme d'inscription classique. L'authentification repose entierement sur la cryptographie Ed25519 de la blockchain Duniter V2.
|
||||
libreDecision n'utilise ni mot de passe ni systeme d'inscription classique. L'authentification repose entierement sur la cryptographie Ed25519 de la blockchain Duniter V2.
|
||||
|
||||
### Flux challenge-response
|
||||
|
||||
@@ -169,10 +169,10 @@ Contenu --> [SHA-256] --> hash
|
||||
### Format du remark on-chain
|
||||
|
||||
```
|
||||
glibredecision:sanctuary:{content_hash_sha256}
|
||||
libredecision:sanctuary:{content_hash_sha256}
|
||||
```
|
||||
|
||||
Le prefixe `glibredecision:sanctuary:` permet d'identifier les ancrages de Glibredecision parmi tous les remarks de la blockchain.
|
||||
Le prefixe `libredecision:sanctuary:` permet d'identifier les ancrages de libreDecision parmi tous les remarks de la blockchain.
|
||||
|
||||
## WebSocket : authentification et securite
|
||||
|
||||
@@ -243,7 +243,7 @@ Les logs d'audit sont conserves de maniere permanente dans la base de donnees. L
|
||||
|
||||
### Processus
|
||||
|
||||
Si vous decouvrez une vulnerabilite de securite dans Glibredecision, merci de suivre cette procedure de divulgation responsable :
|
||||
Si vous decouvrez une vulnerabilite de securite dans libreDecision, merci de suivre cette procedure de divulgation responsable :
|
||||
|
||||
1. **Ne divulguez pas publiquement** la vulnerabilite avant qu'un correctif soit disponible.
|
||||
2. **Contactez l'equipe** via le canal securise indique sur le depot Git Duniter ou via le forum Duniter (message prive aux mainteneurs).
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
title: Documentation utilisateur
|
||||
description: Guide d'utilisation de la plateforme Glibredecision
|
||||
description: Guide d'utilisation de la plateforme libreDecision
|
||||
---
|
||||
|
||||
# Documentation utilisateur
|
||||
|
||||
Bienvenue dans la documentation utilisateur de Glibredecision, la plateforme de decisions collectives pour la communaute Duniter/G1.
|
||||
Bienvenue dans la documentation utilisateur de libreDecision, la plateforme de decisions collectives pour la communaute Duniter/G1.
|
||||
|
||||
## Qu'est-ce que Glibredecision ?
|
||||
## Qu'est-ce que libreDecision ?
|
||||
|
||||
Glibredecision est une plateforme de gouvernance decentralisee qui permet aux membres de la Toile de Confiance (WoT) Duniter de :
|
||||
libreDecision est une plateforme de gouvernance decentralisee qui permet aux membres de la Toile de Confiance (WoT) Duniter de :
|
||||
|
||||
- Gerer des **documents de reference** modulaires (Licence G1, Engagements Forgeron, Reglement du Comite Technique, etc.) sous vote permanent
|
||||
- Prendre des **decisions collectives** via des processus multi-etapes (qualification, examen, vote, execution, rapport)
|
||||
@@ -33,7 +33,7 @@ La plateforme est entierement transparente : tous les votes sont publics, signes
|
||||
|
||||
## Par ou commencer ?
|
||||
|
||||
1. **Nouveau sur Glibredecision ?** Commencez par le guide [Premiers pas](/user/getting-started) pour vous connecter et decouvrir l'interface.
|
||||
1. **Nouveau sur libreDecision ?** Commencez par le guide [Premiers pas](/user/getting-started) pour vous connecter et decouvrir l'interface.
|
||||
2. **Vous voulez voter ?** Consultez le guide [Vote](/user/voting) pour comprendre les types de vote et la formule de seuil.
|
||||
3. **Vous voulez proposer une modification ?** Le guide [Documents](/user/documents) explique comment proposer des modifications aux textes fondateurs.
|
||||
4. **Une question ?** La [FAQ](/user/faq) repond aux questions les plus courantes.
|
||||
@@ -42,7 +42,7 @@ La plateforme est entierement transparente : tous les votes sont publics, signes
|
||||
|
||||
Cette documentation est elle-meme un document en evolution. Si vous constatez une erreur, une imprecision ou un manque, vous pouvez :
|
||||
|
||||
- Ouvrir une issue sur le [depot Git](https://git.duniter.org/tools/glibredecision) de Glibredecision
|
||||
- Ouvrir une issue sur le [depot Git](https://git.duniter.org/tools/libredecision) de libreDecision
|
||||
- Proposer une modification directement via une merge request
|
||||
- En discuter sur le [forum Duniter](https://forum.duniter.org)
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Premiers pas
|
||||
description: Connexion et prise en main de Glibredecision
|
||||
description: Connexion et prise en main de libreDecision
|
||||
---
|
||||
|
||||
# Premiers pas
|
||||
|
||||
Ce guide vous accompagne de votre premiere visite jusqu'a votre premier vote sur Glibredecision.
|
||||
Ce guide vous accompagne de votre premiere visite jusqu'a votre premier vote sur libreDecision.
|
||||
|
||||
## Prerequis
|
||||
|
||||
@@ -28,9 +28,9 @@ Vous pouvez **consulter** les documents, decisions et resultats de vote sans auc
|
||||
3. Creez ou importez votre compte Duniter V2 dans l'extension.
|
||||
4. Assurez-vous que votre adresse SS58 est bien celle liee a votre identite Duniter.
|
||||
|
||||
## Qui peut utiliser Glibredecision ?
|
||||
## Qui peut utiliser libreDecision ?
|
||||
|
||||
Glibredecision est ouvert a tous les membres de la Toile de Confiance (WoT) Duniter V2. Pour utiliser pleinement la plateforme, vous devez posseder une identite Duniter avec une adresse SS58 valide.
|
||||
libreDecision est ouvert a tous les membres de la Toile de Confiance (WoT) Duniter V2. Pour utiliser pleinement la plateforme, vous devez posseder une identite Duniter avec une adresse SS58 valide.
|
||||
|
||||
- **Consultation** : tout visiteur peut consulter les documents, decisions et resultats de vote.
|
||||
- **Participation** (voter, proposer) : reservee aux membres authentifies via leur identite Duniter.
|
||||
@@ -63,7 +63,7 @@ Votre cle privee n'est **jamais** transmise au serveur. Seule la signature du ch
|
||||
|
||||
La barre de navigation en haut de page contient :
|
||||
|
||||
- **Logo Glibredecision** : retour a l'accueil
|
||||
- **Logo libreDecision** : retour a l'accueil
|
||||
- **Menu principal** : acces aux cinq sections
|
||||
- **Bouton de connexion** / **Votre profil** (si connecte)
|
||||
- **Indicateur temps reel** : point colore indiquant l'etat de la connexion WebSocket
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Documents
|
||||
description: Guide des documents de reference sur Glibredecision
|
||||
description: Guide des documents de reference sur libreDecision
|
||||
---
|
||||
|
||||
# Documents de reference
|
||||
@@ -9,7 +9,7 @@ description: Guide des documents de reference sur Glibredecision
|
||||
|
||||
Un document de reference est un **texte fondateur** de la communaute Duniter/G1. Il peut s'agir d'une licence monetaire, d'un engagement que les membres s'engagent a respecter, d'un reglement interieur ou d'un texte constitutif. Ces documents definissent les regles, les valeurs et le fonctionnement de la communaute.
|
||||
|
||||
Ce qui rend Glibredecision unique, c'est que ces documents sont **modulaires** et sous **vote permanent** : chaque document est compose d'items individuels (clauses, regles, verifications, preambules, sections) qui peuvent etre modifies independamment par proposition et vote. La communaute peut faire evoluer ses textes de maniere continue, sans procedure lourde ni periode speciale.
|
||||
Ce qui rend libreDecision unique, c'est que ces documents sont **modulaires** et sous **vote permanent** : chaque document est compose d'items individuels (clauses, regles, verifications, preambules, sections) qui peuvent etre modifies independamment par proposition et vote. La communaute peut faire evoluer ses textes de maniere continue, sans procedure lourde ni periode speciale.
|
||||
|
||||
## Types de documents
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Decisions
|
||||
description: Guide des processus decisionnels sur Glibredecision
|
||||
description: Guide des processus decisionnels sur libreDecision
|
||||
---
|
||||
|
||||
# Decisions
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: Vote
|
||||
description: Guide du systeme de vote sur Glibredecision
|
||||
description: Guide du systeme de vote sur libreDecision
|
||||
---
|
||||
|
||||
# Vote
|
||||
|
||||
## Principe
|
||||
|
||||
Le systeme de vote de Glibredecision est concu pour adapter le seuil d'adoption a la participation reelle. Quand peu de membres votent, une quasi-unanimite est exigee. Quand la participation est elevee, une majorite simple suffit. Ce mecanisme d'**inertie** protege contre les decisions prises par un petit groupe.
|
||||
Le systeme de vote de libreDecision est concu pour adapter le seuil d'adoption a la participation reelle. Quand peu de membres votent, une quasi-unanimite est exigee. Quand la participation est elevee, une majorite simple suffit. Ce mecanisme d'**inertie** protege contre les decisions prises par un petit groupe.
|
||||
|
||||
## Types de vote
|
||||
|
||||
@@ -42,7 +42,7 @@ Le vote nuance est recommande pour :
|
||||
|
||||
### L'analogie de l'inertie
|
||||
|
||||
Imaginez un gros rocher pose au sommet d'une colline. Pour le deplacer, il faut une force considerable : c'est l'**inertie**. Dans Glibredecision, le rocher represente le statu quo et la force necessaire represente le nombre de votes favorables.
|
||||
Imaginez un gros rocher pose au sommet d'une colline. Pour le deplacer, il faut une force considerable : c'est l'**inertie**. Dans libreDecision, le rocher represente le statu quo et la force necessaire represente le nombre de votes favorables.
|
||||
|
||||
- **Quand peu de personnes poussent** (faible participation) : il faut que presque tout le monde pousse dans la meme direction. Si seulement 10 personnes sur 7000 votent, il faut que 9 sur 10 soient pour.
|
||||
- **Quand beaucoup de personnes poussent** (forte participation) : la majorite simple suffit. Si 7000 personnes votent, il suffit que 3500 soient pour (50%).
|
||||
@@ -279,7 +279,7 @@ Le simulateur montre visuellement l'impact : avec un gradient plus eleve, l'exig
|
||||
|
||||
## Mises a jour en temps reel
|
||||
|
||||
Glibredecision utilise une connexion **WebSocket** pour diffuser les mises a jour de vote en temps reel a tous les utilisateurs qui consultent une session de vote.
|
||||
libreDecision utilise une connexion **WebSocket** pour diffuser les mises a jour de vote en temps reel a tous les utilisateurs qui consultent une session de vote.
|
||||
|
||||
### Ce qui est mis a jour en direct
|
||||
|
||||
@@ -318,7 +318,7 @@ Sur la page d'une session de vote, l'onglet **Votes** affiche la liste de tous l
|
||||
- L'horodatage.
|
||||
- Un lien pour verifier la signature Ed25519.
|
||||
|
||||
Les votes sont publics et verifiables par tous. Il n'y a pas de vote secret dans Glibredecision : la transparence est un principe fondamental.
|
||||
Les votes sont publics et verifiables par tous. Il n'y a pas de vote secret dans libreDecision : la transparence est un principe fondamental.
|
||||
|
||||
## Meta-gouvernance : voter sur les regles du vote
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Mandats
|
||||
description: Guide des mandats sur Glibredecision
|
||||
description: Guide des mandats sur libreDecision
|
||||
---
|
||||
|
||||
# Mandats
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
title: Sanctuaire
|
||||
description: Guide de l'archivage immuable sur Glibredecision
|
||||
description: Guide de l'archivage immuable sur libreDecision
|
||||
---
|
||||
|
||||
# Sanctuaire
|
||||
|
||||
## Qu'est-ce que le Sanctuaire ?
|
||||
|
||||
Le Sanctuaire est la couche d'**archivage immuable** de Glibredecision. C'est l'endroit ou les decisions adoptees, les documents archives et les resultats de vote sont preserves de maniere permanente et verifiable.
|
||||
Le Sanctuaire est la couche d'**archivage immuable** de libreDecision. C'est l'endroit ou les decisions adoptees, les documents archives et les resultats de vote sont preserves de maniere permanente et verifiable.
|
||||
|
||||
Le principe est simple : une fois qu'un contenu entre dans le Sanctuaire, il ne peut plus etre modifie ni supprime. Meme si la plateforme Glibredecision disparaissait, les preuves resteraient accessibles et verifiables de maniere independante.
|
||||
Le principe est simple : une fois qu'un contenu entre dans le Sanctuaire, il ne peut plus etre modifie ni supprime. Meme si la plateforme libreDecision disparaissait, les preuves resteraient accessibles et verifiables de maniere independante.
|
||||
|
||||
## Triple preuve : SHA-256 + IPFS + Blockchain
|
||||
|
||||
@@ -58,12 +58,12 @@ L'ancrage on-chain consiste a enregistrer le hash SHA-256 du contenu sur la bloc
|
||||
|
||||
- **Horodatage** : la date du bloc prouve que le contenu existait a cette date.
|
||||
- **Immutabilite** : une fois inscrit dans la blockchain, le remark ne peut pas etre modifie.
|
||||
- **Independance** : la preuve est verifiable sur la blockchain, independamment de Glibredecision.
|
||||
- **Independance** : la preuve est verifiable sur la blockchain, independamment de libreDecision.
|
||||
|
||||
Le format du remark est :
|
||||
|
||||
```
|
||||
glibredecision:sanctuary:{hash_sha256_du_contenu}
|
||||
libredecision:sanctuary:{hash_sha256_du_contenu}
|
||||
```
|
||||
|
||||
**Analogie** : C'est comme publier un hash dans un journal date et immuable. N'importe qui peut verifier que le hash etait bien la a cette date.
|
||||
@@ -74,7 +74,7 @@ La gouvernance exige la transparence et la tracabilite. Le Sanctuaire garantit q
|
||||
|
||||
- **Aucune decision adoptee ne peut etre modifiee retroactivement** : le hash et l'ancrage on-chain rendent toute falsification detectable.
|
||||
- **Tout membre peut verifier l'authenticite** d'un document ou d'un resultat de vote de maniere independante.
|
||||
- **L'historique des decisions est preserve** independamment de la plateforme : meme sans Glibredecision, les preuves restent sur IPFS et la blockchain.
|
||||
- **L'historique des decisions est preserve** independamment de la plateforme : meme sans libreDecision, les preuves restent sur IPFS et la blockchain.
|
||||
|
||||
## Types d'entrees
|
||||
|
||||
@@ -110,7 +110,7 @@ Cette fonctionnalite permet de retracer l'historique complet d'archivage d'un do
|
||||
|
||||
### Verification automatique
|
||||
|
||||
Glibredecision propose une verification automatique d'integrite pour chaque entree du Sanctuaire :
|
||||
libreDecision propose une verification automatique d'integrite pour chaque entree du Sanctuaire :
|
||||
|
||||
1. Ouvrez l'entree a verifier dans le Sanctuaire.
|
||||
2. Cliquez sur **Verifier l'integrite**.
|
||||
@@ -126,7 +126,7 @@ Si les trois controles sont valides, le contenu est authentique et n'a pas ete m
|
||||
|
||||
### Verification manuelle (independante de la plateforme)
|
||||
|
||||
Pour une verification totalement independante de Glibredecision, suivez ces etapes :
|
||||
Pour une verification totalement independante de libreDecision, suivez ces etapes :
|
||||
|
||||
#### Etape 1 : Recuperer le contenu via IPFS
|
||||
|
||||
@@ -161,7 +161,7 @@ Comparez le hash obtenu avec le hash affiche dans le Sanctuaire. Ils doivent etr
|
||||
Si les trois hash correspondent (calcul local, Sanctuaire, on-chain), le contenu est authentique, integre et horodate. La triple preuve est confirmee.
|
||||
|
||||
::callout{type="tip"}
|
||||
L'ancrage on-chain fournit une preuve horodatee et infalsifiable de l'existence du contenu a la date du bloc. Meme si la plateforme Glibredecision disparait, la preuve reste verifiable sur la blockchain.
|
||||
L'ancrage on-chain fournit une preuve horodatee et infalsifiable de l'existence du contenu a la date du bloc. Meme si la plateforme libreDecision disparait, la preuve reste verifiable sur la blockchain.
|
||||
::
|
||||
|
||||
## Comprendre les informations d'ancrage on-chain
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
title: FAQ
|
||||
description: Questions frequentes sur Glibredecision
|
||||
description: Questions frequentes sur libreDecision
|
||||
---
|
||||
|
||||
# Questions frequentes
|
||||
|
||||
## Acces et authentification
|
||||
|
||||
### Ai-je besoin d'un compte Duniter pour utiliser Glibredecision ?
|
||||
### Ai-je besoin d'un compte Duniter pour utiliser libreDecision ?
|
||||
|
||||
Pour **consulter** les documents, decisions et resultats de vote, aucune authentification n'est necessaire. Pour **voter**, **proposer des modifications** ou **creer des decisions**, vous devez posseder une identite Duniter V2 avec une adresse SS58.
|
||||
|
||||
### Comment fonctionne la connexion sans mot de passe ?
|
||||
|
||||
Glibredecision utilise un systeme challenge-response base sur la cryptographie Ed25519. Voici le processus :
|
||||
libreDecision utilise un systeme challenge-response base sur la cryptographie Ed25519. Voici le processus :
|
||||
|
||||
1. Vous fournissez votre adresse Duniter SS58.
|
||||
2. Le serveur genere un texte aleatoire (le "challenge") de 64 caracteres hexadecimaux.
|
||||
@@ -37,7 +37,7 @@ Les sessions durent 24 heures. Reconnectez-vous en suivant le meme processus (ch
|
||||
|
||||
### Que se passe-t-il si je perds l'acces a ma cle privee ?
|
||||
|
||||
Glibredecision ne stocke jamais votre cle privee. Si vous perdez l'acces a votre cle, vous ne pourrez plus vous authentifier avec cette adresse. Vos votes passes restent enregistres et comptabilises. Contactez la communaute Duniter pour les procedures de recuperation d'identite si necessaire.
|
||||
libreDecision ne stocke jamais votre cle privee. Si vous perdez l'acces a votre cle, vous ne pourrez plus vous authentifier avec cette adresse. Vos votes passes restent enregistres et comptabilises. Contactez la communaute Duniter pour les procedures de recuperation d'identite si necessaire.
|
||||
|
||||
### Puis-je me connecter depuis plusieurs appareils ?
|
||||
|
||||
@@ -101,7 +101,7 @@ C'est le codage compact des parametres de formule :
|
||||
|
||||
### Les votes sont-ils secrets ?
|
||||
|
||||
Non. Les votes et leurs signatures cryptographiques sont **publics**, conformement au principe de transparence de la gouvernance. Chaque vote peut etre verifie independamment par quiconque possede la cle publique du votant. Il n'y a pas de vote secret dans Glibredecision.
|
||||
Non. Les votes et leurs signatures cryptographiques sont **publics**, conformement au principe de transparence de la gouvernance. Chaque vote peut etre verifie independamment par quiconque possede la cle publique du votant. Il n'y a pas de vote secret dans libreDecision.
|
||||
|
||||
### Le seuil peut-il changer pendant le vote ?
|
||||
|
||||
@@ -174,7 +174,7 @@ Oui. Un mandat actif peut etre revoque de maniere anticipee via l'action "Revoqu
|
||||
|
||||
### Pourquoi archiver sur IPFS et la blockchain ?
|
||||
|
||||
**IPFS** fournit un stockage distribue : le contenu est accessible meme si la plateforme Glibredecision est hors ligne. L'**ancrage on-chain** via `system.remark` cree une preuve horodatee immuable sur la blockchain Duniter V2. Le **hash SHA-256** garantit l'integrite du contenu. Ensemble, ils forment une **triple preuve** que le contenu n'a pas ete modifie depuis son archivage.
|
||||
**IPFS** fournit un stockage distribue : le contenu est accessible meme si la plateforme libreDecision est hors ligne. L'**ancrage on-chain** via `system.remark` cree une preuve horodatee immuable sur la blockchain Duniter V2. Le **hash SHA-256** garantit l'integrite du contenu. Ensemble, ils forment une **triple preuve** que le contenu n'a pas ete modifie depuis son archivage.
|
||||
|
||||
### Comment verifier qu'un document n'a pas ete modifie ?
|
||||
|
||||
@@ -193,7 +193,7 @@ Oui. L'archivage est declenche automatiquement :
|
||||
- Quand une decision est executee
|
||||
- Quand un document est archive manuellement
|
||||
|
||||
### Puis-je acceder aux archives sans Glibredecision ?
|
||||
### Puis-je acceder aux archives sans libreDecision ?
|
||||
|
||||
Oui. Les contenus archives sont accessibles via :
|
||||
|
||||
@@ -202,9 +202,9 @@ Oui. Les contenus archives sont accessibles via :
|
||||
|
||||
## Questions techniques
|
||||
|
||||
### Sur quelle blockchain Glibredecision fonctionne-t-il ?
|
||||
### Sur quelle blockchain libreDecision fonctionne-t-il ?
|
||||
|
||||
Glibredecision se connecte a la blockchain **Duniter V2** (basee sur Substrate). En environnement de developpement, il se connecte au reseau de test GDev (`wss://gdev.p2p.legal/ws`).
|
||||
libreDecision se connecte a la blockchain **Duniter V2** (basee sur Substrate). En environnement de developpement, il se connecte au reseau de test GDev (`wss://gdev.p2p.legal/ws`).
|
||||
|
||||
### Que se passe-t-il si la blockchain Duniter est indisponible ?
|
||||
|
||||
@@ -224,14 +224,14 @@ Oui. Les votes et leurs signatures cryptographiques sont publics, conformement a
|
||||
|
||||
### Les mises a jour sont-elles en temps reel ?
|
||||
|
||||
Oui. Glibredecision utilise une connexion WebSocket pour diffuser les mises a jour en temps reel :
|
||||
Oui. libreDecision utilise une connexion WebSocket pour diffuser les mises a jour en temps reel :
|
||||
|
||||
- Nouveaux votes soumis : la jauge de seuil est recalculee instantanement
|
||||
- Votes modifies : la jauge reflette le changement immediatement
|
||||
- Sessions cloturees : le resultat final s'affiche
|
||||
- Un indicateur de connexion (point vert/orange/rouge) en bas a droite indique l'etat de la connexion temps reel
|
||||
|
||||
### Ou est heberge Glibredecision ?
|
||||
### Ou est heberge libreDecision ?
|
||||
|
||||
La plateforme est hebergee sur une infrastructure geree par la communaute, avec deploiement automatise via Docker et Woodpecker CI. Le code source est ouvert et disponible sur le depot Git Duniter.
|
||||
|
||||
|
||||
@@ -99,8 +99,11 @@ function isActive(to: string) {
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Center: Mood switcher (desktop) -->
|
||||
<MoodSwitcher class="hidden sm:flex" />
|
||||
<!-- Center: Workspace selector + Mood switcher (desktop) -->
|
||||
<div class="app-header__center">
|
||||
<WorkspaceSelector class="hidden sm:flex" />
|
||||
<MoodSwitcher class="hidden sm:flex" />
|
||||
</div>
|
||||
|
||||
<!-- Right: Auth -->
|
||||
<div class="app-header__right">
|
||||
@@ -159,7 +162,11 @@ function isActive(to: string) {
|
||||
<span>{{ item.label }}</span>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
<!-- Mood switcher in mobile drawer -->
|
||||
<!-- Workspace + Mood in mobile drawer -->
|
||||
<div class="app-mobile-mood">
|
||||
<span class="app-mobile-mood__label">Espace</span>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
<div class="app-mobile-mood">
|
||||
<span class="app-mobile-mood__label">Ambiance</span>
|
||||
<MoodSwitcher />
|
||||
@@ -260,9 +267,18 @@ function isActive(to: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
height: 3.5rem;
|
||||
}
|
||||
|
||||
.app-header__center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
372
frontend/app/components/SectionLayout.vue
Normal file
372
frontend/app/components/SectionLayout.vue
Normal file
@@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SectionLayout — Mise en page pour sections.
|
||||
*
|
||||
* Desktop (≥1024px) : 2 colonnes, toolbox sticky à droite, toujours visible.
|
||||
* Mobile/tablette : toolbox en USlideover droit, bouton flottant.
|
||||
*/
|
||||
|
||||
export interface StatusFilter {
|
||||
id: string
|
||||
label: string
|
||||
count: number
|
||||
cssClass?: string
|
||||
}
|
||||
|
||||
export interface ToolboxItem {
|
||||
title: string
|
||||
description: string
|
||||
actions: Array<{
|
||||
label: string
|
||||
to?: string
|
||||
onClick?: () => void
|
||||
}>
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
statuses: StatusFilter[]
|
||||
toolboxItems?: ToolboxItem[]
|
||||
activeStatus?: string | null
|
||||
toolboxTitle?: string
|
||||
}>(),
|
||||
{
|
||||
subtitle: undefined,
|
||||
toolboxItems: undefined,
|
||||
activeStatus: null,
|
||||
toolboxTitle: 'Boîte à outils',
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:activeStatus': [status: string | null]
|
||||
}>()
|
||||
|
||||
const toolboxOpen = ref(false)
|
||||
|
||||
const statusCssMap: Record<string, string> = {
|
||||
draft: 'status-prepa',
|
||||
qualification: 'status-prepa',
|
||||
candidacy: 'status-prepa',
|
||||
voting: 'status-vote',
|
||||
review: 'status-vote',
|
||||
active: 'status-vigueur',
|
||||
executed: 'status-vigueur',
|
||||
completed: 'status-vigueur',
|
||||
closed: 'status-clos',
|
||||
archived: 'status-clos',
|
||||
revoked: 'status-clos',
|
||||
reporting: 'status-vote',
|
||||
}
|
||||
|
||||
function getStatusClass(status: StatusFilter): string {
|
||||
return status.cssClass || statusCssMap[status.id] || 'status-prepa'
|
||||
}
|
||||
|
||||
function toggleStatus(statusId: string) {
|
||||
if (props.activeStatus === statusId) {
|
||||
emit('update:activeStatus', null)
|
||||
}
|
||||
else {
|
||||
emit('update:activeStatus', statusId)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section">
|
||||
<!-- Header -->
|
||||
<div class="section__header">
|
||||
<div class="section__header-left">
|
||||
<h1 class="section__title">{{ title }}</h1>
|
||||
<p v-if="subtitle" class="section__subtitle">{{ subtitle }}</p>
|
||||
</div>
|
||||
<!-- Mobile toolbox trigger -->
|
||||
<button
|
||||
class="section__toolbox-fab lg:hidden"
|
||||
:class="{ 'section__toolbox-fab--active': toolboxOpen }"
|
||||
@click="toolboxOpen = true"
|
||||
>
|
||||
<UIcon name="i-lucide-wrench" />
|
||||
<span>Outils</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body: content + toolbox -->
|
||||
<div class="section__body">
|
||||
<div class="section__main">
|
||||
<!-- Status pills -->
|
||||
<div v-if="statuses.length > 0" class="section__pills">
|
||||
<button
|
||||
v-for="status in statuses"
|
||||
:key="status.id"
|
||||
type="button"
|
||||
class="status-pill"
|
||||
:class="[getStatusClass(status), { active: activeStatus === status.id }]"
|
||||
@click="toggleStatus(status.id)"
|
||||
>
|
||||
{{ status.label }}
|
||||
<span v-if="status.count > 0" class="section__pill-count">{{ status.count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.search" class="section__search">
|
||||
<slot name="search" />
|
||||
</div>
|
||||
<div class="section__content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop toolbox sidebar (≥1024px) -->
|
||||
<aside class="section__toolbox">
|
||||
<div class="section__toolbox-head">
|
||||
<UIcon name="i-lucide-wrench" class="section__toolbox-head-icon" />
|
||||
<span>{{ toolboxTitle }}</span>
|
||||
</div>
|
||||
<div class="section__toolbox-body">
|
||||
<div v-if="$slots.toolbox">
|
||||
<slot name="toolbox" />
|
||||
</div>
|
||||
<div v-else-if="toolboxItems && toolboxItems.length > 0">
|
||||
<ToolboxVignette
|
||||
v-for="(item, idx) in toolboxItems"
|
||||
:key="idx"
|
||||
:title="item.title"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="section__toolbox-empty">
|
||||
Aucun outil disponible
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Mobile toolbox: USlideover from right -->
|
||||
<USlideover
|
||||
v-model:open="toolboxOpen"
|
||||
side="right"
|
||||
:title="toolboxTitle"
|
||||
:ui="{
|
||||
width: 'max-w-sm',
|
||||
header: { padding: 'p-4' },
|
||||
body: { padding: 'p-4' },
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
<div class="section__toolbox-slideover">
|
||||
<div v-if="$slots.toolbox">
|
||||
<slot name="toolbox" />
|
||||
</div>
|
||||
<div v-else-if="toolboxItems && toolboxItems.length > 0">
|
||||
<ToolboxVignette
|
||||
v-for="(item, idx) in toolboxItems"
|
||||
:key="idx"
|
||||
:title="item.title"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="section__toolbox-empty">
|
||||
Aucun outil disponible
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</USlideover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.section { gap: 1.5rem; }
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.section__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.section__header-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.section__title {
|
||||
font-size: 1.375rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.section__title { font-size: 1.75rem; }
|
||||
}
|
||||
|
||||
.section__subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--mood-text-muted);
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.section__subtitle { font-size: 1rem; }
|
||||
}
|
||||
|
||||
/* Mobile toolbox trigger */
|
||||
.section__toolbox-fab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent);
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section__toolbox-fab:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px var(--mood-shadow);
|
||||
}
|
||||
|
||||
.section__toolbox-fab--active {
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
|
||||
/* Body layout */
|
||||
.section__body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.section__body {
|
||||
grid-template-columns: 1fr 17rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Status pills */
|
||||
.section__pills {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.section__pills::-webkit-scrollbar { display: none; }
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.section__pills {
|
||||
flex-wrap: wrap;
|
||||
overflow-x: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.section__pill-count {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 800;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.section__search {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.section__search { flex-direction: column; }
|
||||
}
|
||||
|
||||
.section__content { min-height: 12rem; }
|
||||
|
||||
/* Desktop toolbox sidebar */
|
||||
.section__toolbox {
|
||||
display: none;
|
||||
position: sticky;
|
||||
top: 4.5rem;
|
||||
flex-direction: column;
|
||||
background: var(--mood-surface);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.section__toolbox { display: flex; }
|
||||
}
|
||||
|
||||
.section__toolbox-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.875rem 1rem 0.625rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.section__toolbox-head-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.section__toolbox-body {
|
||||
padding: 0 0.75rem 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
max-height: calc(100vh - 8rem);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.section__toolbox-empty {
|
||||
font-size: 0.875rem;
|
||||
color: var(--mood-text-muted);
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* Slideover content */
|
||||
.section__toolbox-slideover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
272
frontend/app/components/WorkspaceSelector.vue
Normal file
272
frontend/app/components/WorkspaceSelector.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* WorkspaceSelector — Sélecteur de collectif / espace de travail.
|
||||
* Compartimentage multi-collectifs, multi-sites.
|
||||
* UI-only pour l'instant, prêt pour le backend (collective_id sur toutes les entités).
|
||||
*/
|
||||
|
||||
interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
icon: string
|
||||
role?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
// Mock data — sera remplacé par le store collectifs
|
||||
const workspaces: Workspace[] = [
|
||||
{
|
||||
id: 'g1-main',
|
||||
name: 'Duniter G1',
|
||||
slug: 'duniter-g1',
|
||||
icon: 'i-lucide-coins',
|
||||
role: 'Membre',
|
||||
color: 'accent',
|
||||
},
|
||||
{
|
||||
id: 'axiom',
|
||||
name: 'Axiom Team',
|
||||
slug: 'axiom-team',
|
||||
icon: 'i-lucide-layers',
|
||||
role: 'Admin',
|
||||
color: 'secondary',
|
||||
},
|
||||
]
|
||||
|
||||
const activeId = ref('g1-main')
|
||||
const isOpen = ref(false)
|
||||
|
||||
const active = computed(() => workspaces.find(w => w.id === activeId.value) ?? workspaces[0])
|
||||
|
||||
function selectWorkspace(id: string) {
|
||||
activeId.value = id
|
||||
isOpen.value = false
|
||||
// TODO: store.setActiveCollective(id) + refetch all data
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (containerRef.value && !containerRef.value.contains(e.target as Node)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="ws">
|
||||
<button class="ws__trigger" :class="{ 'ws__trigger--open': isOpen }" @click="isOpen = !isOpen">
|
||||
<div class="ws__icon" :class="`ws__icon--${active.color}`">
|
||||
<UIcon :name="active.icon" />
|
||||
</div>
|
||||
<span class="ws__name">{{ active.name }}</span>
|
||||
<UIcon name="i-lucide-chevrons-up-down" class="ws__caret" />
|
||||
</button>
|
||||
|
||||
<Transition name="dropdown">
|
||||
<div v-if="isOpen" class="ws__dropdown">
|
||||
<div class="ws__dropdown-header">
|
||||
Espace de travail
|
||||
</div>
|
||||
<div class="ws__items">
|
||||
<button
|
||||
v-for="ws in workspaces"
|
||||
:key="ws.id"
|
||||
class="ws__item"
|
||||
:class="{ 'ws__item--active': ws.id === activeId }"
|
||||
@click="selectWorkspace(ws.id)"
|
||||
>
|
||||
<div class="ws__item-icon" :class="`ws__icon--${ws.color}`">
|
||||
<UIcon :name="ws.icon" />
|
||||
</div>
|
||||
<div class="ws__item-info">
|
||||
<span class="ws__item-name">{{ ws.name }}</span>
|
||||
<span v-if="ws.role" class="ws__item-role">{{ ws.role }}</span>
|
||||
</div>
|
||||
<UIcon v-if="ws.id === activeId" name="i-lucide-check" class="ws__item-check" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="ws__dropdown-footer">
|
||||
<button class="ws__new-btn" disabled>
|
||||
<UIcon name="i-lucide-plus" />
|
||||
Nouveau collectif
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ws {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ws__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease;
|
||||
min-height: 2rem;
|
||||
max-width: 11rem;
|
||||
}
|
||||
|
||||
.ws__trigger:hover {
|
||||
background: color-mix(in srgb, var(--mood-accent-soft) 80%, var(--mood-accent) 20%);
|
||||
}
|
||||
|
||||
.ws__trigger--open {
|
||||
background: color-mix(in srgb, var(--mood-accent-soft) 60%, var(--mood-accent) 40%);
|
||||
}
|
||||
|
||||
.ws__icon {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ws__icon--accent { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||||
.ws__icon--secondary {
|
||||
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 20%, transparent);
|
||||
color: var(--mood-secondary, var(--mood-accent));
|
||||
}
|
||||
|
||||
.ws__name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ws__caret {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.ws__dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.375rem);
|
||||
left: 0;
|
||||
min-width: 13rem;
|
||||
background: var(--mood-surface);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 32px var(--mood-shadow);
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ws__dropdown-header {
|
||||
padding: 0.625rem 0.875rem 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.ws__items {
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ws__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.625rem 0.625rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ws__item:hover { background: var(--mood-accent-soft); }
|
||||
|
||||
.ws__item--active { background: var(--mood-accent-soft); }
|
||||
|
||||
.ws__item-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.ws__item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ws__item-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.ws__item-role {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.ws__item-check {
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ws__dropdown-footer {
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid var(--mood-accent-soft);
|
||||
}
|
||||
|
||||
.ws__new-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
background: none;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Transition */
|
||||
.dropdown-enter-active, .dropdown-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
transform-origin: top left;
|
||||
}
|
||||
.dropdown-enter-from, .dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-4px);
|
||||
}
|
||||
</style>
|
||||
659
frontend/app/components/toolbox/ContextMapper.vue
Normal file
659
frontend/app/components/toolbox/ContextMapper.vue
Normal file
@@ -0,0 +1,659 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* ContextMapper — Recommandeur de méthode de décision.
|
||||
* 4 questions de contexte → méthode optimale + justification.
|
||||
* Basé sur : Smith (WoT G1), Laloux (advice process), sociocracie.
|
||||
*/
|
||||
|
||||
interface Option { value: string; label: string; icon: string }
|
||||
interface Question { id: string; question: string; hint?: string; options: Option[] }
|
||||
|
||||
interface MethodRec {
|
||||
name: string
|
||||
icon: string
|
||||
tag: string
|
||||
tagColor: string
|
||||
description: string
|
||||
formula?: string
|
||||
when: string
|
||||
pros: string[]
|
||||
cons: string[]
|
||||
}
|
||||
|
||||
const questions: Question[] = [
|
||||
{
|
||||
id: 'urgency',
|
||||
question: 'Quelle est l\'urgence ?',
|
||||
hint: 'Le délai disponible avant que la décision soit nécessaire',
|
||||
options: [
|
||||
{ value: 'immediate', label: 'Immédiate', icon: 'i-lucide-zap' },
|
||||
{ value: 'short', label: '< 48h', icon: 'i-lucide-clock' },
|
||||
{ value: 'normal', label: 'Planifiable', icon: 'i-lucide-calendar' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stakes',
|
||||
question: 'Quel est l\'enjeu ?',
|
||||
hint: 'L\'impact et la réversibilité de la décision',
|
||||
options: [
|
||||
{ value: 'irreversible', label: 'Irréversible', icon: 'i-lucide-lock' },
|
||||
{ value: 'major', label: 'Majeur', icon: 'i-lucide-alert-triangle' },
|
||||
{ value: 'moderate', label: 'Modéré', icon: 'i-lucide-minus-circle' },
|
||||
{ value: 'minor', label: 'Mineur', icon: 'i-lucide-info' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'groupSize',
|
||||
question: 'Taille du groupe ?',
|
||||
hint: 'Nombre de personnes concernées ou habilitées à voter',
|
||||
options: [
|
||||
{ value: 'small', label: '< 10', icon: 'i-lucide-user' },
|
||||
{ value: 'medium', label: '10 – 100', icon: 'i-lucide-users' },
|
||||
{ value: 'large', label: '100+', icon: 'i-lucide-globe' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'nature',
|
||||
question: 'Nature de la décision ?',
|
||||
hint: 'Le type de compétence principalement sollicité',
|
||||
options: [
|
||||
{ value: 'technical', label: 'Technique', icon: 'i-lucide-cpu' },
|
||||
{ value: 'political', label: 'Politique', icon: 'i-lucide-landmark' },
|
||||
{ value: 'operational', label: 'Opérationnelle', icon: 'i-lucide-settings' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const answers = ref<Record<string, string>>({})
|
||||
const step = ref(0)
|
||||
const animating = ref(false)
|
||||
|
||||
const currentQuestion = computed(() => questions[step.value])
|
||||
const isComplete = computed(() => Object.keys(answers.value).length === questions.length)
|
||||
const progress = computed(() => (step.value / questions.length) * 100)
|
||||
|
||||
function selectAnswer(questionId: string, value: string) {
|
||||
answers.value = { ...answers.value, [questionId]: value }
|
||||
if (step.value < questions.length - 1) {
|
||||
animating.value = true
|
||||
setTimeout(() => {
|
||||
step.value++
|
||||
animating.value = false
|
||||
}, 160)
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (step.value > 0) step.value--
|
||||
}
|
||||
|
||||
function reset() {
|
||||
answers.value = {}
|
||||
step.value = 0
|
||||
}
|
||||
|
||||
const recommendation = computed((): MethodRec | null => {
|
||||
if (!isComplete.value) return null
|
||||
const { urgency, stakes, groupSize, nature } = answers.value
|
||||
|
||||
// Immediate → Advice process (Laloux)
|
||||
if (urgency === 'immediate') {
|
||||
return {
|
||||
name: 'Processus de sollicitation d\'avis',
|
||||
icon: 'i-lucide-message-circle',
|
||||
tag: 'Laloux / Teal',
|
||||
tagColor: 'teal',
|
||||
description: 'Le décideur identifié consulte les personnes expertes et impactées, puis décide seul et en rend compte. Rapide, non-bloquant, responsabilisant.',
|
||||
formula: 'Pas de vote — consultation libre → décision documentée → compte-rendu',
|
||||
when: 'Urgence opérationnelle, décision réversible, responsable clairement identifié.',
|
||||
pros: ['Rapide (< 2h)', 'Non-bloquant', 'Responsabilise le décideur'],
|
||||
cons: ['Requiert confiance dans le décideur', 'Pas de validation collective'],
|
||||
}
|
||||
}
|
||||
|
||||
// Technical + medium/large → Smith WoT
|
||||
if (nature === 'technical' && groupSize !== 'small') {
|
||||
return {
|
||||
name: 'Vote inertiel WoT + critère Smith',
|
||||
icon: 'i-lucide-network',
|
||||
tag: 'G1 standard',
|
||||
tagColor: 'accent',
|
||||
description: 'Vote communautaire avec seuil adaptatif à la participation. Le critère Smith garantit que la décision reflète l\'expertise des validateurs.',
|
||||
formula: 'R = C + B^W + (M + (1−M)·(1−(T/W)^G))·max(0,T−C)\nSeuil Smith : ⌈SmithWoT^S⌉',
|
||||
when: 'Décision technique nécessitant validation par les experts WoT (forgerons, CoTec).',
|
||||
pros: ['Validé par expertise', 'Adaptatif à la participation', 'Tracé on-chain'],
|
||||
cons: ['Durée minimum 7-30j', 'Complexité de la formule'],
|
||||
}
|
||||
}
|
||||
|
||||
// Irreversible + large → High threshold WoT
|
||||
if (stakes === 'irreversible' && groupSize === 'large') {
|
||||
return {
|
||||
name: 'Vote inertiel WoT (inertie forte)',
|
||||
icon: 'i-lucide-shield',
|
||||
tag: 'G1 renforcé',
|
||||
tagColor: 'secondary',
|
||||
description: 'Pour les décisions irréversibles à fort impact : seuil de quasi-unanimité si faible participation, majorité qualifiée avec forte participation.',
|
||||
formula: 'R = C + B^W + (M + (1−M)·(1−(T/W)^G))·max(0,T−C)\nParamètres : M=67%, G=0.3 (inertie forte)',
|
||||
when: 'Textes fondateurs, modifications structurelles, décisions irréversibles pour 100+ membres.',
|
||||
pros: ['Protection maximale', 'Légitimité forte', 'Résistant aux minorités actives'],
|
||||
cons: ['Durée longue (30+ jours)', 'Peut bloquer les évolutions nécessaires'],
|
||||
}
|
||||
}
|
||||
|
||||
// Small group → Sociocratic consent
|
||||
if (groupSize === 'small') {
|
||||
return {
|
||||
name: 'Consentement sociocratique',
|
||||
icon: 'i-lucide-check-circle-2',
|
||||
tag: 'Sociocracie',
|
||||
tagColor: 'tertiary',
|
||||
description: 'Adoption si aucune objection grave n\'est soulevée. Une objection grave = la décision nuit à la mission commune, pas juste une préférence personnelle.',
|
||||
formula: 'Adoptée si : aucune objection grave parmi les membres du cercle',
|
||||
when: 'Cercle de travail (< 10 membres), enjeu modéré, décision réversible.',
|
||||
pros: ['Rapide', 'Inclusif', 'Distingue objection grave et préférence'],
|
||||
cons: ['Ne convient pas aux grands groupes', 'Risque de pression sociale'],
|
||||
}
|
||||
}
|
||||
|
||||
// Political + medium → WoT majority
|
||||
if (nature === 'political') {
|
||||
return {
|
||||
name: 'Vote majoritaire WoT',
|
||||
icon: 'i-lucide-vote',
|
||||
tag: 'G1 standard',
|
||||
tagColor: 'accent',
|
||||
description: 'Vote binaire (Pour/Contre) avec seuil adaptatif à la participation WoT. Standard pour les décisions politiques de la communauté.',
|
||||
formula: 'R = C + B^W + (M + (1−M)·(1−(T/W)^G))·max(0,T−C)',
|
||||
when: 'Décision politique communautaire, participation variable, groupe >10.',
|
||||
pros: ['Standard WoT', 'Adaptatif', 'Tracé on-chain'],
|
||||
cons: ['Durée 7-30j', 'Participation faible possible'],
|
||||
}
|
||||
}
|
||||
|
||||
// Default: minor/operational
|
||||
return {
|
||||
name: 'Advice process + validation légère',
|
||||
icon: 'i-lucide-thumbs-up',
|
||||
tag: 'Léger',
|
||||
tagColor: 'teal',
|
||||
description: 'Pour les décisions mineures ou opérationnelles : consultation des parties concernées, décision par le responsable désigné, notification de la communauté.',
|
||||
formula: 'Consultation → Décision → Notification (sans vote formel)',
|
||||
when: 'Décision opérationnelle de faible impact, facilement réversible.',
|
||||
pros: ['Très rapide', 'Non-bloquant', 'Adapté à l\'opérationnel'],
|
||||
cons: ['Légitimité limitée', 'Ne convient pas aux enjeux majeurs'],
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ use: [name: string] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cmap">
|
||||
<!-- Header -->
|
||||
<div class="cmap__head">
|
||||
<UIcon name="i-lucide-compass" class="cmap__head-icon" />
|
||||
<div>
|
||||
<h3 class="cmap__title">Choisir une méthode</h3>
|
||||
<p class="cmap__subtitle">4 questions pour la méthode adaptée</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<Transition name="fade-up" mode="out-in">
|
||||
<div v-if="isComplete" key="result" class="cmap__result">
|
||||
<div class="cmap__result-header">
|
||||
<div class="cmap__result-icon">
|
||||
<UIcon :name="recommendation!.icon" />
|
||||
</div>
|
||||
<div class="cmap__result-info">
|
||||
<span class="cmap__result-tag" :class="`cmap__result-tag--${recommendation!.tagColor}`">
|
||||
{{ recommendation!.tag }}
|
||||
</span>
|
||||
<h4 class="cmap__result-name">{{ recommendation!.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="cmap__result-desc">{{ recommendation!.description }}</p>
|
||||
|
||||
<div v-if="recommendation!.formula" class="cmap__formula">
|
||||
<span class="cmap__formula-label">Formule</span>
|
||||
<pre class="cmap__formula-code">{{ recommendation!.formula }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="cmap__pros-cons">
|
||||
<div>
|
||||
<span class="cmap__pros-label">Pour</span>
|
||||
<ul class="cmap__list cmap__list--pro">
|
||||
<li v-for="p in recommendation!.pros" :key="p">{{ p }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cmap__cons-label">Contre</span>
|
||||
<ul class="cmap__list cmap__list--con">
|
||||
<li v-for="c in recommendation!.cons" :key="c">{{ c }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="cmap__when">
|
||||
<UIcon name="i-lucide-lightbulb" />
|
||||
{{ recommendation!.when }}
|
||||
</p>
|
||||
|
||||
<div class="cmap__result-actions">
|
||||
<button class="cmap__btn-reset" @click="reset">
|
||||
<UIcon name="i-lucide-refresh-cw" />
|
||||
Recommencer
|
||||
</button>
|
||||
<button class="cmap__btn-use" @click="emit('use', recommendation!.name)">
|
||||
<UIcon name="i-lucide-play" />
|
||||
Utiliser cette méthode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quiz -->
|
||||
<div v-else key="quiz" class="cmap__quiz">
|
||||
<!-- Progress -->
|
||||
<div class="cmap__progress">
|
||||
<div class="cmap__progress-bar" :style="{ width: `${progress}%` }" />
|
||||
</div>
|
||||
<span class="cmap__step-label">{{ step + 1 }} / {{ questions.length }}</span>
|
||||
|
||||
<!-- Question -->
|
||||
<Transition name="slide-right" mode="out-in">
|
||||
<div :key="step" class="cmap__question-block">
|
||||
<p class="cmap__question">{{ currentQuestion.question }}</p>
|
||||
<p v-if="currentQuestion.hint" class="cmap__hint">{{ currentQuestion.hint }}</p>
|
||||
|
||||
<div class="cmap__options">
|
||||
<button
|
||||
v-for="opt in currentQuestion.options"
|
||||
:key="opt.value"
|
||||
class="cmap__option"
|
||||
:class="{ 'cmap__option--selected': answers[currentQuestion.id] === opt.value }"
|
||||
@click="selectAnswer(currentQuestion.id, opt.value)"
|
||||
>
|
||||
<div class="cmap__option-icon">
|
||||
<UIcon :name="opt.icon" />
|
||||
</div>
|
||||
<span class="cmap__option-label">{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<button v-if="step > 0" class="cmap__back" @click="goBack">
|
||||
<UIcon name="i-lucide-chevron-left" />
|
||||
Retour
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cmap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cmap__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.cmap__head-icon {
|
||||
font-size: 1.375rem;
|
||||
color: var(--mood-accent);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.cmap__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cmap__subtitle {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Progress */
|
||||
.cmap__progress {
|
||||
height: 4px;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cmap__progress-bar {
|
||||
height: 100%;
|
||||
background: var(--mood-accent);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.cmap__step-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
/* Question */
|
||||
.cmap__question-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.cmap__question {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cmap__hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cmap__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cmap__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease;
|
||||
text-align: left;
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
.cmap__option:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px var(--mood-shadow);
|
||||
}
|
||||
|
||||
.cmap__option:active { transform: translateY(0); }
|
||||
|
||||
.cmap__option--selected {
|
||||
background: var(--mood-accent);
|
||||
}
|
||||
|
||||
.cmap__option--selected .cmap__option-icon,
|
||||
.cmap__option--selected .cmap__option-label {
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
|
||||
.cmap__option-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
flex-shrink: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.cmap__option--selected .cmap__option-icon {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
|
||||
.cmap__option-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.cmap__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0;
|
||||
transition: color 0.1s ease;
|
||||
}
|
||||
.cmap__back:hover { color: var(--mood-text); }
|
||||
|
||||
/* Result */
|
||||
.cmap__result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.cmap__result-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.cmap__result-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.cmap__result-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.cmap__result-tag {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
}
|
||||
.cmap__result-tag--accent {
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
.cmap__result-tag--teal {
|
||||
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
|
||||
color: var(--mood-success);
|
||||
}
|
||||
.cmap__result-tag--secondary {
|
||||
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 15%, transparent);
|
||||
color: var(--mood-secondary, var(--mood-accent));
|
||||
}
|
||||
.cmap__result-tag--tertiary {
|
||||
background: color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 15%, transparent);
|
||||
color: var(--mood-tertiary, var(--mood-accent));
|
||||
}
|
||||
|
||||
.cmap__result-name {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.cmap__result-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cmap__formula {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
padding: 0.625rem 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.cmap__formula-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
|
||||
.cmap__formula-code {
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cmap__pros-cons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cmap__pros-label,
|
||||
.cmap__cons-label {
|
||||
display: block;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.cmap__pros-label { color: var(--mood-success); }
|
||||
.cmap__cons-label { color: var(--mood-error); }
|
||||
|
||||
.cmap__list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.cmap__list li {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
padding-left: 0.875rem;
|
||||
position: relative;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cmap__list--pro li::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--mood-success);
|
||||
font-weight: 700;
|
||||
font-size: 0.5rem;
|
||||
top: 0.2em;
|
||||
}
|
||||
|
||||
.cmap__list--con li::before {
|
||||
content: '·';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--mood-error);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cmap__when {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cmap__result-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cmap__btn-reset {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
.cmap__btn-reset:hover { transform: translateY(-1px); color: var(--mood-text); }
|
||||
|
||||
.cmap__btn-use {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1.125rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent-text);
|
||||
background: var(--mood-accent);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
}
|
||||
.cmap__btn-use:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--mood-shadow);
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-up-enter-active, .fade-up-leave-active { transition: all 0.2s ease; }
|
||||
.fade-up-enter-from { opacity: 0; transform: translateY(8px); }
|
||||
.fade-up-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
|
||||
.slide-right-enter-active, .slide-right-leave-active { transition: all 0.16s ease; }
|
||||
.slide-right-enter-from { opacity: 0; transform: translateX(12px); }
|
||||
.slide-right-leave-to { opacity: 0; transform: translateX(-8px); }
|
||||
</style>
|
||||
666
frontend/app/components/toolbox/SocioElection.vue
Normal file
666
frontend/app/components/toolbox/SocioElection.vue
Normal file
@@ -0,0 +1,666 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SocioElection — Guide processus d'élection sociocratique.
|
||||
* 6 étapes canoniques + advice process Laloux + clarté de rôle.
|
||||
* Référence : "La Sociocracie" (Robertson), "Reinventing Organizations" (Laloux).
|
||||
*/
|
||||
|
||||
interface Step {
|
||||
num: number
|
||||
title: string
|
||||
actor: string
|
||||
duration: string
|
||||
icon: string
|
||||
description: string
|
||||
tips: string[]
|
||||
pitfall?: string
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
num: 1,
|
||||
title: 'Clarifier le rôle',
|
||||
actor: 'Facilitateur + cercle',
|
||||
duration: '10-15 min',
|
||||
icon: 'i-lucide-clipboard-list',
|
||||
description: 'Définir ensemble la mission du rôle, ses domaines d\'autorité, ses redevabilités et la durée du mandat. Le rôle précède la personne.',
|
||||
tips: [
|
||||
'Distinguer redevabilités (obligations) et autorité (domaine de décision)',
|
||||
'Fixer une durée standard (ex: 1 an renouvelable)',
|
||||
'Identifier les compétences nécessaires — pas souhaitables',
|
||||
],
|
||||
pitfall: 'Ne pas définir le rôle sur mesure pour un candidat déjà imaginé.',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
title: 'Nommer en silence',
|
||||
actor: 'Tous les membres',
|
||||
duration: '3-5 min',
|
||||
icon: 'i-lucide-pencil',
|
||||
description: 'Chacun écrit sur papier le nom d\'une personne (y compris soi-même) et la raison principale de son choix. En silence, sans influence mutuelle.',
|
||||
tips: [
|
||||
'Pas de discussion pendant cette étape',
|
||||
'S\'auto-nommer est bienvenu et valorisé',
|
||||
'Une seule nomination par personne',
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
title: 'Recueillir les nominations',
|
||||
actor: 'Facilitateur',
|
||||
duration: '5-10 min',
|
||||
icon: 'i-lucide-list-checks',
|
||||
description: 'Le facilitateur lit chaque nomination à voix haute avec la raison. Pas de commentaire, pas de débat. Pure collecte.',
|
||||
tips: [
|
||||
'Lire nom + raison tels qu\'écrits',
|
||||
'Le facilitateur lit aussi sa propre nomination',
|
||||
'Compter et afficher les nominations',
|
||||
],
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
title: 'Argumenter',
|
||||
actor: 'Chaque membre',
|
||||
duration: '1-2 min / personne',
|
||||
icon: 'i-lucide-message-square',
|
||||
description: 'Chaque membre peut changer sa nomination et expliquer pourquoi (brièvement). Tour de table structuré, pas de croisements.',
|
||||
tips: [
|
||||
'1 minute maximum par personne',
|
||||
'Argumenter pour, pas contre',
|
||||
'Les candidats s\'expriment aussi brièvement',
|
||||
],
|
||||
pitfall: 'Éviter les longues plaidoiries — la clarté du rôle doit guider.',
|
||||
},
|
||||
{
|
||||
num: 5,
|
||||
title: 'Lever les objections',
|
||||
actor: 'Facilitateur + cercle',
|
||||
duration: '5-15 min',
|
||||
icon: 'i-lucide-shield-check',
|
||||
description: 'Le facilitateur propose l\'élection de la personne la plus nommée. Silence = consentement. Une objection grave peut être soulevée et traitée.',
|
||||
tips: [
|
||||
'Objection grave ≠ préférence — nuit-elle à la mission du cercle ?',
|
||||
'Une objection peut mener à reconsidérer une candidature',
|
||||
'L\'élu·e peut décliner — c\'est légitime',
|
||||
],
|
||||
pitfall: 'Une objection n\'est pas un veto — elle doit être travaillée collectivement.',
|
||||
},
|
||||
{
|
||||
num: 6,
|
||||
title: 'Célébrer',
|
||||
actor: 'Tous',
|
||||
duration: '2-3 min',
|
||||
icon: 'i-lucide-star',
|
||||
description: 'L\'élection est proclamée. L\'élu·e remercie et s\'engage publiquement. La communauté accueille le nouveau rôle.',
|
||||
tips: [
|
||||
'Documenter l\'élection (date, durée, personnes présentes)',
|
||||
'Annoncer à la communauté au sens large',
|
||||
'Fixer la prochaine évaluation du rôle',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const expandedStep = ref<number | null>(null)
|
||||
|
||||
function toggleStep(num: number) {
|
||||
expandedStep.value = expandedStep.value === num ? null : num
|
||||
}
|
||||
|
||||
// Advice process (Laloux)
|
||||
const adviceSteps = [
|
||||
{ icon: 'i-lucide-search', text: 'Identifier les personnes expertes ET impactées' },
|
||||
{ icon: 'i-lucide-message-circle', text: 'Les consulter — écouter vraiment' },
|
||||
{ icon: 'i-lucide-user-check', text: 'Décider seul·e, en intégrant les avis reçus' },
|
||||
{ icon: 'i-lucide-file-text', text: 'Documenter et communiquer la décision + raisons' },
|
||||
]
|
||||
|
||||
// Role clarity framework
|
||||
interface RoleAxis {
|
||||
label: string
|
||||
icon: string
|
||||
question: string
|
||||
example: string
|
||||
}
|
||||
|
||||
const roleAxes: RoleAxis[] = [
|
||||
{
|
||||
label: 'Mission',
|
||||
icon: 'i-lucide-target',
|
||||
question: 'Pourquoi ce rôle existe-t-il ?',
|
||||
example: 'Assurer la disponibilité des nœuds validateurs 24h/24',
|
||||
},
|
||||
{
|
||||
label: 'Domaine',
|
||||
icon: 'i-lucide-shield',
|
||||
question: 'Sur quoi a-t-il autorité exclusive ?',
|
||||
example: 'Configuration des serveurs de forge, rotation des clés',
|
||||
},
|
||||
{
|
||||
label: 'Redevabilités',
|
||||
icon: 'i-lucide-check-square',
|
||||
question: 'Quelles activités doit-il assurer ?',
|
||||
example: 'Publier un rapport mensuel, alerter en cas d\'incident',
|
||||
},
|
||||
{
|
||||
label: 'Durée',
|
||||
icon: 'i-lucide-calendar',
|
||||
question: 'Pour combien de temps ?',
|
||||
example: '1 an, renouvelable une fois, réévaluation à 6 mois',
|
||||
},
|
||||
]
|
||||
|
||||
const activeTab = ref<'election' | 'advice' | 'role'>('election')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="se">
|
||||
<!-- Tabs -->
|
||||
<div class="se__tabs">
|
||||
<button
|
||||
class="se__tab"
|
||||
:class="{ 'se__tab--active': activeTab === 'election' }"
|
||||
@click="activeTab = 'election'"
|
||||
>
|
||||
<UIcon name="i-lucide-users" />
|
||||
Élection
|
||||
</button>
|
||||
<button
|
||||
class="se__tab"
|
||||
:class="{ 'se__tab--active': activeTab === 'advice' }"
|
||||
@click="activeTab = 'advice'"
|
||||
>
|
||||
<UIcon name="i-lucide-message-circle" />
|
||||
Conseil
|
||||
</button>
|
||||
<button
|
||||
class="se__tab"
|
||||
:class="{ 'se__tab--active': activeTab === 'role' }"
|
||||
@click="activeTab = 'role'"
|
||||
>
|
||||
<UIcon name="i-lucide-clipboard-list" />
|
||||
Rôle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Election sociocratique -->
|
||||
<div v-if="activeTab === 'election'" class="se__panel">
|
||||
<p class="se__intro">
|
||||
Processus en 6 étapes garantissant que l'élection repose sur la clarté du rôle
|
||||
et le consentement collectif — pas sur la popularité.
|
||||
</p>
|
||||
|
||||
<div class="se__steps">
|
||||
<div
|
||||
v-for="s in steps"
|
||||
:key="s.num"
|
||||
class="se__step"
|
||||
:class="{ 'se__step--open': expandedStep === s.num }"
|
||||
>
|
||||
<button class="se__step-head" @click="toggleStep(s.num)">
|
||||
<div class="se__step-num">{{ s.num }}</div>
|
||||
<div class="se__step-icon">
|
||||
<UIcon :name="s.icon" />
|
||||
</div>
|
||||
<div class="se__step-info">
|
||||
<span class="se__step-title">{{ s.title }}</span>
|
||||
<span class="se__step-meta">{{ s.actor }} · {{ s.duration }}</span>
|
||||
</div>
|
||||
<UIcon
|
||||
:name="expandedStep === s.num ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||
class="se__step-toggle"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-if="expandedStep === s.num" class="se__step-body">
|
||||
<p class="se__step-desc">{{ s.description }}</p>
|
||||
<ul class="se__step-tips">
|
||||
<li v-for="tip in s.tips" :key="tip">{{ tip }}</li>
|
||||
</ul>
|
||||
<div v-if="s.pitfall" class="se__step-pitfall">
|
||||
<UIcon name="i-lucide-alert-triangle" />
|
||||
{{ s.pitfall }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advice process -->
|
||||
<div v-if="activeTab === 'advice'" class="se__panel">
|
||||
<div class="se__advice-header">
|
||||
<span class="se__advice-tag">Laloux / Teal</span>
|
||||
<h4 class="se__advice-title">Processus de sollicitation d'avis</h4>
|
||||
<p class="se__advice-subtitle">
|
||||
Toute personne peut prendre une décision — à condition d'avoir d'abord
|
||||
consulté les experts et les impactés.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="se__advice-steps">
|
||||
<div v-for="(as, i) in adviceSteps" :key="i" class="se__advice-step">
|
||||
<div class="se__advice-dot">
|
||||
<UIcon :name="as.icon" />
|
||||
</div>
|
||||
<span class="se__advice-text">{{ as.text }}</span>
|
||||
<div v-if="i < adviceSteps.length - 1" class="se__advice-line" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="se__advice-rule">
|
||||
<UIcon name="i-lucide-lightbulb" class="se__advice-rule-icon" />
|
||||
<div>
|
||||
<strong>Règle d'or :</strong> plus la décision est impactante, plus il faut
|
||||
consulter largement. Mais la décision finale appartient toujours à celui ou
|
||||
celle qui l'a initiée.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="se__advice-when">
|
||||
<div class="se__advice-when-item se__advice-when-item--yes">
|
||||
<span class="se__advice-when-label">Adapter pour</span>
|
||||
<ul>
|
||||
<li>Décisions urgentes</li>
|
||||
<li>Rôles bien définis</li>
|
||||
<li>Culture de confiance</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="se__advice-when-item se__advice-when-item--no">
|
||||
<span class="se__advice-when-label">Éviter si</span>
|
||||
<ul>
|
||||
<li>Décision irréversible</li>
|
||||
<li>Groupe > 100 personnes</li>
|
||||
<li>Enjeu fondateur</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role clarity -->
|
||||
<div v-if="activeTab === 'role'" class="se__panel">
|
||||
<p class="se__intro">
|
||||
Un rôle bien défini évite les zones grises, les conflits d'autorité
|
||||
et les mandats flous. Quatre axes suffisent.
|
||||
</p>
|
||||
|
||||
<div class="se__role-axes">
|
||||
<div v-for="axis in roleAxes" :key="axis.label" class="se__role-axis">
|
||||
<div class="se__role-axis-icon">
|
||||
<UIcon :name="axis.icon" />
|
||||
</div>
|
||||
<div class="se__role-axis-body">
|
||||
<span class="se__role-axis-label">{{ axis.label }}</span>
|
||||
<p class="se__role-axis-question">{{ axis.question }}</p>
|
||||
<p class="se__role-axis-example">ex: {{ axis.example }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="se__role-tip">
|
||||
<UIcon name="i-lucide-info" />
|
||||
<span>Un rôle n'est pas une fiche de poste. Il peut évoluer au prochain cycle
|
||||
de gouvernance sans changer la personne qui le tient.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.se { display: flex; flex-direction: column; gap: 1rem; }
|
||||
|
||||
/* Tabs */
|
||||
.se__tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.se__tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text-muted);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.se__tab--active {
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
box-shadow: 0 1px 4px var(--mood-shadow);
|
||||
}
|
||||
|
||||
.se__panel { display: flex; flex-direction: column; gap: 0.875rem; }
|
||||
|
||||
.se__intro {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Steps */
|
||||
.se__steps { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
|
||||
.se__step {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.se__step--open { background: var(--mood-surface); }
|
||||
|
||||
.se__step-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 0.875rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.se__step-num {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.se__step-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.se__step-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.se__step-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.se__step-meta {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.se__step-toggle {
|
||||
color: var(--mood-text-muted);
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.se__step-body {
|
||||
padding: 0 0.875rem 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.se__step-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.se__step-tips {
|
||||
margin: 0;
|
||||
padding: 0 0 0 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
list-style-type: disc;
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
.se__step-tips li::marker { color: var(--mood-accent); }
|
||||
|
||||
.se__step-pitfall {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
background: color-mix(in srgb, var(--mood-error) 10%, transparent);
|
||||
border-radius: 8px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-error);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Advice */
|
||||
.se__advice-header { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
|
||||
.se__advice-tag {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
|
||||
color: var(--mood-success);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.se__advice-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.se__advice-subtitle {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.se__advice-steps { display: flex; flex-direction: column; gap: 0; }
|
||||
|
||||
.se__advice-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
position: relative;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.se__advice-dot {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.se__advice-text {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text);
|
||||
padding-top: 0.375rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.se__advice-line {
|
||||
position: absolute;
|
||||
left: calc(1rem - 1px);
|
||||
top: calc(0.5rem + 2rem);
|
||||
width: 2px;
|
||||
height: calc(100% - 2rem + 0.5rem);
|
||||
background: color-mix(in srgb, var(--mood-accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.se__advice-rule {
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.se__advice-rule-icon { color: var(--mood-accent); flex-shrink: 0; margin-top: 0.1rem; }
|
||||
.se__advice-rule strong { color: var(--mood-text); }
|
||||
|
||||
.se__advice-when {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.se__advice-when-item {
|
||||
padding: 0.625rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.se__advice-when-item--yes {
|
||||
background: color-mix(in srgb, var(--mood-success) 10%, transparent);
|
||||
}
|
||||
|
||||
.se__advice-when-item--no {
|
||||
background: color-mix(in srgb, var(--mood-error) 8%, transparent);
|
||||
}
|
||||
|
||||
.se__advice-when-label {
|
||||
display: block;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.se__advice-when-item--yes .se__advice-when-label { color: var(--mood-success); }
|
||||
.se__advice-when-item--no .se__advice-when-label { color: var(--mood-error); }
|
||||
|
||||
.se__advice-when-item ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 0.875rem;
|
||||
color: var(--mood-text-muted);
|
||||
list-style-type: disc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
/* Role */
|
||||
.se__role-axes { display: flex; flex-direction: column; gap: 0.625rem; }
|
||||
|
||||
.se__role-axis {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.se__role-axis-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-surface);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.se__role-axis-body { flex: 1; min-width: 0; }
|
||||
|
||||
.se__role-axis-label {
|
||||
display: block;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--mood-accent);
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.se__role-axis-question {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.se__role-axis-example {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0.125rem 0 0;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.se__role-tip {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.5;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Expand transition */
|
||||
.expand-enter-active, .expand-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.expand-enter-from, .expand-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.expand-enter-to, .expand-leave-from {
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
551
frontend/app/components/toolbox/WorkflowMilestones.vue
Normal file
551
frontend/app/components/toolbox/WorkflowMilestones.vue
Normal file
@@ -0,0 +1,551 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* WorkflowMilestones — 11 jalons de protocole de fonctionnement.
|
||||
* Sélectif et qualitatif : ce qui fait la différence entre un protocole
|
||||
* qui tient et un qui dérive.
|
||||
* Référence : g1vote, sociocracie, Laloux, Elinor Ostrom (gouvernance des communs).
|
||||
*/
|
||||
|
||||
interface Milestone {
|
||||
num: number
|
||||
name: string
|
||||
icon: string
|
||||
actor: string
|
||||
duration: { min: string; standard: string; major: string }
|
||||
description: string
|
||||
essential: boolean
|
||||
tip?: string
|
||||
ostrom?: string
|
||||
}
|
||||
|
||||
const milestones: Milestone[] = [
|
||||
{
|
||||
num: 1,
|
||||
name: 'Prise d\'initiative',
|
||||
icon: 'i-lucide-lightbulb',
|
||||
actor: 'Tout membre',
|
||||
duration: { min: '—', standard: '1-2j', major: '1-2j' },
|
||||
description: 'Formaliser l\'intention : quel problème, quel besoin, quelle cible visée. Nommer un·e porteur·euse responsable.',
|
||||
essential: true,
|
||||
tip: 'Une initiative sans porteur identifié ne décolle pas. La responsabilité individuelle est le premier jalon.',
|
||||
ostrom: 'Principe 1 — Frontières claires : qui est concerné, pourquoi.',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
name: 'Processus d\'avis (advice)',
|
||||
icon: 'i-lucide-message-circle',
|
||||
actor: 'Porteur + experts + impactés',
|
||||
duration: { min: '1j', standard: '3-7j', major: '7-14j' },
|
||||
description: 'Consulter les personnes qui ont l\'expertise ET celles qui seront impactées. Écouter vraiment, intégrer ou expliquer pourquoi on n\'intègre pas.',
|
||||
essential: true,
|
||||
tip: 'Ce jalon est souvent escamoté. C\'est la principale cause d\'échec ou de résistance en implémentation.',
|
||||
ostrom: 'Principe 5 — Résolution des conflits accessible et peu coûteuse.',
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
name: 'Rédaction + amendements',
|
||||
icon: 'i-lucide-file-edit',
|
||||
actor: 'Porteur + communauté',
|
||||
duration: { min: '1-2j', standard: '3-7j', major: '7-21j' },
|
||||
description: 'Rédiger la proposition formelle. Ouvrir une période d\'amendements publics. Intégrer les modifications acceptées, rejeter les autres avec justification.',
|
||||
essential: true,
|
||||
tip: 'Distinguer amendements substantiels (re-vote possible) et de forme (porteur décide).',
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
name: 'Qualification technique',
|
||||
icon: 'i-lucide-shield-check',
|
||||
actor: 'Comité technique (si applicable)',
|
||||
duration: { min: '—', standard: '2-5j', major: '5-10j' },
|
||||
description: 'Pour les décisions techniques : revue par les experts désignés. Évaluation de faisabilité, risques, impact. Avis formel (non bloquant, sauf veto défini).',
|
||||
essential: false,
|
||||
tip: 'Optionnel selon la nature de la décision. Systématique pour les Runtime Upgrades.',
|
||||
},
|
||||
{
|
||||
num: 5,
|
||||
name: 'Ouverture du vote',
|
||||
icon: 'i-lucide-vote',
|
||||
actor: 'Porteur + plateforme',
|
||||
duration: { min: '—', standard: '1j', major: '1j' },
|
||||
description: 'Publier la proposition finale. Notifier la communauté. Ouvrir la session de vote avec les paramètres définis (protocole, formule, durée).',
|
||||
essential: true,
|
||||
tip: 'L\'ouverture doit être annoncée à l\'avance (délai de préavis selon règlement).',
|
||||
},
|
||||
{
|
||||
num: 6,
|
||||
name: 'Phase de vote',
|
||||
icon: 'i-lucide-bar-chart-2',
|
||||
actor: 'Membres habilités',
|
||||
duration: { min: '3j', standard: '7-14j', major: '21-30j' },
|
||||
description: 'Les membres habilités votent selon le protocole. Seuil de participation minimal surveillé. Résultats intermédiaires visibles (ou non, selon le protocole).',
|
||||
essential: true,
|
||||
ostrom: 'Principe 3 — Choix collectifs : ceux qui sont concernés participent aux décisions.',
|
||||
},
|
||||
{
|
||||
num: 7,
|
||||
name: 'Contrôle du quorum',
|
||||
icon: 'i-lucide-check-circle',
|
||||
actor: 'Plateforme + porteur',
|
||||
duration: { min: '—', standard: '—', major: '—' },
|
||||
description: 'Vérifier que le quorum minimum est atteint avant clôture. Si non atteint : prolonger, relancer, ou annuler selon les règles préétablies.',
|
||||
essential: true,
|
||||
tip: 'Définir à l\'avance le quorum et la procédure si non atteint — évite les ambiguïtés.',
|
||||
ostrom: 'Principe 4 — Supervision des règles par les membres.',
|
||||
},
|
||||
{
|
||||
num: 8,
|
||||
name: 'Proclamation des résultats',
|
||||
icon: 'i-lucide-megaphone',
|
||||
actor: 'Plateforme + porteur',
|
||||
duration: { min: '—', standard: '1j', major: '1j' },
|
||||
description: 'Annoncer le résultat officiel avec les chiffres détaillés (votes pour, contre, abstentions, taux participation, seuil requis). Archiver on-chain si adopté.',
|
||||
essential: true,
|
||||
tip: 'La transparence des résultats est aussi importante que le résultat lui-même.',
|
||||
ostrom: 'Principe 8 — Gouvernance emboîtée : résultats remontés aux niveaux supérieurs.',
|
||||
},
|
||||
{
|
||||
num: 9,
|
||||
name: 'Mise en application',
|
||||
icon: 'i-lucide-play-circle',
|
||||
actor: 'Porteur + implémenteurs',
|
||||
duration: { min: '—', standard: 'Variable', major: 'Variable' },
|
||||
description: 'Planifier l\'application effective de la décision. Désigner les responsables. Fixer des jalons d\'implémentation si complexe.',
|
||||
essential: true,
|
||||
tip: 'Une décision adoptée mais non implémentée érode la confiance dans le processus.',
|
||||
},
|
||||
{
|
||||
num: 10,
|
||||
name: 'Suivi et accountability',
|
||||
icon: 'i-lucide-activity',
|
||||
actor: 'Porteur + communauté',
|
||||
duration: { min: '—', standard: 'Continu', major: 'Continu' },
|
||||
description: 'Rapports réguliers sur l\'avancement. Signalement des écarts. Mécanisme de remontée si la décision produit des effets inattendus.',
|
||||
essential: false,
|
||||
tip: 'Intégrer dans le prochain cycle de gouvernance si des ajustements s\'imposent.',
|
||||
ostrom: 'Principe 4 — Surveillance continue des comportements et résultats.',
|
||||
},
|
||||
{
|
||||
num: 11,
|
||||
name: 'Rétrospective',
|
||||
icon: 'i-lucide-rotate-ccw',
|
||||
actor: 'Cercle concerné',
|
||||
duration: { min: '—', standard: '1-2h', major: '1-2j' },
|
||||
description: 'Évaluer : le processus a-t-il bien fonctionné ? La décision produit-elle les effets attendus ? Quoi améliorer pour la prochaine fois ?',
|
||||
essential: false,
|
||||
tip: 'La rétrospective est le moteur d\'amélioration du protocole lui-même (méta-gouvernance).',
|
||||
ostrom: 'Principe 7 — Reconnaissance externe de l\'organisation par des autorités supérieures.',
|
||||
},
|
||||
]
|
||||
|
||||
const showOstrom = ref(false)
|
||||
const activeDecisionType = ref<'minor' | 'standard' | 'major'>('standard')
|
||||
|
||||
const decisionTypes = [
|
||||
{ value: 'minor', label: 'Mineur', color: 'teal' },
|
||||
{ value: 'standard', label: 'Standard', color: 'accent' },
|
||||
{ value: 'major', label: 'Majeur', color: 'secondary' },
|
||||
]
|
||||
|
||||
const essentialMilestones = computed(() =>
|
||||
milestones.filter(m => m.essential),
|
||||
)
|
||||
|
||||
const optionalMilestones = computed(() =>
|
||||
milestones.filter(m => !m.essential),
|
||||
)
|
||||
|
||||
const totalDuration = computed(() => {
|
||||
const type = activeDecisionType.value
|
||||
const durations = {
|
||||
minor: '5-10 jours',
|
||||
standard: '14-30 jours',
|
||||
major: '45-90 jours',
|
||||
}
|
||||
return durations[type]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wm">
|
||||
<!-- Header -->
|
||||
<div class="wm__header">
|
||||
<h3 class="wm__title">Jalons de protocole</h3>
|
||||
<p class="wm__subtitle">
|
||||
11 jalons, dont 7 indispensables. Durées recommandées selon le type de décision.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Decision type selector -->
|
||||
<div class="wm__type-selector">
|
||||
<button
|
||||
v-for="dt in decisionTypes"
|
||||
:key="dt.value"
|
||||
class="wm__type-btn"
|
||||
:class="[
|
||||
`wm__type-btn--${dt.color}`,
|
||||
{ 'wm__type-btn--active': activeDecisionType === dt.value },
|
||||
]"
|
||||
@click="activeDecisionType = dt.value as 'minor' | 'standard' | 'major'"
|
||||
>
|
||||
{{ dt.label }}
|
||||
</button>
|
||||
<span class="wm__total-duration">≈ {{ totalDuration }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Essential milestones -->
|
||||
<div class="wm__section">
|
||||
<div class="wm__section-label">
|
||||
<span class="wm__section-badge wm__section-badge--essential">7 essentiels</span>
|
||||
</div>
|
||||
<div class="wm__milestones">
|
||||
<div
|
||||
v-for="m in essentialMilestones"
|
||||
:key="m.num"
|
||||
class="wm__milestone wm__milestone--essential"
|
||||
>
|
||||
<div class="wm__milestone-left">
|
||||
<div class="wm__milestone-num">{{ m.num }}</div>
|
||||
<div v-if="m.num < milestones.length" class="wm__milestone-line" />
|
||||
</div>
|
||||
<div class="wm__milestone-icon">
|
||||
<UIcon :name="m.icon" />
|
||||
</div>
|
||||
<div class="wm__milestone-body">
|
||||
<div class="wm__milestone-head">
|
||||
<span class="wm__milestone-name">{{ m.name }}</span>
|
||||
<span class="wm__milestone-duration">
|
||||
{{ m.duration[activeDecisionType] || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="wm__milestone-desc">{{ m.description }}</p>
|
||||
<div v-if="m.tip" class="wm__milestone-tip">
|
||||
<UIcon name="i-lucide-lightbulb" />
|
||||
{{ m.tip }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Optional milestones -->
|
||||
<div class="wm__section">
|
||||
<div class="wm__section-label">
|
||||
<span class="wm__section-badge wm__section-badge--optional">4 contextuels</span>
|
||||
</div>
|
||||
<div class="wm__milestones">
|
||||
<div
|
||||
v-for="m in optionalMilestones"
|
||||
:key="m.num"
|
||||
class="wm__milestone wm__milestone--optional"
|
||||
>
|
||||
<div class="wm__milestone-left">
|
||||
<div class="wm__milestone-num wm__milestone-num--optional">{{ m.num }}</div>
|
||||
</div>
|
||||
<div class="wm__milestone-icon wm__milestone-icon--optional">
|
||||
<UIcon :name="m.icon" />
|
||||
</div>
|
||||
<div class="wm__milestone-body">
|
||||
<div class="wm__milestone-head">
|
||||
<span class="wm__milestone-name">{{ m.name }}</span>
|
||||
<span class="wm__milestone-duration">
|
||||
{{ m.duration[activeDecisionType] || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="wm__milestone-desc">{{ m.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ostrom toggle -->
|
||||
<button class="wm__ostrom-toggle" @click="showOstrom = !showOstrom">
|
||||
<UIcon name="i-lucide-book-open" />
|
||||
<span>Principes Ostrom appliqués</span>
|
||||
<UIcon :name="showOstrom ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-if="showOstrom" class="wm__ostrom">
|
||||
<p class="wm__ostrom-intro">
|
||||
Elinor Ostrom (Nobel 2009) a identifié 8 principes pour la gouvernance
|
||||
durable des communs. Les jalons ci-dessus les incarnent.
|
||||
</p>
|
||||
<div class="wm__ostrom-items">
|
||||
<div
|
||||
v-for="m in milestones.filter(x => x.ostrom)"
|
||||
:key="m.num"
|
||||
class="wm__ostrom-item"
|
||||
>
|
||||
<span class="wm__ostrom-jalon">Jalon {{ m.num }}</span>
|
||||
<span class="wm__ostrom-text">{{ m.ostrom }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wm { display: flex; flex-direction: column; gap: 1rem; }
|
||||
|
||||
.wm__header { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
|
||||
.wm__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wm__subtitle {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Type selector */
|
||||
.wm__type-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wm__type-btn {
|
||||
padding: 0.375rem 0.875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-text-muted);
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
|
||||
.wm__type-btn--accent.wm__type-btn--active {
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
|
||||
.wm__type-btn--teal.wm__type-btn--active {
|
||||
background: color-mix(in srgb, var(--mood-success) 20%, transparent);
|
||||
color: var(--mood-success);
|
||||
}
|
||||
|
||||
.wm__type-btn--secondary.wm__type-btn--active {
|
||||
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 20%, transparent);
|
||||
color: var(--mood-secondary, var(--mood-accent));
|
||||
}
|
||||
|
||||
.wm__total-duration {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.wm__section { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
|
||||
.wm__section-label { display: flex; align-items: center; gap: 0.5rem; }
|
||||
|
||||
.wm__section-badge {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.wm__section-badge--essential {
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
|
||||
.wm__section-badge--optional {
|
||||
background: color-mix(in srgb, var(--mood-text-muted) 12%, transparent);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
/* Milestones */
|
||||
.wm__milestones { display: flex; flex-direction: column; gap: 0; }
|
||||
|
||||
.wm__milestone {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.wm__milestone-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 1.375rem;
|
||||
}
|
||||
|
||||
.wm__milestone-num {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wm__milestone-num--optional {
|
||||
background: color-mix(in srgb, var(--mood-text-muted) 20%, transparent);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.wm__milestone-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 1.25rem;
|
||||
background: color-mix(in srgb, var(--mood-accent) 20%, transparent);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.wm__milestone-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.wm__milestone-icon--optional {
|
||||
background: color-mix(in srgb, var(--mood-text-muted) 10%, transparent);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.wm__milestone-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.wm__milestone-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wm__milestone-name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.wm__milestone--optional .wm__milestone-name {
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.wm__milestone-duration {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
color: var(--mood-accent);
|
||||
background: var(--mood-accent-soft);
|
||||
padding: 1px 6px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.wm__milestone-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0.125rem 0 0;
|
||||
}
|
||||
|
||||
.wm__milestone-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.375rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: color-mix(in srgb, var(--mood-accent) 8%, transparent);
|
||||
border-radius: 8px;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-accent);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Ostrom */
|
||||
.wm__ostrom-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-text-muted);
|
||||
transition: color 0.12s ease;
|
||||
text-align: left;
|
||||
}
|
||||
.wm__ostrom-toggle:hover { color: var(--mood-text); }
|
||||
.wm__ostrom-toggle .i-lucide-book-open { color: var(--mood-accent); }
|
||||
|
||||
.wm__ostrom {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 12px;
|
||||
padding: 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wm__ostrom-intro {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wm__ostrom-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wm__ostrom-item {
|
||||
display: flex;
|
||||
gap: 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wm__ostrom-jalon {
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wm__ostrom-text { color: var(--mood-text-muted); }
|
||||
|
||||
/* Expand transition */
|
||||
.expand-enter-active, .expand-leave-active { transition: all 0.2s ease; overflow: hidden; }
|
||||
.expand-enter-from, .expand-leave-to { max-height: 0; opacity: 0; }
|
||||
.expand-enter-to, .expand-leave-from { max-height: 1000px; opacity: 1; }
|
||||
</style>
|
||||
@@ -9,6 +9,31 @@ const decisions = useDecisionsStore()
|
||||
const protocols = useProtocolsStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
// Toolbox state
|
||||
const showConsentModal = ref(false)
|
||||
const selectedMethod = ref<string | null>(null)
|
||||
|
||||
const consentSteps = [
|
||||
'Présenter la proposition clairement (2 min)',
|
||||
'Tour de clarification — questions de compréhension uniquement',
|
||||
'Tour de réaction — chacun réagit brièvement',
|
||||
'Porteur amende si nécessaire',
|
||||
'Tour d\'objections — silence = consentement',
|
||||
'Lever les objections valides par amendement',
|
||||
'Adopter ou reporter',
|
||||
]
|
||||
|
||||
function handleMethodSelect(method: string) {
|
||||
selectedMethod.value = method
|
||||
if (method.toLowerCase().includes('consentement')) {
|
||||
showConsentModal.value = true
|
||||
}
|
||||
else if (method.toLowerCase().includes('avis')) {
|
||||
// Navigate to advice process guide in mandates toolbox
|
||||
navigateTo('/mandates')
|
||||
}
|
||||
}
|
||||
|
||||
const activeStatus = ref<string | null>(null)
|
||||
const searchQuery = ref('')
|
||||
const sortBy = ref<'date' | 'title' | 'status'>('date')
|
||||
@@ -212,29 +237,78 @@ function formatDate(dateStr: string): string {
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<!-- Context mapper -->
|
||||
<div class="toolbox-block">
|
||||
<div class="toolbox-block__head">
|
||||
<UIcon name="i-lucide-compass" />
|
||||
<span>Quelle méthode ?</span>
|
||||
</div>
|
||||
<ContextMapper @use="handleMethodSelect" />
|
||||
</div>
|
||||
|
||||
<!-- Vote inertiel WoT -->
|
||||
<ToolboxVignette
|
||||
title="Vote majoritaire WoT"
|
||||
:bullets="['Seuil adaptatif à la participation', 'Formule g1vote inertielle']"
|
||||
title="Vote inertiel WoT"
|
||||
:bullets="[
|
||||
'Seuil adaptatif à la participation',
|
||||
'Faible participation → quasi-unanimité',
|
||||
'Formule g1vote — tracé on-chain',
|
||||
]"
|
||||
:actions="[
|
||||
{ label: 'Simuler', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
|
||||
{ label: 'Protocoles', icon: 'i-lucide-settings', to: '/protocols' },
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Consentement sociocratique -->
|
||||
<ToolboxVignette
|
||||
title="Vote nuancé"
|
||||
:bullets="['6 niveaux de préférence', 'Seuil de satisfaction 80%']"
|
||||
title="Consentement sociocratique"
|
||||
:bullets="[
|
||||
'Aucune objection grave = adopté',
|
||||
'Rapide pour petits groupes',
|
||||
'Distingue préférence et objection',
|
||||
]"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-bar-chart-3', emit: 'nuance' },
|
||||
{ label: 'Guide', icon: 'i-lucide-book-open', emit: 'consent', primary: true },
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Advice process -->
|
||||
<ToolboxVignette
|
||||
title="Mandature"
|
||||
:bullets="['Élection en binôme', 'Transparence et révocation']"
|
||||
title="Processus d'avis (Laloux)"
|
||||
:bullets="[
|
||||
'Décisions urgentes : < 2h',
|
||||
'Consultant experts + impactés',
|
||||
'Responsabilise le porteur',
|
||||
]"
|
||||
:actions="[
|
||||
{ label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', primary: true },
|
||||
{ label: 'Guide', icon: 'i-lucide-message-circle', emit: 'advice', primary: true },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</SectionLayout>
|
||||
|
||||
<!-- Modal consent guide -->
|
||||
<UModal v-model:open="showConsentModal">
|
||||
<template #content>
|
||||
<div class="decision-modal">
|
||||
<h3 class="decision-modal__title">Consentement sociocratique</h3>
|
||||
<p class="decision-modal__text">
|
||||
Une décision est adoptée par consentement quand aucun membre ne soulève d'objection grave.
|
||||
Une objection grave est une raison pour laquelle la proposition nuit à la mission commune —
|
||||
pas une simple préférence.
|
||||
</p>
|
||||
<div class="decision-modal__steps">
|
||||
<div v-for="(step, i) in consentSteps" :key="i" class="decision-modal__step">
|
||||
<div class="decision-modal__step-num">{{ i + 1 }}</div>
|
||||
<div class="decision-modal__step-text">{{ step }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="decision-modal__ref">Référence : "La Sociocracie" — Gerard Endenburg, Brian Robertson (Holacracy)</p>
|
||||
<button class="decision-modal__close" @click="showConsentModal = false">Fermer</button>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -458,17 +532,105 @@ function formatDate(dateStr: string): string {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toolbox-section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.25rem;
|
||||
.toolbox-block {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 14px;
|
||||
padding: 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toolbox-empty-text {
|
||||
.toolbox-block__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
font-weight: 800;
|
||||
color: var(--mood-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Decision modal */
|
||||
.decision-modal {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.decision-modal { padding: 2rem; gap: 1.25rem; }
|
||||
}
|
||||
|
||||
.decision-modal__title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.decision-modal__text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.decision-modal__steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.decision-modal__step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.decision-modal__step-num {
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.decision-modal__step-text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--mood-text);
|
||||
padding-top: 0.125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.decision-modal__ref {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.decision-modal__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent-text);
|
||||
background: var(--mood-accent);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
align-self: flex-end;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
.decision-modal__close:hover { transform: translateY(-1px); }
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,41 @@ const documents = useDocumentsStore()
|
||||
const protocols = useProtocolsStore()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const inertiaLevels = [
|
||||
{
|
||||
id: 'light',
|
||||
name: 'Léger',
|
||||
color: 'teal',
|
||||
params: 'B=0.05, G=0.1',
|
||||
desc: 'Modification facile. Majorité simple suffit avec bonne participation.',
|
||||
example: 'Clarifications rédactionnelles, notes de bas de page.',
|
||||
},
|
||||
{
|
||||
id: 'standard',
|
||||
name: 'Standard',
|
||||
color: 'accent',
|
||||
params: 'B=0.1, G=0.2',
|
||||
desc: 'Seuil adaptatif standard. La formule g1vote dans son paramétrage habituel.',
|
||||
example: 'Articles de fond, engagements opérationnels.',
|
||||
},
|
||||
{
|
||||
id: 'strong',
|
||||
name: 'Fort',
|
||||
color: 'secondary',
|
||||
params: 'B=0.15, G=0.3',
|
||||
desc: 'Forte résistance. Faible participation → quasi-unanimité requise.',
|
||||
example: 'Principes fondateurs, formules de vote, critères WoT.',
|
||||
},
|
||||
{
|
||||
id: 'very-strong',
|
||||
name: 'Très fort',
|
||||
color: 'error',
|
||||
params: 'B=0.2, G=0.4',
|
||||
desc: 'Protection maximale. Seule une forte mobilisation peut modifier.',
|
||||
example: 'Clause de licence, identité du projet, droits des membres.',
|
||||
},
|
||||
]
|
||||
|
||||
const activeStatus = ref<string | null>(null)
|
||||
const searchQuery = ref('')
|
||||
const sortBy = ref<'date' | 'title' | 'status'>('date')
|
||||
@@ -251,25 +286,55 @@ async function createDocument() {
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<!-- Inertia guide -->
|
||||
<div class="toolbox-block">
|
||||
<div class="toolbox-block__head">
|
||||
<UIcon name="i-lucide-sliders-horizontal" />
|
||||
<span>Niveaux d'inertie</span>
|
||||
</div>
|
||||
<div class="inertia-guide">
|
||||
<div v-for="level in inertiaLevels" :key="level.id" class="inertia-level">
|
||||
<div class="inertia-level__header">
|
||||
<span class="inertia-level__name" :class="`inertia-level__name--${level.color}`">
|
||||
{{ level.name }}
|
||||
</span>
|
||||
<span class="inertia-level__params">{{ level.params }}</span>
|
||||
</div>
|
||||
<p class="inertia-level__desc">{{ level.desc }}</p>
|
||||
<p class="inertia-level__example">{{ level.example }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink to="/protocols/formulas" class="toolbox-link-btn">
|
||||
<UIcon name="i-lucide-calculator" />
|
||||
Simuler les formules
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Structure document -->
|
||||
<ToolboxVignette
|
||||
title="Modules"
|
||||
:bullets="['Structurer en sections et clauses', 'Vote indépendant par clause']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-puzzle', emit: 'modules' },
|
||||
title="Structure d'un document"
|
||||
:bullets="[
|
||||
'Items = clauses individuelles',
|
||||
'Sections = groupes thématiques',
|
||||
'Chaque clause : vote indépendant',
|
||||
'Genesis block : traçabilité d\'origine',
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Votes permanents"
|
||||
:bullets="['Chaque clause est modifiable', 'Seuil adaptatif WoT']"
|
||||
:actions="[
|
||||
{ label: 'Formules', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
|
||||
{ label: 'Nouveau doc', icon: 'i-lucide-file-plus', emit: 'new', primary: true },
|
||||
]"
|
||||
@action="e => e === 'new' && openNewDocModal()"
|
||||
/>
|
||||
|
||||
<!-- Sanctuaire -->
|
||||
<ToolboxVignette
|
||||
title="Inertie de remplacement"
|
||||
:bullets="['4 niveaux de difficulté', 'Protège les textes fondamentaux']"
|
||||
title="Sanctuaire IPFS"
|
||||
:bullets="[
|
||||
'Document adopté → archivé on-chain',
|
||||
'Hash IPFS + system.remark Duniter',
|
||||
'Immuable, vérifiable, décentralisé',
|
||||
]"
|
||||
:actions="[
|
||||
{ label: 'Simuler', icon: 'i-lucide-sliders-horizontal', to: '/protocols/formulas', primary: true },
|
||||
{ label: 'Sanctuaire', icon: 'i-lucide-archive', to: '/sanctuary', primary: true },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
@@ -469,18 +534,104 @@ async function createDocument() {
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.25rem;
|
||||
/* Toolbox blocks */
|
||||
.toolbox-block {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 14px;
|
||||
padding: 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toolbox-empty-text {
|
||||
.toolbox-block__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Inertia guide */
|
||||
.inertia-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.inertia-level {
|
||||
background: var(--mood-surface);
|
||||
border-radius: 10px;
|
||||
padding: 0.625rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.inertia-level__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.inertia-level__name {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.inertia-level__name--teal { color: var(--mood-success); }
|
||||
.inertia-level__name--accent { color: var(--mood-accent); }
|
||||
.inertia-level__name--secondary { color: var(--mood-secondary, var(--mood-accent)); }
|
||||
.inertia-level__name--error { color: var(--mood-error); }
|
||||
|
||||
.inertia-level__params {
|
||||
font-size: 0.6875rem;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
color: var(--mood-text-muted);
|
||||
background: var(--mood-accent-soft);
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inertia-level__desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.inertia-level__example {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.toolbox-link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent-text);
|
||||
background: var(--mood-accent);
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
align-self: flex-start;
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
}
|
||||
.toolbox-link-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 3px 10px var(--mood-shadow);
|
||||
}
|
||||
|
||||
/* --- Modern search / sort / action --- */
|
||||
|
||||
@@ -272,32 +272,41 @@ async function handleCreate() {
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<!-- Sociocratic election guide -->
|
||||
<div class="toolbox-block">
|
||||
<div class="toolbox-block__head">
|
||||
<UIcon name="i-lucide-users" />
|
||||
<span>Nomination & Élection</span>
|
||||
</div>
|
||||
<SocioElection />
|
||||
</div>
|
||||
|
||||
<!-- Mandat cycle -->
|
||||
<ToolboxVignette
|
||||
title="Ouverture"
|
||||
:bullets="['Définir mission et périmètre', 'Durée et objectifs clairs']"
|
||||
:actions="[
|
||||
{ label: 'Créer', icon: 'i-lucide-door-open', emit: 'create', primary: true },
|
||||
title="Cycle de mandat"
|
||||
:bullets="[
|
||||
'1. Ouverture + définition du rôle',
|
||||
'2. Candidatures (auto ou par pairs)',
|
||||
'3. Élection sociocratique',
|
||||
'4. Période active + rapports',
|
||||
'5. Renouvellement ou clôture',
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Nomination"
|
||||
:bullets="['Élection en binôme', 'Titulaire + suppléant']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-users', emit: 'nomination' },
|
||||
{ label: 'Nouveau mandat', icon: 'i-lucide-plus', emit: 'create', primary: true },
|
||||
]"
|
||||
@action="e => e === 'create' && (showCreateModal = true)"
|
||||
/>
|
||||
|
||||
<!-- Révocation -->
|
||||
<ToolboxVignette
|
||||
title="Transparence"
|
||||
:bullets="['Rapports d\'activité', 'Soumis au vote communautaire']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-eye', emit: 'transparence' },
|
||||
title="Révocation"
|
||||
:bullets="[
|
||||
'Initiée par 3 membres ou plus',
|
||||
'Vote communautaire ordinaire',
|
||||
'Bilan de clôture obligatoire',
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Cloture"
|
||||
:bullets="['Fin de mandat ou révocation', 'Bilan et transmission']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-lock', emit: 'cloture' },
|
||||
{ label: 'Voir', icon: 'i-lucide-shield-off', emit: 'revoke' },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
@@ -549,18 +558,24 @@ async function handleCreate() {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.toolbox-section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.25rem;
|
||||
.toolbox-block {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 14px;
|
||||
padding: 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toolbox-empty-text {
|
||||
.toolbox-block__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
font-weight: 800;
|
||||
color: var(--mood-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.mandate-card__type-badge {
|
||||
|
||||
@@ -136,6 +136,24 @@ interface OperationalProtocol {
|
||||
}
|
||||
|
||||
const operationalProtocols: OperationalProtocol[] = [
|
||||
{
|
||||
slug: 'election-sociocratique',
|
||||
name: 'Élection sociocratique',
|
||||
description: 'Processus d\'élection d\'un rôle par consentement : clarification du rôle, nominations silencieuses, argumentaire, levée d\'objections. Garantit légitimité et clarté.',
|
||||
category: 'gouvernance',
|
||||
icon: 'i-lucide-users',
|
||||
instancesLabel: 'Tout renouvellement de rôle',
|
||||
linkedRefs: [
|
||||
{ label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', kind: 'decision' },
|
||||
],
|
||||
steps: [
|
||||
{ label: 'Clarifier le rôle', actor: 'Cercle', icon: 'i-lucide-clipboard-list', type: 'checklist' },
|
||||
{ label: 'Nominations silencieuses', actor: 'Tous les membres', icon: 'i-lucide-pencil', type: 'checklist' },
|
||||
{ label: 'Recueil & argumentaire', actor: 'Facilitateur', icon: 'i-lucide-list-checks', type: 'checklist' },
|
||||
{ label: 'Objections & consentement', actor: 'Cercle', icon: 'i-lucide-shield-check', type: 'certification' },
|
||||
{ label: 'Proclamation', actor: 'Facilitateur', icon: 'i-lucide-star', type: 'on_chain' },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'embarquement-forgeron',
|
||||
name: 'Embarquement Forgeron',
|
||||
@@ -391,12 +409,20 @@ const n8nWorkflows = [
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<!-- Workflow milestones -->
|
||||
<div class="toolbox-block">
|
||||
<div class="toolbox-block__head">
|
||||
<UIcon name="i-lucide-git-branch" />
|
||||
<span>Jalons de protocole</span>
|
||||
</div>
|
||||
<WorkflowMilestones />
|
||||
</div>
|
||||
|
||||
<!-- Simulateur -->
|
||||
<ToolboxVignette
|
||||
title="Simulateur de formules"
|
||||
:bullets="['Testez WoT, Smith, TechComm', 'Ajustez les paramètres en temps réel', 'Visualisez les seuils']"
|
||||
:bullets="['WoT, Smith, TechComm', 'Paramètres en temps réel', 'Visualise les seuils']"
|
||||
:actions="[
|
||||
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
|
||||
{ label: 'Ouvrir', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
|
||||
]"
|
||||
/>
|
||||
@@ -405,12 +431,8 @@ const n8nWorkflows = [
|
||||
<div class="n8n-section">
|
||||
<div class="n8n-section__head">
|
||||
<UIcon name="i-lucide-workflow" class="text-xs" />
|
||||
<span>Workflows n8n</span>
|
||||
<span>Automatisations</span>
|
||||
</div>
|
||||
<p class="n8n-section__desc">
|
||||
Automatisations reliées via MCP
|
||||
</p>
|
||||
|
||||
<div class="n8n-workflows">
|
||||
<div
|
||||
v-for="wf in n8nWorkflows"
|
||||
@@ -439,10 +461,8 @@ const n8nWorkflows = [
|
||||
<!-- Meta-gouvernance -->
|
||||
<ToolboxVignette
|
||||
title="Méta-gouvernance"
|
||||
:bullets="['Les formules sont soumises au vote', 'Modifier les seuils collectivement', 'Transparence totale']"
|
||||
:bullets="['Les formules sont soumises au vote', 'Seuils modifiables collectivement', 'Transparence totale']"
|
||||
:actions="[
|
||||
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
|
||||
{ label: 'Formules', icon: 'i-lucide-calculator', emit: 'formules' },
|
||||
{ label: 'Démarrer', icon: 'i-lucide-play', emit: 'meta', primary: true },
|
||||
]"
|
||||
/>
|
||||
@@ -830,6 +850,27 @@ const n8nWorkflows = [
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
/* Toolbox blocks */
|
||||
.toolbox-block {
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 14px;
|
||||
padding: 0.875rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toolbox-block__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* --- n8n Section --- */
|
||||
.n8n-section {
|
||||
background: var(--mood-accent-soft);
|
||||
|
||||
Reference in New Issue
Block a user