Composants engagement: GenesisBlock, InertiaSlider, MiniVoteBoard, EngagementCard, DocumentTuto
Backend: genesis_json sur Document, section_tag/inertia_preset/is_permanent_vote sur DocumentItem Frontend: 5 nouveaux composants pour vue detail document enrichie - GenesisBlock: sources, outils, synthese forum, contributeurs (depliable) - InertiaSlider: visualisation inertie 4 niveaux avec params formule G/M - MiniVoteBoard: tableau vote compact (barre seuil, pour/contre, participation) - EngagementCard: carte item enrichie integrant vote + inertie + actions - DocumentTuto: modal pedagogique vote permanent/inertie/seuils Seed et page [slug] enrichis pour exploiter les nouveaux champs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Uuid, func
|
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, Uuid, Boolean, func
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from app.database import Base
|
from app.database import Base
|
||||||
@@ -19,10 +19,11 @@ class Document(Base):
|
|||||||
description: Mapped[str | None] = mapped_column(Text)
|
description: Mapped[str | None] = mapped_column(Text)
|
||||||
ipfs_cid: Mapped[str | None] = mapped_column(String(128))
|
ipfs_cid: Mapped[str | None] = mapped_column(String(128))
|
||||||
chain_anchor: Mapped[str | None] = mapped_column(String(128))
|
chain_anchor: Mapped[str | None] = mapped_column(String(128))
|
||||||
|
genesis_json: Mapped[str | None] = mapped_column(Text) # JSON: source files, repos, forum URLs, formula trigger
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
items: Mapped[list["DocumentItem"]] = relationship(back_populates="document", cascade="all, delete-orphan", order_by="DocumentItem.position")
|
items: Mapped[list["DocumentItem"]] = relationship(back_populates="document", cascade="all, delete-orphan", order_by="DocumentItem.sort_order")
|
||||||
|
|
||||||
|
|
||||||
class DocumentItem(Base):
|
class DocumentItem(Base):
|
||||||
@@ -31,11 +32,14 @@ class DocumentItem(Base):
|
|||||||
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
|
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
|
||||||
document_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("documents.id"), nullable=False)
|
document_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("documents.id"), nullable=False)
|
||||||
position: Mapped[str] = mapped_column(String(16), nullable=False) # "1", "1.1", "3.2"
|
position: Mapped[str] = mapped_column(String(16), nullable=False) # "1", "1.1", "3.2"
|
||||||
item_type: Mapped[str] = mapped_column(String(32), default="clause") # clause, rule, verification, preamble, section
|
item_type: Mapped[str] = mapped_column(String(32), default="clause") # clause, rule, verification, preamble, section, genesis
|
||||||
title: Mapped[str | None] = mapped_column(String(256))
|
title: Mapped[str | None] = mapped_column(String(256))
|
||||||
current_text: Mapped[str] = mapped_column(Text, nullable=False)
|
current_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
voting_protocol_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("voting_protocols.id"))
|
voting_protocol_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("voting_protocols.id"))
|
||||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
section_tag: Mapped[str | None] = mapped_column(String(64)) # genesis, fondamental, technique, annexe, formule, inertie, ordonnancement
|
||||||
|
inertia_preset: Mapped[str] = mapped_column(String(16), default="standard") # low, standard, high, very_high
|
||||||
|
is_permanent_vote: Mapped[bool] = mapped_column(default=True) # permanent vote vs time-bounded
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class DocumentOut(BaseModel):
|
|||||||
description: str | None = None
|
description: str | None = None
|
||||||
ipfs_cid: str | None = None
|
ipfs_cid: str | None = None
|
||||||
chain_anchor: str | None = None
|
chain_anchor: str | None = None
|
||||||
|
genesis_json: str | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
items_count: int = Field(default=0, description="Number of items in this document")
|
items_count: int = Field(default=0, description="Number of items in this document")
|
||||||
@@ -54,10 +55,13 @@ class DocumentItemCreate(BaseModel):
|
|||||||
"""Payload for creating a document item (clause, rule, etc.)."""
|
"""Payload for creating a document item (clause, rule, etc.)."""
|
||||||
|
|
||||||
position: str = Field(..., max_length=16, description='Hierarchical position e.g. "1", "1.1", "3.2"')
|
position: str = Field(..., max_length=16, description='Hierarchical position e.g. "1", "1.1", "3.2"')
|
||||||
item_type: str = Field(default="clause", max_length=32, description="clause, rule, verification, preamble, section")
|
item_type: str = Field(default="clause", max_length=32, description="clause, rule, verification, preamble, section, genesis")
|
||||||
title: str | None = Field(default=None, max_length=256)
|
title: str | None = Field(default=None, max_length=256)
|
||||||
current_text: str = Field(..., min_length=1)
|
current_text: str = Field(..., min_length=1)
|
||||||
voting_protocol_id: UUID | None = None
|
voting_protocol_id: UUID | None = None
|
||||||
|
section_tag: str | None = Field(default=None, max_length=64)
|
||||||
|
inertia_preset: str = Field(default="standard", max_length=16)
|
||||||
|
is_permanent_vote: bool = True
|
||||||
|
|
||||||
|
|
||||||
class DocumentItemUpdate(BaseModel):
|
class DocumentItemUpdate(BaseModel):
|
||||||
@@ -82,6 +86,9 @@ class DocumentItemOut(BaseModel):
|
|||||||
current_text: str
|
current_text: str
|
||||||
voting_protocol_id: UUID | None = None
|
voting_protocol_id: UUID | None = None
|
||||||
sort_order: int
|
sort_order: int
|
||||||
|
section_tag: str | None = None
|
||||||
|
inertia_preset: str = "standard"
|
||||||
|
is_permanent_vote: bool = True
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
@@ -99,6 +106,9 @@ class DocumentItemFullOut(BaseModel):
|
|||||||
current_text: str
|
current_text: str
|
||||||
voting_protocol_id: UUID | None = None
|
voting_protocol_id: UUID | None = None
|
||||||
sort_order: int
|
sort_order: int
|
||||||
|
section_tag: str | None = None
|
||||||
|
inertia_preset: str = "standard"
|
||||||
|
is_permanent_vote: bool = True
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
versions: list[ItemVersionOut] = Field(default_factory=list)
|
versions: list[ItemVersionOut] = Field(default_factory=list)
|
||||||
@@ -118,6 +128,7 @@ class DocumentFullOut(BaseModel):
|
|||||||
description: str | None = None
|
description: str | None = None
|
||||||
ipfs_cid: str | None = None
|
ipfs_cid: str | None = None
|
||||||
chain_anchor: str | None = None
|
chain_anchor: str | None = None
|
||||||
|
genesis_json: str | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
items: list[DocumentItemOut] = Field(default_factory=list)
|
items: list[DocumentItemOut] = Field(default_factory=list)
|
||||||
|
|||||||
666
backend/seed.py
666
backend/seed.py
@@ -2,9 +2,17 @@
|
|||||||
|
|
||||||
Sources:
|
Sources:
|
||||||
- Engagement Forgeron v2.0.0: https://forum.monnaie-libre.fr/t/33165
|
- Engagement Forgeron v2.0.0: https://forum.monnaie-libre.fr/t/33165
|
||||||
- Engagement Certification (Licence G1): monnaie-libre.fr/licence-g1/
|
- Engagement Certification (Licence G1 v0.3.0):
|
||||||
|
https://git.duniter.org/documents/g1_monetary_license/-/raw/master/g1_monetary_license_fr.rst
|
||||||
|
- Charte 1.0 (rejected): https://forum.monnaie-libre.fr/t/proposition-charte-1-0/31066
|
||||||
|
- Licence v0.4.0 (in progress): https://forum.monnaie-libre.fr/t/32375
|
||||||
- Runtime Upgrade process template
|
- Runtime Upgrade process template
|
||||||
|
|
||||||
|
Genesis references:
|
||||||
|
- Licence repo: https://git.duniter.org/documents/g1_monetary_license
|
||||||
|
- g1vote: https://git.duniter.org/tools/g1vote-view
|
||||||
|
- g1vote live: https://g1vote-view-237903.pages.duniter.org/
|
||||||
|
|
||||||
Idempotent: checks if data already exists before inserting.
|
Idempotent: checks if data already exists before inserting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -12,6 +20,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
@@ -61,13 +70,47 @@ def fake_signature(payload: str) -> str:
|
|||||||
async def seed_formula_configs(session: AsyncSession) -> dict[str, FormulaConfig]:
|
async def seed_formula_configs(session: AsyncSession) -> dict[str, FormulaConfig]:
|
||||||
configs: dict[str, dict] = {
|
configs: dict[str, dict] = {
|
||||||
"Standard Licence G1": {
|
"Standard Licence G1": {
|
||||||
"description": "Formule standard pour la Licence G1 : vote binaire WoT.",
|
"description": "Formule standard pour la Licence G1 : vote binaire WoT. Inertie standard.",
|
||||||
"duration_days": 30,
|
"duration_days": 30,
|
||||||
"majority_pct": 50,
|
"majority_pct": 50,
|
||||||
"base_exponent": 0.1,
|
"base_exponent": 0.1,
|
||||||
"gradient_exponent": 0.2,
|
"gradient_exponent": 0.2,
|
||||||
"constant_base": 0.0,
|
"constant_base": 0.0,
|
||||||
},
|
},
|
||||||
|
"Inertie basse (Annexes)": {
|
||||||
|
"description": (
|
||||||
|
"Formule a inertie basse pour les annexes et recommandations. "
|
||||||
|
"Gradient faible = plus facile a remplacer meme a faible participation."
|
||||||
|
),
|
||||||
|
"duration_days": 30,
|
||||||
|
"majority_pct": 50,
|
||||||
|
"base_exponent": 0.1,
|
||||||
|
"gradient_exponent": 0.1,
|
||||||
|
"constant_base": 0.0,
|
||||||
|
},
|
||||||
|
"Inertie haute (Formule)": {
|
||||||
|
"description": (
|
||||||
|
"Formule a inertie haute pour la section formule elle-meme. "
|
||||||
|
"Gradient eleve = necessite une forte mobilisation pour changer."
|
||||||
|
),
|
||||||
|
"duration_days": 30,
|
||||||
|
"majority_pct": 60,
|
||||||
|
"base_exponent": 0.1,
|
||||||
|
"gradient_exponent": 0.4,
|
||||||
|
"constant_base": 0.0,
|
||||||
|
},
|
||||||
|
"Inertie tres haute (Meta-reglage)": {
|
||||||
|
"description": (
|
||||||
|
"Formule a inertie maximale pour le reglage de l'inertie elle-meme. "
|
||||||
|
"Quasi-unanimite requise a toute participation. Protection contre "
|
||||||
|
"la modification des regles de modification."
|
||||||
|
),
|
||||||
|
"duration_days": 30,
|
||||||
|
"majority_pct": 66,
|
||||||
|
"base_exponent": 0.1,
|
||||||
|
"gradient_exponent": 0.6,
|
||||||
|
"constant_base": 0.0,
|
||||||
|
},
|
||||||
"Forgeron avec Smith": {
|
"Forgeron avec Smith": {
|
||||||
"description": "Vote forgeron avec critere Smith sub-WoT.",
|
"description": "Vote forgeron avec critere Smith sub-WoT.",
|
||||||
"duration_days": 30,
|
"duration_days": 30,
|
||||||
@@ -170,112 +213,599 @@ async def seed_voting_protocols(
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Seed: Engagement Certification (Licence G1 - obligations certificateur)
|
# Seed: Engagement Certification (Acte d'engagement certification)
|
||||||
|
#
|
||||||
|
# Full structured document built from:
|
||||||
|
# - Licence G1 v0.3.0 (in force)
|
||||||
|
# - Charte 1.0 (rejected proposal, topic 31066)
|
||||||
|
# - Licence v0.4.0 (in progress, topic 32375)
|
||||||
|
# - Yvv's "Acte d'engagement" position
|
||||||
|
# - Checklist certification (topic 32412)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
GENESIS_CERTIFICATION = {
|
||||||
|
"source_document": {
|
||||||
|
"title": "Licence de la monnaie libre G1 v0.3.0",
|
||||||
|
"url": "https://git.duniter.org/documents/g1_monetary_license/-/raw/master/g1_monetary_license_fr.rst",
|
||||||
|
"repo": "https://git.duniter.org/documents/g1_monetary_license",
|
||||||
|
},
|
||||||
|
"reference_tools": {
|
||||||
|
"g1vote_repo": "https://git.duniter.org/tools/g1vote-view",
|
||||||
|
"g1vote_live": "https://g1vote-view-237903.pages.duniter.org/",
|
||||||
|
"cesium": "https://g1.duniter.org",
|
||||||
|
"gecko": "https://gecko.music-all.org",
|
||||||
|
},
|
||||||
|
"forum_synthesis": [
|
||||||
|
{
|
||||||
|
"title": "Proposition Charte 1.0 (rejetee)",
|
||||||
|
"url": "https://forum.monnaie-libre.fr/t/proposition-charte-1-0/31066",
|
||||||
|
"status": "rejected",
|
||||||
|
"posts": 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Preparation licence v0.4.0",
|
||||||
|
"url": "https://forum.monnaie-libre.fr/t/preparation-dune-proposition-devolution-de-la-licence-1/32375",
|
||||||
|
"status": "in_progress",
|
||||||
|
"posts": 38,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Checklist de certification (annexe licence)",
|
||||||
|
"url": "https://forum.monnaie-libre.fr/t/prepa-checklist-de-certification-annexe-licence-1/32412",
|
||||||
|
"status": "in_progress",
|
||||||
|
"posts": 16,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Regles de modifications (annexe licence)",
|
||||||
|
"url": "https://forum.monnaie-libre.fr/t/prepa-regles-de-modifications-annexe-licence-1/32409",
|
||||||
|
"status": "in_progress",
|
||||||
|
"posts": 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Vote nuance licence",
|
||||||
|
"url": "https://forum.monnaie-libre.fr/t/processus-de-validation-licence-par-vote-nuance/31729",
|
||||||
|
"status": "reference",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"formula_trigger": (
|
||||||
|
"Quand un item atteint le seuil d'adoption (formule WoT), "
|
||||||
|
"le texte de remplacement est integre au document officiel. "
|
||||||
|
"Le hash IPFS du document mis a jour est ancre on-chain via system.remark. "
|
||||||
|
"Les applications (Cesium, Gecko) pointent vers le depot git officiel "
|
||||||
|
"qui est synchronise avec l'etat valide par les votes."
|
||||||
|
),
|
||||||
|
"contributors": [
|
||||||
|
{"name": "1000i100", "role": "Pilote principal, redacteur"},
|
||||||
|
{"name": "Natha", "role": "Co-redactrice"},
|
||||||
|
{"name": "Pini", "role": "Co-initiateur"},
|
||||||
|
{"name": "Yvv", "role": "Contributions structurelles, concepteur 'Acte d'engagement'"},
|
||||||
|
{"name": "elois", "role": "Dev g1vote, contributions techniques"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
|
ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
|
||||||
|
# ===================================================================
|
||||||
|
# INTRODUCTION
|
||||||
|
# ===================================================================
|
||||||
{
|
{
|
||||||
"position": "C1",
|
"position": "I1",
|
||||||
"item_type": "clause",
|
"item_type": "preamble",
|
||||||
"title": "Transmission de la licence",
|
"title": "Preambule",
|
||||||
"sort_order": 1,
|
"sort_order": 1,
|
||||||
|
"section_tag": "introduction",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Toute operation de certification d'un nouveau membre de la monnaie libre G1 "
|
"Le present acte d'engagement definit les obligations reciproques "
|
||||||
"doit prealablement s'accompagner de la transmission de cette licence "
|
"des membres de la toile de confiance de la monnaie libre G1. "
|
||||||
"de la monnaie libre G1 au certifie."
|
"Cet acte est de fait l'unique relation contractuelle de notre "
|
||||||
|
"toile fiduciaire. Toute certification doit s'accompagner de la "
|
||||||
|
"transmission de ce document, dont le certificateur doit s'assurer "
|
||||||
|
"qu'il a ete etudie, compris et accepte par le certifie."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": "C2",
|
"position": "I2",
|
||||||
"item_type": "clause",
|
"item_type": "preamble",
|
||||||
"title": "Connaissance suffisante du certifie",
|
"title": "Les deux garanties reciproques",
|
||||||
"sort_order": 2,
|
"sort_order": 2,
|
||||||
|
"section_tag": "introduction",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Certifier n'est pas uniquement s'assurer que vous avez rencontre la personne, "
|
"La certification repose sur deux garanties reciproques :\n\n"
|
||||||
"c'est assurer a la communaute G1 que vous connaissez suffisamment bien la "
|
"**1.** Derriere une cle publique creatrice de monnaie "
|
||||||
"personne que vous vous appretez a certifier."
|
"se trouve un **etre humain vivant**.\n\n"
|
||||||
|
"**2.** Derriere cet etre humain se trouve **une seule et unique** "
|
||||||
|
"cle publique creatrice de monnaie.\n\n"
|
||||||
|
"La certification est un acte technique et fiduciaire, "
|
||||||
|
"pas un acte d'adhesion morale ou de sympathie."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# TITRE 1 : ENGAGEMENTS FONDAMENTAUX (sur l'honneur)
|
||||||
|
# ===================================================================
|
||||||
{
|
{
|
||||||
"position": "C3",
|
"position": "T1",
|
||||||
"item_type": "clause",
|
"item_type": "section",
|
||||||
"title": "Contact par plusieurs moyens",
|
"title": "Engagements fondamentaux",
|
||||||
"sort_order": 3,
|
"sort_order": 3,
|
||||||
|
"section_tag": "fondamental",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Connaitre la personne par plusieurs moyens de contact differents "
|
"Les engagements fondamentaux ci-dessous constituent le socle "
|
||||||
"(physique, electronique, etc.) permettant de verifier son identite "
|
"irreductible de l'acte d'engagement. Ils sont pris sur l'honneur "
|
||||||
"et de maintenir un lien de confiance dans la duree."
|
"par tout membre de la toile de confiance."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": "C4",
|
"position": "E1",
|
||||||
"item_type": "clause",
|
"item_type": "clause",
|
||||||
"title": "Ne jamais certifier seul",
|
"title": "Unicite du compte",
|
||||||
"sort_order": 4,
|
"sort_order": 4,
|
||||||
|
"section_tag": "fondamental",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Ne certifiez jamais seul, mais accompagne d'au moins un autre membre "
|
"Je m'engage sur l'honneur a n'avoir et n'avoir jamais "
|
||||||
"de la TdC G1, pour garantir un double controle et eviter les "
|
"qu'un seul et unique compte cocreateur de monnaie G1."
|
||||||
"certifications abusives."
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": "C5",
|
"position": "E2",
|
||||||
"item_type": "verification",
|
"item_type": "clause",
|
||||||
"title": "Verification des certifications existantes",
|
"title": "Certification responsable",
|
||||||
"sort_order": 5,
|
"sort_order": 5,
|
||||||
|
"section_tag": "fondamental",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Avant toute certification, assurez-vous de verifier si le compte du "
|
"Je m'engage sur l'honneur a ne certifier que des personnes "
|
||||||
"certifie a deja recu une ou plusieurs certifications, et de qui "
|
"physiques qui respectent scrupuleusement ces deux presents "
|
||||||
"elles proviennent."
|
"engagements fondamentaux."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# TITRE 2 : ENGAGEMENTS TECHNIQUES
|
||||||
|
# ===================================================================
|
||||||
{
|
{
|
||||||
"position": "C6",
|
"position": "T2",
|
||||||
"item_type": "verification",
|
"item_type": "section",
|
||||||
"title": "Verification de maitrise du compte",
|
"title": "Engagements techniques",
|
||||||
"sort_order": 6,
|
"sort_order": 6,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Verifier la maitrise du compte par un transfert test : envoyer "
|
"Les engagements techniques definissent les obligations "
|
||||||
"quelques G1 et demander un renvoi, afin de s'assurer que la personne "
|
"pratiques et verifiables du certificateur pour garantir "
|
||||||
"controle bien sa cle privee."
|
"la qualite de la toile de confiance."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": "C7",
|
"position": "E3",
|
||||||
"item_type": "verification",
|
"item_type": "clause",
|
||||||
"title": "Verification de la licence",
|
"title": "Connaissance suffisante",
|
||||||
"sort_order": 7,
|
"sort_order": 7,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"Verifiez que vos contacts ont bien etudie et compris la licence G1 "
|
"Je me suis assure de connaitre suffisamment la personne "
|
||||||
"a jour avant de proceder a la certification."
|
"qui gere cette cle publique. Connaitre suffisamment ne signifie "
|
||||||
|
"pas « avoir vu » ; c'est assurer a la communaute G1 que je "
|
||||||
|
"pourrai la contacter facilement et etre en mesure de reperer "
|
||||||
|
"un double-compte ou tout autre probleme."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": "C8",
|
"position": "E4",
|
||||||
"item_type": "verification",
|
"item_type": "clause",
|
||||||
"title": "Document de revocation",
|
"title": "Verification personnelle de la cle",
|
||||||
"sort_order": 8,
|
"sort_order": 8,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"D'avoir bien verifie avec la personne concernee qu'elle a bien genere "
|
"J'ai personnellement verifie que c'est bien cette cle publique "
|
||||||
"son document Duniter de revocation de compte, et qu'elle le conserve "
|
"que je m'apprete a certifier, en la comparant avec la personne "
|
||||||
|
"concernee et non par un intermediaire."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "E5",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Joignabilite reciproque",
|
||||||
|
"sort_order": 9,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"Je suis joignable rapidement et facilement par mes certifieurs, "
|
||||||
|
"et je peux joindre rapidement et facilement les personnes que "
|
||||||
|
"je certifie, par plusieurs moyens de communication differents "
|
||||||
|
"et independants (physique, telephone, email, messagerie, etc.)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "E6",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Document de revocation",
|
||||||
|
"sort_order": 10,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"J'ai verifie avec la personne certifiee qu'elle a bien genere "
|
||||||
|
"son document de revocation de compte et qu'elle le conserve "
|
||||||
"en lieu sur."
|
"en lieu sur."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"position": "C9",
|
"position": "E7",
|
||||||
"item_type": "clause",
|
"item_type": "clause",
|
||||||
"title": "Rencontre physique ou multi-canal",
|
"title": "Rencontre physique ou verification multi-canaux",
|
||||||
"sort_order": 9,
|
"sort_order": 11,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
"current_text": (
|
"current_text": (
|
||||||
"De rencontrer la personne physiquement, OU de verifier a distance "
|
"J'ai rencontre la personne physiquement (preferable), **OU** "
|
||||||
"le lien personne / cle publique par plusieurs moyens de communication "
|
"j'ai verifie a distance le lien personne / cle publique par "
|
||||||
"differents et independants."
|
"plusieurs moyens de communication differents et independants : "
|
||||||
|
"courrier + reseau social + forum + email + visio + telephone."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# TITRE 3 : CONSEILS ET BONNES PRATIQUES
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "T3",
|
||||||
|
"item_type": "section",
|
||||||
|
"title": "Conseils et bonnes pratiques",
|
||||||
|
"sort_order": 12,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"Les pratiques suivantes sont fortement recommandees pour "
|
||||||
|
"garantir la qualite et la securite de la toile de confiance."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "E8",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Ne jamais certifier seul",
|
||||||
|
"sort_order": 13,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"Ne certifiez jamais seul, mais accompagne d'au moins un autre "
|
||||||
|
"membre de la toile de confiance G1, pour garantir un double "
|
||||||
|
"controle."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "E9",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Verification des certifications existantes",
|
||||||
|
"sort_order": 14,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"Avant toute certification, verifiez si le compte a deja "
|
||||||
|
"recu des certifications et de qui elles proviennent. "
|
||||||
|
"Contactez les certifieurs existants en cas de doute. "
|
||||||
|
"Si un certifieurs existant ne connait pas la personne, "
|
||||||
|
"alertez immediatement les experts de la communaute."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "E10",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Verification de maitrise du compte",
|
||||||
|
"sort_order": 15,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"Verifiez la maitrise du compte par un transfert-test : "
|
||||||
|
"envoyez quelques G1 et demandez le renvoi, afin de vous "
|
||||||
|
"assurer que la personne controle bien sa cle privee."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "E11",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Transmission et comprehension du document",
|
||||||
|
"sort_order": 16,
|
||||||
|
"section_tag": "technique",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"Verifiez que vos contacts ont bien etudie et compris "
|
||||||
|
"le present acte d'engagement dans sa version a jour "
|
||||||
|
"avant de proceder a la certification."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# CONCLUSION
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "K1",
|
||||||
|
"item_type": "preamble",
|
||||||
|
"title": "Regles abregees de la toile de confiance",
|
||||||
|
"sort_order": 17,
|
||||||
|
"section_tag": "conclusion",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"**Parametres protocolaires en vigueur :**\n\n"
|
||||||
|
"- Stock de **100 certifications** possibles\n"
|
||||||
|
"- 1 certification emissible tous les **5 jours**\n"
|
||||||
|
"- Nouveau compte valide si **>= 5 certifications** recues en **2 mois**\n"
|
||||||
|
"- Condition de distance : **<= 5 pas** de **80% des sentinelles**\n"
|
||||||
|
"- Sentinelle : membre ayant recu et emis >= Y[N] certifs "
|
||||||
|
"(Y = ceil(N^(1/5)))\n"
|
||||||
|
"- Certifications actives valables **2 ans**\n"
|
||||||
|
"- Renouvellement de l'accord tous les **12 mois**"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "K2",
|
||||||
|
"item_type": "preamble",
|
||||||
|
"title": "Monnaie G1",
|
||||||
|
"sort_order": 18,
|
||||||
|
"section_tag": "conclusion",
|
||||||
|
"inertia_preset": "standard",
|
||||||
|
"current_text": (
|
||||||
|
"**Parametres monetaires :**\n\n"
|
||||||
|
"- 1 Dividende Universel (DU) par personne par jour\n"
|
||||||
|
"- Reevaluation a chaque equinoxe : "
|
||||||
|
"`DU(n+1) = DU(n) + c² × (M/N) / 182.625` avec c = 4.88%\n"
|
||||||
|
"- DU(0) = 10.00 G1"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# ANNEXE 1 : INTEGRATION LOGICIELLE
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "X1",
|
||||||
|
"item_type": "section",
|
||||||
|
"title": "Annexe 1 : Integration logicielle",
|
||||||
|
"sort_order": 19,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"Les obligations pour les logiciels implementant la certification G1."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "X1.1",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Depot de reference",
|
||||||
|
"sort_order": 20,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"Le texte de reference de l'acte d'engagement certification est "
|
||||||
|
"heberge dans le depot git officiel : "
|
||||||
|
"https://git.duniter.org/documents/g1_monetary_license\n\n"
|
||||||
|
"Les applications Cesium, Gecko et toute application de "
|
||||||
|
"certification doivent pointer vers ce depot pour afficher "
|
||||||
|
"la version en vigueur."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "X1.2",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Obligation de transmission",
|
||||||
|
"sort_order": 21,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"Tout logiciel G1 permettant la certification doit transmettre "
|
||||||
|
"le present acte d'engagement au certifie et en afficher les "
|
||||||
|
"parametres du bloc 0 de la blockchain Duniter."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "X1.3",
|
||||||
|
"item_type": "clause",
|
||||||
|
"title": "Logiciel auditable",
|
||||||
|
"sort_order": 22,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"Tout logiciel utilise pour la certification ou la creation "
|
||||||
|
"monetaire doit etre publie sous licence libre, afin de "
|
||||||
|
"permettre son audit par la communaute."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# ANNEXE 2 : QUESTIONS A LA CERTIFICATION (checklist logicielle)
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "X2",
|
||||||
|
"item_type": "section",
|
||||||
|
"title": "Annexe 2 : Questions a la certification",
|
||||||
|
"sort_order": 23,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"Liste de questions a presenter dans les logiciels de certification. "
|
||||||
|
"Inspiree de la checklist de la Charte 1.0 et des discussions "
|
||||||
|
"forum (topic 32412)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "X2.1",
|
||||||
|
"item_type": "verification",
|
||||||
|
"title": "Questions piege (reponse attendue : NON)",
|
||||||
|
"sort_order": 24,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"**Si OUI a l'une de ces questions, la certification doit etre refusee.**\n\n"
|
||||||
|
"- La personne m'a contacte uniquement pour obtenir ma certification\n"
|
||||||
|
"- Je certifie sous la pression ou pour faire plaisir\n"
|
||||||
|
"- Je n'ai aucun moyen de verifier l'identite de la personne\n"
|
||||||
|
"- La personne refuse de me donner plusieurs moyens de contact\n"
|
||||||
|
"- Je soupçonne que la personne possede deja un compte membre\n"
|
||||||
|
"- Je ne connais aucun de ses autres certifieurs\n"
|
||||||
|
"- La personne ne comprend pas ce qu'est la monnaie libre\n"
|
||||||
|
"- La personne n'a pas lu le present acte d'engagement"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "X2.2",
|
||||||
|
"item_type": "verification",
|
||||||
|
"title": "Questions identite (reponse attendue : OUI)",
|
||||||
|
"sort_order": 25,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"**Si NON a l'une de ces questions, la certification doit etre refusee.**\n\n"
|
||||||
|
"- Je connais cette personne suffisamment pour la recontacter\n"
|
||||||
|
"- J'ai verifie personnellement le lien personne / cle publique\n"
|
||||||
|
"- La personne est une personne physique vivante (pas une entite morale)\n"
|
||||||
|
"- Je pourrais reconnaitre cette personne si je la croisais\n"
|
||||||
|
"- J'ai au moins 2 moyens de contact differents pour cette personne"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "X2.3",
|
||||||
|
"item_type": "verification",
|
||||||
|
"title": "Questions securite (reponse attendue : OUI)",
|
||||||
|
"sort_order": 26,
|
||||||
|
"section_tag": "annexe",
|
||||||
|
"inertia_preset": "low",
|
||||||
|
"current_text": (
|
||||||
|
"**Si NON, la certification est deconseille.**\n\n"
|
||||||
|
"- La personne a genere son document de revocation\n"
|
||||||
|
"- La personne maitrise effectivement son compte "
|
||||||
|
"(test de transfert effectue)\n"
|
||||||
|
"- La personne sait ou retrouver le present acte d'engagement "
|
||||||
|
"dans sa version a jour"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# SECTION SPECIALE : FORMULE DE VOTE
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "F1",
|
||||||
|
"item_type": "section",
|
||||||
|
"title": "Formule de vote",
|
||||||
|
"sort_order": 27,
|
||||||
|
"section_tag": "formule",
|
||||||
|
"inertia_preset": "high",
|
||||||
|
"current_text": (
|
||||||
|
"La formule qui regit l'adoption ou le rejet de chaque modification "
|
||||||
|
"du present document. Reference : g1vote "
|
||||||
|
"(https://g1vote-view-237903.pages.duniter.org/)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "F1.1",
|
||||||
|
"item_type": "rule",
|
||||||
|
"title": "Formule du seuil WoT",
|
||||||
|
"sort_order": 28,
|
||||||
|
"section_tag": "formule",
|
||||||
|
"inertia_preset": "high",
|
||||||
|
"current_text": (
|
||||||
|
"**Seuil = C + B^W + (M + (1-M) × (1 - (T/W)^G)) × max(0, T-C)**\n\n"
|
||||||
|
"Ou :\n"
|
||||||
|
"- **C** = constante de base (plancher fixe)\n"
|
||||||
|
"- **B** = exposant de base (B^W tend vers 0)\n"
|
||||||
|
"- **W** = taille de la toile de confiance (electeurs eligibles)\n"
|
||||||
|
"- **T** = total des votes exprimes (pour + contre)\n"
|
||||||
|
"- **M** = ratio de majorite cible (M = majority_pct / 100)\n"
|
||||||
|
"- **G** = exposant du gradient d'inertie\n\n"
|
||||||
|
"**Comportement :** Faible participation → quasi-unanimite requise. "
|
||||||
|
"Forte participation → majorite simple M suffit."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "F1.2",
|
||||||
|
"item_type": "rule",
|
||||||
|
"title": "Parametres par defaut",
|
||||||
|
"sort_order": 29,
|
||||||
|
"section_tag": "formule",
|
||||||
|
"inertia_preset": "high",
|
||||||
|
"current_text": (
|
||||||
|
"Parametres de reference pour les engagements (inertie standard) :\n\n"
|
||||||
|
"| Parametre | Code | Valeur |\n"
|
||||||
|
"|-----------|------|--------|\n"
|
||||||
|
"| Duree | D | 30 jours (permanent) |\n"
|
||||||
|
"| Majorite | M | 50% |\n"
|
||||||
|
"| Base | B | 0.1 |\n"
|
||||||
|
"| Gradient | G | 0.2 |\n"
|
||||||
|
"| Constante | C | 0.0 |\n\n"
|
||||||
|
"Mode compact : **D30M50B.1G.2**"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "F1.3",
|
||||||
|
"item_type": "rule",
|
||||||
|
"title": "Processus de depot officiel",
|
||||||
|
"sort_order": 30,
|
||||||
|
"section_tag": "formule",
|
||||||
|
"inertia_preset": "high",
|
||||||
|
"current_text": (
|
||||||
|
"Lorsqu'une proposition alternative atteint le seuil d'adoption :\n\n"
|
||||||
|
"1. Le texte de remplacement est integre au document officiel\n"
|
||||||
|
"2. Le hash IPFS du document mis a jour est calcule\n"
|
||||||
|
"3. Le hash est ancre on-chain via `system.remark`\n"
|
||||||
|
"4. Le depot git officiel est synchronise\n"
|
||||||
|
"5. Les applications (Cesium, Gecko) mettent a jour automatiquement"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# SECTION SPECIALE : REGLAGE DE L'INERTIE
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "N1",
|
||||||
|
"item_type": "section",
|
||||||
|
"title": "Reglage de l'inertie",
|
||||||
|
"sort_order": 31,
|
||||||
|
"section_tag": "inertie",
|
||||||
|
"inertia_preset": "very_high",
|
||||||
|
"current_text": (
|
||||||
|
"Le reglage de l'inertie definit la difficulte de remplacement "
|
||||||
|
"de chaque section du document. Ce reglage est lui-meme soumis "
|
||||||
|
"a l'inertie la plus elevee, pour empecher la modification "
|
||||||
|
"des regles de modification."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"position": "N1.1",
|
||||||
|
"item_type": "rule",
|
||||||
|
"title": "Niveaux d'inertie",
|
||||||
|
"sort_order": 32,
|
||||||
|
"section_tag": "inertie",
|
||||||
|
"inertia_preset": "very_high",
|
||||||
|
"current_text": (
|
||||||
|
"| Niveau | Gradient G | Majorite M | Application |\n"
|
||||||
|
"|--------|-----------|------------|-------------|\n"
|
||||||
|
"| **Basse** | 0.1 | 50% | Annexes, recommandations |\n"
|
||||||
|
"| **Standard** | 0.2 | 50% | Engagements fondamentaux et techniques |\n"
|
||||||
|
"| **Haute** | 0.4 | 60% | Formule de vote |\n"
|
||||||
|
"| **Tres haute** | 0.6 | 66% | Reglage de l'inertie |\n\n"
|
||||||
|
"Plus le gradient est eleve, plus il faut de participation "
|
||||||
|
"et de consensus pour qu'une modification soit adoptee."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# ===================================================================
|
||||||
|
# SECTION SPECIALE : ORDONNANCEMENT
|
||||||
|
# ===================================================================
|
||||||
|
{
|
||||||
|
"position": "O1",
|
||||||
|
"item_type": "section",
|
||||||
|
"title": "Ordonnancement du document",
|
||||||
|
"sort_order": 33,
|
||||||
|
"section_tag": "ordonnancement",
|
||||||
|
"inertia_preset": "high",
|
||||||
|
"current_text": (
|
||||||
|
"L'ordre de presentation des items dans le document est "
|
||||||
|
"lui-meme soumis au vote. Toute proposition de reorganisation "
|
||||||
|
"doit atteindre le seuil d'adoption avec l'inertie haute."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
async def seed_document_engagement_certification(session: AsyncSession) -> Document:
|
async def seed_document_engagement_certification(
|
||||||
|
session: AsyncSession,
|
||||||
|
protocols: dict[str, VotingProtocol],
|
||||||
|
) -> Document:
|
||||||
|
genesis = json.dumps(GENESIS_CERTIFICATION, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
doc, created = await get_or_create(
|
doc, created = await get_or_create(
|
||||||
session,
|
session,
|
||||||
Document,
|
Document,
|
||||||
@@ -286,15 +816,33 @@ async def seed_document_engagement_certification(session: AsyncSession) -> Docum
|
|||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
status="active",
|
status="active",
|
||||||
description=(
|
description=(
|
||||||
"Obligations des certificateurs de la toile de confiance Duniter. "
|
"Acte d'engagement des certificateurs de la toile de confiance G1. "
|
||||||
"Chaque clause est soumise au vote permanent de la communaute."
|
"Document modulaire sous vote permanent : chaque item peut etre "
|
||||||
|
"remplace par une alternative qui atteint le seuil d'adoption. "
|
||||||
|
"Construit a partir de la Licence G1 v0.3.0, des discussions "
|
||||||
|
"communautaires et de la position 'Acte d'engagement' (Yvv)."
|
||||||
),
|
),
|
||||||
|
genesis_json=genesis,
|
||||||
)
|
)
|
||||||
print(f" Document 'Acte d'engagement certification': {'created' if created else 'exists'}")
|
print(f" Document 'Acte d'engagement certification': {'created' if created else 'exists'}")
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
|
# Map inertia presets to voting protocols
|
||||||
|
inertia_protocol_map = {
|
||||||
|
"low": protocols.get("Vote WoT standard"),
|
||||||
|
"standard": protocols.get("Vote WoT standard"),
|
||||||
|
"high": protocols.get("Vote WoT standard"),
|
||||||
|
"very_high": protocols.get("Vote WoT standard"),
|
||||||
|
}
|
||||||
|
|
||||||
for item_data in ENGAGEMENT_CERTIFICATION_ITEMS:
|
for item_data in ENGAGEMENT_CERTIFICATION_ITEMS:
|
||||||
item = DocumentItem(document_id=doc.id, **item_data)
|
preset = item_data.get("inertia_preset", "standard")
|
||||||
|
protocol = inertia_protocol_map.get(preset)
|
||||||
|
item = DocumentItem(
|
||||||
|
document_id=doc.id,
|
||||||
|
voting_protocol_id=protocol.id if protocol else None,
|
||||||
|
**item_data,
|
||||||
|
)
|
||||||
session.add(item)
|
session.add(item)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
print(f" -> {len(ENGAGEMENT_CERTIFICATION_ITEMS)} items created")
|
print(f" -> {len(ENGAGEMENT_CERTIFICATION_ITEMS)} items created")
|
||||||
@@ -903,7 +1451,7 @@ async def run_seed():
|
|||||||
protocols = await seed_voting_protocols(session, formulas)
|
protocols = await seed_voting_protocols(session, formulas)
|
||||||
|
|
||||||
print("\n[3/7] Document: Acte d'engagement certification...")
|
print("\n[3/7] Document: Acte d'engagement certification...")
|
||||||
await seed_document_engagement_certification(session)
|
await seed_document_engagement_certification(session, protocols)
|
||||||
|
|
||||||
print("\n[4/7] Document: Acte d'engagement forgeron v2.0.0...")
|
print("\n[4/7] Document: Acte d'engagement forgeron v2.0.0...")
|
||||||
doc_forgeron = await seed_document_engagement_forgeron(session)
|
doc_forgeron = await seed_document_engagement_forgeron(session)
|
||||||
|
|||||||
127
frontend/app/components/documents/DocumentTuto.vue
Normal file
127
frontend/app/components/documents/DocumentTuto.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* DocumentTuto — Quick tutorial overlay explaining how the document works.
|
||||||
|
* Shows how permanent voting, inertia, counter-proposals, and thresholds work.
|
||||||
|
*/
|
||||||
|
const open = ref(false)
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-infinity',
|
||||||
|
title: 'Vote permanent',
|
||||||
|
text: 'Chaque engagement est sous vote permanent. A tout moment, vous pouvez proposer une alternative ou voter pour/contre le texte en vigueur.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-sliders-horizontal',
|
||||||
|
title: 'Inertie variable',
|
||||||
|
text: 'Les engagements fondamentaux ont une inertie standard (difficulte de remplacement moderee). Les annexes sont plus faciles a modifier. La formule et ses reglages sont tres proteges.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-scale',
|
||||||
|
title: 'Seuil adaptatif',
|
||||||
|
text: 'La formule WoT adapte le seuil a la participation : peu de votants = quasi-unanimite requise ; beaucoup de votants = majorite simple suffit.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-pen-line',
|
||||||
|
title: 'Contre-propositions',
|
||||||
|
text: 'Cliquez sur "Proposer une alternative" pour soumettre un texte de remplacement. Il sera soumis au vote et devra atteindre le seuil d\'adoption pour remplacer le texte en vigueur.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'i-lucide-git-branch',
|
||||||
|
title: 'Depot automatique',
|
||||||
|
text: 'Quand une alternative est adoptee, le document officiel est mis a jour, ancre sur IPFS et on-chain, puis deploye dans les applications (Cesium, Gecko).',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UButton
|
||||||
|
label="Comment ca marche ?"
|
||||||
|
icon="i-lucide-circle-help"
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
size="sm"
|
||||||
|
@click="open = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UModal v-model:open="open" :ui="{ content: 'max-w-lg' }">
|
||||||
|
<template #content>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h2 class="text-lg font-bold" style="color: var(--mood-text)">
|
||||||
|
Comment ca marche ?
|
||||||
|
</h2>
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-x"
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
size="xs"
|
||||||
|
@click="open = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(step, idx) in steps"
|
||||||
|
:key="idx"
|
||||||
|
class="tuto-step"
|
||||||
|
>
|
||||||
|
<div class="tuto-step__icon">
|
||||||
|
<UIcon :name="step.icon" class="text-base" />
|
||||||
|
</div>
|
||||||
|
<div class="tuto-step__content">
|
||||||
|
<h4 class="text-sm font-bold" style="color: var(--mood-text)">
|
||||||
|
{{ step.title }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-xs leading-relaxed" style="color: var(--mood-text-muted)">
|
||||||
|
{{ step.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 pt-4 border-t" style="border-color: color-mix(in srgb, var(--mood-text) 8%, transparent)">
|
||||||
|
<p class="text-xs text-center" style="color: var(--mood-text-muted)">
|
||||||
|
Reference : formule g1vote —
|
||||||
|
<a
|
||||||
|
href="https://g1vote-view-237903.pages.duniter.org/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
style="color: var(--mood-accent)"
|
||||||
|
>
|
||||||
|
g1vote-view
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tuto-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tuto-step__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 10%, transparent);
|
||||||
|
color: var(--mood-accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tuto-step__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
252
frontend/app/components/documents/EngagementCard.vue
Normal file
252
frontend/app/components/documents/EngagementCard.vue
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* EngagementCard — Enhanced item card with inline mini vote board,
|
||||||
|
* inertia indicator, and action buttons.
|
||||||
|
*
|
||||||
|
* Replaces the basic ItemCard for the document detail view.
|
||||||
|
*/
|
||||||
|
import type { DocumentItem } from '~/stores/documents'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
item: DocumentItem
|
||||||
|
documentSlug: string
|
||||||
|
showActions?: boolean
|
||||||
|
showVoteBoard?: boolean
|
||||||
|
}>(), {
|
||||||
|
showActions: false,
|
||||||
|
showVoteBoard: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
propose: [item: DocumentItem]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isSection = computed(() => props.item.item_type === 'section')
|
||||||
|
const isPreamble = computed(() => props.item.item_type === 'preamble')
|
||||||
|
|
||||||
|
const itemTypeIcon = computed(() => {
|
||||||
|
switch (props.item.item_type) {
|
||||||
|
case 'clause': return 'i-lucide-shield-check'
|
||||||
|
case 'rule': return 'i-lucide-scale'
|
||||||
|
case 'verification': return 'i-lucide-check-circle'
|
||||||
|
case 'preamble': return 'i-lucide-scroll-text'
|
||||||
|
case 'section': return 'i-lucide-layout-list'
|
||||||
|
default: return 'i-lucide-file-text'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemTypeLabel = computed(() => {
|
||||||
|
switch (props.item.item_type) {
|
||||||
|
case 'clause': return 'Engagement'
|
||||||
|
case 'rule': return 'Regle'
|
||||||
|
case 'verification': return 'Verification'
|
||||||
|
case 'preamble': return 'Preambule'
|
||||||
|
case 'section': return 'Titre'
|
||||||
|
default: return props.item.item_type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function navigateToItem() {
|
||||||
|
navigateTo(`/documents/${props.documentSlug}/items/${props.item.id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Section header (visual separator, not a card) -->
|
||||||
|
<div v-if="isSection" class="engagement-section">
|
||||||
|
<div class="engagement-section__line" />
|
||||||
|
<div class="engagement-section__content">
|
||||||
|
<h3 class="engagement-section__title">
|
||||||
|
{{ item.title }}
|
||||||
|
</h3>
|
||||||
|
<p class="engagement-section__text">
|
||||||
|
{{ item.current_text }}
|
||||||
|
</p>
|
||||||
|
<InertiaSlider
|
||||||
|
:preset="item.inertia_preset"
|
||||||
|
compact
|
||||||
|
class="mt-2 max-w-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regular item card -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="engagement-card"
|
||||||
|
:class="{
|
||||||
|
'engagement-card--preamble': isPreamble,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Card header -->
|
||||||
|
<div class="engagement-card__header" @click="navigateToItem">
|
||||||
|
<div class="flex items-center gap-2.5 min-w-0">
|
||||||
|
<div class="engagement-card__position">
|
||||||
|
{{ item.position }}
|
||||||
|
</div>
|
||||||
|
<UIcon :name="itemTypeIcon" class="text-sm shrink-0" style="color: var(--mood-accent)" />
|
||||||
|
<span v-if="item.title" class="engagement-card__title">
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
<UBadge variant="subtle" color="neutral" size="xs" class="shrink-0">
|
||||||
|
{{ itemTypeLabel }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Item text -->
|
||||||
|
<div class="engagement-card__body" @click="navigateToItem">
|
||||||
|
<MarkdownRenderer :content="item.current_text" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mini vote board -->
|
||||||
|
<div v-if="showVoteBoard" class="engagement-card__vote">
|
||||||
|
<MiniVoteBoard
|
||||||
|
:votes-for="10"
|
||||||
|
:votes-against="1"
|
||||||
|
:wot-size="7224"
|
||||||
|
:is-permanent="item.is_permanent_vote"
|
||||||
|
:inertia-preset="item.inertia_preset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inertia indicator -->
|
||||||
|
<div class="engagement-card__inertia">
|
||||||
|
<InertiaSlider
|
||||||
|
:preset="item.inertia_preset"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div v-if="showActions" class="engagement-card__actions">
|
||||||
|
<UButton
|
||||||
|
label="Proposer une alternative"
|
||||||
|
icon="i-lucide-pen-line"
|
||||||
|
variant="soft"
|
||||||
|
color="primary"
|
||||||
|
size="xs"
|
||||||
|
@click.stop="emit('propose', item)"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
label="Voter"
|
||||||
|
icon="i-lucide-vote"
|
||||||
|
variant="soft"
|
||||||
|
color="success"
|
||||||
|
size="xs"
|
||||||
|
@click.stop="navigateToItem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Section separator */
|
||||||
|
.engagement-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-section__line {
|
||||||
|
width: 4px;
|
||||||
|
background: var(--mood-accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-section__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-section__title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--mood-text);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-section__text {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.engagement-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--mood-surface);
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card:hover {
|
||||||
|
box-shadow: 0 2px 12px color-mix(in srgb, var(--mood-accent) 12%, transparent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card--preamble {
|
||||||
|
border-left: 4px solid color-mix(in srgb, var(--mood-accent) 40%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.875rem 1rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__position {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2rem;
|
||||||
|
height: 1.625rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--mood-accent);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--mood-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__body {
|
||||||
|
padding: 0.5rem 1rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--mood-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__vote {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__inertia {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engagement-card__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--mood-text) 6%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
334
frontend/app/components/documents/GenesisBlock.vue
Normal file
334
frontend/app/components/documents/GenesisBlock.vue
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* Genesis block: displays source documents, repos, forum synthesis, and formula trigger
|
||||||
|
* for a reference document. Collapsible by default.
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
genesisJson: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const expanded = ref(false)
|
||||||
|
|
||||||
|
interface GenesisData {
|
||||||
|
source_document: {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
repo: string
|
||||||
|
}
|
||||||
|
reference_tools: Record<string, string>
|
||||||
|
forum_synthesis: Array<{
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
status: string
|
||||||
|
posts?: number
|
||||||
|
}>
|
||||||
|
formula_trigger: string
|
||||||
|
contributors: Array<{
|
||||||
|
name: string
|
||||||
|
role: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const genesis = computed((): GenesisData | null => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(props.genesisJson)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'rejected': return 'error'
|
||||||
|
case 'in_progress': return 'warning'
|
||||||
|
case 'reference': return 'info'
|
||||||
|
default: return 'neutral'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'rejected': return 'Rejetee'
|
||||||
|
case 'in_progress': return 'En cours'
|
||||||
|
case 'reference': return 'Reference'
|
||||||
|
default: return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="genesis" class="genesis-block">
|
||||||
|
<!-- Header (always visible) -->
|
||||||
|
<button
|
||||||
|
class="genesis-block__header"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="genesis-block__icon">
|
||||||
|
<UIcon name="i-lucide-file-archive" class="text-lg" />
|
||||||
|
</div>
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-sm font-bold uppercase tracking-wide" style="color: var(--mood-accent)">
|
||||||
|
Bloc de genese
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs" style="color: var(--mood-text-muted)">
|
||||||
|
Sources, references et formule de depot
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UIcon
|
||||||
|
:name="expanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||||
|
class="text-lg"
|
||||||
|
style="color: var(--mood-text-muted)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Expandable content -->
|
||||||
|
<Transition name="genesis-expand">
|
||||||
|
<div v-if="expanded" class="genesis-block__body">
|
||||||
|
<!-- Source document -->
|
||||||
|
<div class="genesis-section">
|
||||||
|
<h4 class="genesis-section__title">
|
||||||
|
<UIcon name="i-lucide-file-text" />
|
||||||
|
Document source
|
||||||
|
</h4>
|
||||||
|
<div class="genesis-card">
|
||||||
|
<p class="font-medium text-sm" style="color: var(--mood-text)">
|
||||||
|
{{ genesis.source_document.title }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-1 mt-2">
|
||||||
|
<a
|
||||||
|
:href="genesis.source_document.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="genesis-link"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-external-link" class="text-xs" />
|
||||||
|
Texte officiel
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="genesis.source_document.repo"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="genesis-link"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-git-branch" class="text-xs" />
|
||||||
|
Depot git
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reference tools -->
|
||||||
|
<div class="genesis-section">
|
||||||
|
<h4 class="genesis-section__title">
|
||||||
|
<UIcon name="i-lucide-wrench" />
|
||||||
|
Outils de reference
|
||||||
|
</h4>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<a
|
||||||
|
v-for="(url, name) in genesis.reference_tools"
|
||||||
|
:key="name"
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="genesis-card genesis-card--tool"
|
||||||
|
>
|
||||||
|
<span class="text-xs font-semibold capitalize" style="color: var(--mood-text)">
|
||||||
|
{{ name.replace(/_/g, ' ') }}
|
||||||
|
</span>
|
||||||
|
<UIcon name="i-lucide-external-link" class="text-xs" style="color: var(--mood-text-muted)" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forum synthesis -->
|
||||||
|
<div class="genesis-section">
|
||||||
|
<h4 class="genesis-section__title">
|
||||||
|
<UIcon name="i-lucide-messages-square" />
|
||||||
|
Synthese des discussions
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<a
|
||||||
|
v-for="topic in genesis.forum_synthesis"
|
||||||
|
:key="topic.url"
|
||||||
|
:href="topic.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="genesis-card genesis-card--forum"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<span class="text-xs font-medium" style="color: var(--mood-text)">
|
||||||
|
{{ topic.title }}
|
||||||
|
</span>
|
||||||
|
<UBadge
|
||||||
|
:color="statusColor(topic.status)"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
class="shrink-0"
|
||||||
|
>
|
||||||
|
{{ statusLabel(topic.status) }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
<span v-if="topic.posts" class="text-xs" style="color: var(--mood-text-muted)">
|
||||||
|
{{ topic.posts }} messages
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formula trigger -->
|
||||||
|
<div class="genesis-section">
|
||||||
|
<h4 class="genesis-section__title">
|
||||||
|
<UIcon name="i-lucide-zap" />
|
||||||
|
Processus de depot
|
||||||
|
</h4>
|
||||||
|
<div class="genesis-card">
|
||||||
|
<p class="text-xs leading-relaxed" style="color: var(--mood-text)">
|
||||||
|
{{ genesis.formula_trigger }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contributors -->
|
||||||
|
<div class="genesis-section">
|
||||||
|
<h4 class="genesis-section__title">
|
||||||
|
<UIcon name="i-lucide-users" />
|
||||||
|
Contributeurs
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div
|
||||||
|
v-for="c in genesis.contributors"
|
||||||
|
:key="c.name"
|
||||||
|
class="genesis-contributor"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-xs" style="color: var(--mood-text)">{{ c.name }}</span>
|
||||||
|
<span class="text-xs" style="color: var(--mood-text-muted)">{{ c.role }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.genesis-block {
|
||||||
|
background: var(--mood-surface);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--mood-accent) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-block__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-block__header:hover {
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 5%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-block__icon {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 12%, transparent);
|
||||||
|
color: var(--mood-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-block__body {
|
||||||
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-section__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--mood-accent);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-card {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 4%, var(--mood-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-card--tool {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-card--tool:hover {
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 10%, var(--mood-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-card--forum {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-card--forum:hover {
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 10%, var(--mood-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--mood-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-contributor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 4%, var(--mood-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expand/collapse transition */
|
||||||
|
.genesis-expand-enter-active,
|
||||||
|
.genesis-expand-leave-active {
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genesis-expand-enter-from,
|
||||||
|
.genesis-expand-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
192
frontend/app/components/documents/InertiaSlider.vue
Normal file
192
frontend/app/components/documents/InertiaSlider.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* Inertia slider — displays the inertia preset level for a section.
|
||||||
|
* Read-only indicator (voting on the preset uses the standard vote flow).
|
||||||
|
* Shows the formula parameters underneath.
|
||||||
|
*/
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
preset: string
|
||||||
|
compact?: boolean
|
||||||
|
}>(), {
|
||||||
|
compact: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
interface InertiaLevel {
|
||||||
|
label: string
|
||||||
|
gradient: number
|
||||||
|
majority: number
|
||||||
|
color: string
|
||||||
|
position: number // 0-100 for slider position
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEVELS: Record<string, InertiaLevel> = {
|
||||||
|
low: {
|
||||||
|
label: 'Basse',
|
||||||
|
gradient: 0.1,
|
||||||
|
majority: 50,
|
||||||
|
color: '#22c55e',
|
||||||
|
position: 10,
|
||||||
|
description: 'Facile a remplacer',
|
||||||
|
},
|
||||||
|
standard: {
|
||||||
|
label: 'Standard',
|
||||||
|
gradient: 0.2,
|
||||||
|
majority: 50,
|
||||||
|
color: '#3b82f6',
|
||||||
|
position: 37,
|
||||||
|
description: 'Equilibre participation/consensus',
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
label: 'Haute',
|
||||||
|
gradient: 0.4,
|
||||||
|
majority: 60,
|
||||||
|
color: '#f59e0b',
|
||||||
|
position: 63,
|
||||||
|
description: 'Forte mobilisation requise',
|
||||||
|
},
|
||||||
|
very_high: {
|
||||||
|
label: 'Tres haute',
|
||||||
|
gradient: 0.6,
|
||||||
|
majority: 66,
|
||||||
|
color: '#ef4444',
|
||||||
|
position: 90,
|
||||||
|
description: 'Quasi-unanimite requise',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standard!)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="inertia" :class="{ 'inertia--compact': compact }">
|
||||||
|
<!-- Slider track -->
|
||||||
|
<div class="inertia__track">
|
||||||
|
<div class="inertia__fill" :style="{ width: `${level.position}%`, background: level.color }" />
|
||||||
|
<div
|
||||||
|
class="inertia__thumb"
|
||||||
|
:style="{ left: `${level.position}%`, borderColor: level.color }"
|
||||||
|
/>
|
||||||
|
<!-- Level marks -->
|
||||||
|
<div
|
||||||
|
v-for="(lvl, key) in LEVELS"
|
||||||
|
:key="key"
|
||||||
|
class="inertia__mark"
|
||||||
|
:class="{ 'inertia__mark--active': key === preset }"
|
||||||
|
:style="{ left: `${lvl.position}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label row -->
|
||||||
|
<div class="inertia__info">
|
||||||
|
<span class="inertia__label" :style="{ color: level.color }">
|
||||||
|
{{ level.label }}
|
||||||
|
</span>
|
||||||
|
<span v-if="!compact" class="inertia__params">
|
||||||
|
G={{ level.gradient }} M={{ level.majority }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description (not in compact mode) -->
|
||||||
|
<p v-if="!compact" class="inertia__desc">
|
||||||
|
{{ level.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.inertia {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia--compact {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__track {
|
||||||
|
position: relative;
|
||||||
|
height: 6px;
|
||||||
|
background: color-mix(in srgb, var(--mood-text) 10%, transparent);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia--compact .inertia__track {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__fill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__fill {
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mood-bg);
|
||||||
|
border: 3px solid;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia--compact .inertia__thumb {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__mark {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: color-mix(in srgb, var(--mood-text) 20%, transparent);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__mark--active {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__label {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia--compact .inertia__label {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__params {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inertia__desc {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
248
frontend/app/components/documents/MiniVoteBoard.vue
Normal file
248
frontend/app/components/documents/MiniVoteBoard.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* MiniVoteBoard — compact inline vote status for an engagement item.
|
||||||
|
*
|
||||||
|
* Shows: vote bar, counts, threshold, pass/fail, and vote buttons.
|
||||||
|
* Uses mock data when no vote session is linked (dev mode).
|
||||||
|
*/
|
||||||
|
import { useVoteFormula } from '~/composables/useVoteFormula'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
votesFor?: number
|
||||||
|
votesAgainst?: number
|
||||||
|
wotSize?: number
|
||||||
|
isPermanent?: boolean
|
||||||
|
inertiaPreset?: string
|
||||||
|
startsAt?: string | null
|
||||||
|
endsAt?: string | null
|
||||||
|
}>(), {
|
||||||
|
votesFor: 0,
|
||||||
|
votesAgainst: 0,
|
||||||
|
wotSize: 7224,
|
||||||
|
isPermanent: true,
|
||||||
|
inertiaPreset: 'standard',
|
||||||
|
startsAt: null,
|
||||||
|
endsAt: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { computeThreshold } = useVoteFormula()
|
||||||
|
|
||||||
|
const INERTIA_PARAMS: Record<string, { majority_pct: number; base_exponent: number; gradient_exponent: number; constant_base: number }> = {
|
||||||
|
low: { majority_pct: 50, base_exponent: 0.1, gradient_exponent: 0.1, constant_base: 0 },
|
||||||
|
standard: { majority_pct: 50, base_exponent: 0.1, gradient_exponent: 0.2, constant_base: 0 },
|
||||||
|
high: { majority_pct: 60, base_exponent: 0.1, gradient_exponent: 0.4, constant_base: 0 },
|
||||||
|
very_high: { majority_pct: 66, base_exponent: 0.1, gradient_exponent: 0.6, constant_base: 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const formulaParams = computed(() => INERTIA_PARAMS[props.inertiaPreset] ?? INERTIA_PARAMS.standard!)
|
||||||
|
|
||||||
|
const totalVotes = computed(() => props.votesFor + props.votesAgainst)
|
||||||
|
|
||||||
|
const threshold = computed(() => {
|
||||||
|
if (totalVotes.value === 0) return 1
|
||||||
|
return computeThreshold(props.wotSize, totalVotes.value, formulaParams.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isPassing = computed(() => props.votesFor >= threshold.value)
|
||||||
|
|
||||||
|
const forPct = computed(() => {
|
||||||
|
if (totalVotes.value === 0) return 0
|
||||||
|
return (props.votesFor / totalVotes.value) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const againstPct = computed(() => {
|
||||||
|
if (totalVotes.value === 0) return 0
|
||||||
|
return (props.votesAgainst / totalVotes.value) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const thresholdPct = computed(() => {
|
||||||
|
if (totalVotes.value === 0) return 50
|
||||||
|
return Math.min((threshold.value / totalVotes.value) * 100, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const participationRate = computed(() => {
|
||||||
|
if (props.wotSize === 0) return 0
|
||||||
|
return (totalVotes.value / props.wotSize) * 100
|
||||||
|
})
|
||||||
|
|
||||||
|
const remaining = computed(() => {
|
||||||
|
const diff = threshold.value - props.votesFor
|
||||||
|
return diff > 0 ? diff : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatDate(d: string): string {
|
||||||
|
return new Date(d).toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mini-board">
|
||||||
|
<!-- Vote type badge -->
|
||||||
|
<div class="mini-board__header">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UBadge
|
||||||
|
v-if="isPermanent"
|
||||||
|
color="primary"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-infinity" class="mr-1" />
|
||||||
|
Vote permanent
|
||||||
|
</UBadge>
|
||||||
|
<template v-else>
|
||||||
|
<UBadge color="info" variant="subtle" size="xs">
|
||||||
|
<UIcon name="i-lucide-clock" class="mr-1" />
|
||||||
|
Vote temporaire
|
||||||
|
</UBadge>
|
||||||
|
<span v-if="startsAt && endsAt" class="text-xs" style="color: var(--mood-text-muted)">
|
||||||
|
{{ formatDate(startsAt) }} - {{ formatDate(endsAt) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UBadge
|
||||||
|
:color="isPassing ? 'success' : 'neutral'"
|
||||||
|
:variant="isPassing ? 'solid' : 'subtle'"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ isPassing ? 'Adopte' : 'En attente' }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div class="mini-board__bar">
|
||||||
|
<div
|
||||||
|
class="mini-board__bar-for"
|
||||||
|
:style="{ width: `${forPct}%` }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mini-board__bar-against"
|
||||||
|
:style="{ left: `${forPct}%`, width: `${againstPct}%` }"
|
||||||
|
/>
|
||||||
|
<!-- Threshold marker -->
|
||||||
|
<div
|
||||||
|
v-if="totalVotes > 0"
|
||||||
|
class="mini-board__bar-threshold"
|
||||||
|
:style="{ left: `${thresholdPct}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats row -->
|
||||||
|
<div class="mini-board__stats">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="mini-board__stat mini-board__stat--for">
|
||||||
|
{{ votesFor }} pour
|
||||||
|
</span>
|
||||||
|
<span class="mini-board__stat mini-board__stat--against">
|
||||||
|
{{ votesAgainst }} contre
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="mini-board__stat">
|
||||||
|
{{ votesFor }}/{{ threshold }} requis
|
||||||
|
</span>
|
||||||
|
<span v-if="remaining > 0" class="mini-board__stat mini-board__stat--remaining">
|
||||||
|
{{ remaining }} manquant{{ remaining > 1 ? 's' : '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Participation -->
|
||||||
|
<div class="mini-board__footer">
|
||||||
|
<span class="text-xs" style="color: var(--mood-text-muted)">
|
||||||
|
{{ totalVotes }} vote{{ totalVotes !== 1 ? 's' : '' }} / {{ wotSize }} membres
|
||||||
|
({{ participationRate.toFixed(2) }}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mini-board {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 3%, var(--mood-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__bar {
|
||||||
|
position: relative;
|
||||||
|
height: 6px;
|
||||||
|
background: color-mix(in srgb, var(--mood-text) 10%, transparent);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__bar-for {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
right: auto;
|
||||||
|
background: #22c55e;
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__bar-against {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
right: auto;
|
||||||
|
background: #ef4444;
|
||||||
|
transition: width 0.4s ease, left 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__bar-threshold {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
width: 2px;
|
||||||
|
background: #facc15;
|
||||||
|
border-radius: 1px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
transition: left 0.4s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__stat {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__stat--for {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__stat--against {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__stat--remaining {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-board__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* Document detail page — full structured view with:
|
||||||
|
* - Genesis block (source files, repos, forum synthesis, formula trigger)
|
||||||
|
* - Sectioned items grouped by section_tag
|
||||||
|
* - Mini vote boards per item
|
||||||
|
* - Inertia sliders per section
|
||||||
|
* - Permanent vote signage
|
||||||
|
* - Tuto overlay
|
||||||
|
*/
|
||||||
import type { DocumentItem } from '~/stores/documents'
|
import type { DocumentItem } from '~/stores/documents'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -6,7 +15,6 @@ const documents = useDocumentsStore()
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const slug = computed(() => route.params.slug as string)
|
const slug = computed(() => route.params.slug as string)
|
||||||
|
|
||||||
const archiving = ref(false)
|
const archiving = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -23,6 +31,77 @@ watch(slug, async (newSlug) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ─── Section grouping ──────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Section {
|
||||||
|
tag: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
inertiaPreset: string
|
||||||
|
items: DocumentItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_META: Record<string, { label: string; icon: string }> = {
|
||||||
|
introduction: { label: 'Introduction', icon: 'i-lucide-scroll-text' },
|
||||||
|
fondamental: { label: 'Engagements fondamentaux', icon: 'i-lucide-shield-check' },
|
||||||
|
technique: { label: 'Engagements techniques', icon: 'i-lucide-wrench' },
|
||||||
|
conclusion: { label: 'Conclusion', icon: 'i-lucide-bookmark' },
|
||||||
|
annexe: { label: 'Annexes', icon: 'i-lucide-paperclip' },
|
||||||
|
formule: { label: 'Formule de vote', icon: 'i-lucide-calculator' },
|
||||||
|
inertie: { label: 'Reglage de l\'inertie', icon: 'i-lucide-sliders-horizontal' },
|
||||||
|
ordonnancement: { label: 'Ordonnancement', icon: 'i-lucide-list-ordered' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_ORDER = ['introduction', 'fondamental', 'technique', 'conclusion', 'annexe', 'formule', 'inertie', 'ordonnancement']
|
||||||
|
|
||||||
|
const sections = computed((): Section[] => {
|
||||||
|
const grouped: Record<string, DocumentItem[]> = {}
|
||||||
|
const ungrouped: DocumentItem[] = []
|
||||||
|
|
||||||
|
for (const item of documents.items) {
|
||||||
|
const tag = item.section_tag
|
||||||
|
if (tag) {
|
||||||
|
if (!grouped[tag]) grouped[tag] = []
|
||||||
|
grouped[tag].push(item)
|
||||||
|
} else {
|
||||||
|
ungrouped.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Section[] = []
|
||||||
|
|
||||||
|
for (const tag of SECTION_ORDER) {
|
||||||
|
if (grouped[tag]) {
|
||||||
|
const meta = SECTION_META[tag] || { label: tag, icon: 'i-lucide-file-text' }
|
||||||
|
const firstItem = grouped[tag][0]
|
||||||
|
result.push({
|
||||||
|
tag,
|
||||||
|
label: meta.label,
|
||||||
|
icon: meta.icon,
|
||||||
|
inertiaPreset: firstItem?.inertia_preset || 'standard',
|
||||||
|
items: grouped[tag],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ungrouped items
|
||||||
|
if (ungrouped.length > 0) {
|
||||||
|
result.push({
|
||||||
|
tag: '_other',
|
||||||
|
label: 'Autres',
|
||||||
|
icon: 'i-lucide-file-text',
|
||||||
|
inertiaPreset: 'standard',
|
||||||
|
items: ungrouped,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalItems = computed(() => documents.items.length)
|
||||||
|
|
||||||
|
// ─── Helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
const typeLabel = (docType: string) => {
|
const typeLabel = (docType: string) => {
|
||||||
switch (docType) {
|
switch (docType) {
|
||||||
case 'licence': return 'Licence'
|
case 'licence': return 'Licence'
|
||||||
@@ -55,12 +134,24 @@ async function archiveToSanctuary() {
|
|||||||
archiving.value = false
|
archiving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Active section (scroll spy) ──────────────────────────────
|
||||||
|
|
||||||
|
const activeSection = ref<string | null>(null)
|
||||||
|
|
||||||
|
function scrollToSection(tag: string) {
|
||||||
|
const el = document.getElementById(`section-${tag}`)
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
activeSection.value = tag
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="doc-page">
|
||||||
<!-- Back link -->
|
<!-- Back link -->
|
||||||
<div>
|
<div class="doc-page__nav">
|
||||||
<UButton
|
<UButton
|
||||||
to="/documents"
|
to="/documents"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -94,31 +185,36 @@ async function archiveToSanctuary() {
|
|||||||
|
|
||||||
<!-- Document detail -->
|
<!-- Document detail -->
|
||||||
<template v-else-if="documents.current">
|
<template v-else-if="documents.current">
|
||||||
<!-- Header -->
|
<!-- ═══ HEADER ═══ -->
|
||||||
<div>
|
<div class="doc-page__header">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
<h1 class="doc-page__title">
|
||||||
{{ documents.current.title }}
|
{{ documents.current.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex items-center gap-3 mt-2">
|
<div class="flex items-center gap-3 mt-2 flex-wrap">
|
||||||
<UBadge variant="subtle" color="primary">
|
<UBadge variant="subtle" color="primary">
|
||||||
{{ typeLabel(documents.current.doc_type) }}
|
{{ typeLabel(documents.current.doc_type) }}
|
||||||
</UBadge>
|
</UBadge>
|
||||||
<StatusBadge :status="documents.current.status" type="document" />
|
<StatusBadge :status="documents.current.status" type="document" :clickable="false" />
|
||||||
<span class="text-sm text-gray-500 font-mono">
|
<span class="text-sm font-mono" style="color: var(--mood-text-muted)">
|
||||||
v{{ documents.current.version }}
|
v{{ documents.current.version }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="text-sm" style="color: var(--mood-text-muted)">
|
||||||
|
{{ totalItems }} items
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Archive button for authenticated users with active documents -->
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<div v-if="auth.isAuthenticated && documents.current.status === 'active'" class="flex items-center gap-2">
|
<DocumentTuto />
|
||||||
<UButton
|
<UButton
|
||||||
label="Archiver dans le Sanctuaire"
|
v-if="auth.isAuthenticated && documents.current.status === 'active'"
|
||||||
|
label="Archiver"
|
||||||
icon="i-lucide-archive"
|
icon="i-lucide-archive"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
:loading="archiving"
|
:loading="archiving"
|
||||||
@click="archiveToSanctuary"
|
@click="archiveToSanctuary"
|
||||||
/>
|
/>
|
||||||
@@ -126,71 +222,251 @@ async function archiveToSanctuary() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p v-if="documents.current.description" class="mt-4 text-gray-600 dark:text-gray-400">
|
<p v-if="documents.current.description" class="doc-page__desc">
|
||||||
{{ documents.current.description }}
|
{{ documents.current.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Metadata -->
|
<!-- ═══ METADATA ═══ -->
|
||||||
<UCard>
|
<div class="doc-page__meta">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
<div class="doc-page__meta-grid">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500">Cree le</p>
|
<p class="doc-page__meta-label">Cree le</p>
|
||||||
<p class="font-medium text-gray-900 dark:text-white">
|
<p class="doc-page__meta-value">{{ formatDate(documents.current.created_at) }}</p>
|
||||||
{{ formatDate(documents.current.created_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500">Mis a jour le</p>
|
<p class="doc-page__meta-label">Mis a jour le</p>
|
||||||
<p class="font-medium text-gray-900 dark:text-white">
|
<p class="doc-page__meta-value">{{ formatDate(documents.current.updated_at) }}</p>
|
||||||
{{ formatDate(documents.current.updated_at) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500">Nombre d'items</p>
|
<p class="doc-page__meta-label">Ancrage IPFS</p>
|
||||||
<p class="font-medium text-gray-900 dark:text-white">
|
|
||||||
{{ documents.current.items_count }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-500">Ancrage IPFS</p>
|
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<IPFSLink :cid="documents.current.ipfs_cid" />
|
<IPFSLink :cid="documents.current.ipfs_cid" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="documents.current.chain_anchor">
|
||||||
|
<p class="doc-page__meta-label">Ancrage on-chain</p>
|
||||||
<!-- Chain anchor info -->
|
|
||||||
<div v-if="documents.current.chain_anchor" class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<p class="text-sm text-gray-500">Ancrage on-chain :</p>
|
|
||||||
<ChainAnchor :tx-hash="documents.current.chain_anchor" :block="null" />
|
<ChainAnchor :tx-hash="documents.current.chain_anchor" :block="null" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UCard>
|
</div>
|
||||||
|
|
||||||
<!-- Document items -->
|
<!-- ═══ GENESIS BLOCK ═══ -->
|
||||||
<div>
|
<GenesisBlock
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
v-if="documents.current.genesis_json"
|
||||||
Contenu du document ({{ documents.items.length }} items)
|
:genesis-json="documents.current.genesis_json"
|
||||||
</h2>
|
/>
|
||||||
|
|
||||||
<div v-if="documents.items.length === 0" class="text-center py-8">
|
<!-- ═══ SECTION NAVIGATOR ═══ -->
|
||||||
<UIcon name="i-lucide-file-plus" class="text-4xl text-gray-400 mb-3" />
|
<div v-if="sections.length > 1" class="doc-page__section-nav">
|
||||||
<p class="text-gray-500">Aucun item dans ce document</p>
|
<button
|
||||||
</div>
|
v-for="section in sections"
|
||||||
|
:key="section.tag"
|
||||||
|
class="doc-page__section-pill"
|
||||||
|
:class="{ 'doc-page__section-pill--active': activeSection === section.tag }"
|
||||||
|
@click="scrollToSection(section.tag)"
|
||||||
|
>
|
||||||
|
<UIcon :name="section.icon" class="text-xs" />
|
||||||
|
{{ section.label }}
|
||||||
|
<span class="doc-page__section-count">{{ section.items.length }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<!-- ═══ SECTIONS WITH ITEMS ═══ -->
|
||||||
<ItemCard
|
<div class="doc-page__sections">
|
||||||
v-for="item in documents.items"
|
<div
|
||||||
:key="item.id"
|
v-for="section in sections"
|
||||||
:item="item"
|
:key="section.tag"
|
||||||
:document-slug="slug"
|
:id="`section-${section.tag}`"
|
||||||
:show-actions="auth.isAuthenticated"
|
class="doc-page__section"
|
||||||
@propose="handlePropose"
|
>
|
||||||
/>
|
<!-- Section header -->
|
||||||
|
<div class="doc-page__section-header">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon :name="section.icon" style="color: var(--mood-accent)" />
|
||||||
|
<h2 class="doc-page__section-title">
|
||||||
|
{{ section.label }}
|
||||||
|
</h2>
|
||||||
|
<UBadge variant="subtle" color="neutral" size="xs">
|
||||||
|
{{ section.items.length }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
<InertiaSlider :preset="section.inertiaPreset" compact class="max-w-48" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items -->
|
||||||
|
<div class="doc-page__section-items">
|
||||||
|
<EngagementCard
|
||||||
|
v-for="item in section.items"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
:document-slug="slug"
|
||||||
|
:show-actions="auth.isAuthenticated"
|
||||||
|
@propose="handlePropose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.doc-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 56rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__nav {
|
||||||
|
margin-bottom: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.doc-page__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--mood-text);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.doc-page__title {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__desc {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metadata */
|
||||||
|
.doc-page__meta {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
background: var(--mood-surface);
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.doc-page__meta-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__meta-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__meta-value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mood-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section navigator */
|
||||||
|
.doc-page__section-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.5rem 0.875rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--mood-surface);
|
||||||
|
color: var(--mood-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-pill:hover {
|
||||||
|
background: color-mix(in srgb, var(--mood-accent) 10%, var(--mood-surface));
|
||||||
|
color: var(--mood-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-pill--active {
|
||||||
|
background: var(--mood-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-count {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 800;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.doc-page__sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
scroll-margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 2px solid color-mix(in srgb, var(--mood-accent) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--mood-text);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.doc-page__section-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-page__section-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export interface DocumentItem {
|
|||||||
current_text: string
|
current_text: string
|
||||||
voting_protocol_id: string | null
|
voting_protocol_id: string | null
|
||||||
sort_order: number
|
sort_order: number
|
||||||
|
section_tag: string | null
|
||||||
|
inertia_preset: string
|
||||||
|
is_permanent_vote: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -27,6 +30,7 @@ export interface Document {
|
|||||||
description: string | null
|
description: string | null
|
||||||
ipfs_cid: string | null
|
ipfs_cid: string | null
|
||||||
chain_anchor: string | null
|
chain_anchor: string | null
|
||||||
|
genesis_json: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
items_count: number
|
items_count: number
|
||||||
|
|||||||
Reference in New Issue
Block a user