Toolbox 30rem sticky + accordéons collapsibles + renommage libreDecision

- Boîte à outils élargie à 30rem (×1.75) — flottante sticky, zéro scroll visible
- ToolboxSection : nouveau composant accordéon générique (chevron, défaut fermé)
- ToolboxVignette : titre cliquable, bullets/actions cachés par défaut
- 4 pages : ContextMapper/SocioElection/WorkflowMilestones/inertie → ToolboxSection
- Suppression doublon SectionLayout (common/) — conflit de nommage résolu
- Renommage complet Glibredecision → libreDecision dans configs/docker/CI
- README.md + CONTRIBUTING.md ajoutés

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-17 00:48:20 +01:00
parent 290548703d
commit ed9ed11cd4
24 changed files with 295 additions and 522 deletions

View File

@@ -69,7 +69,7 @@ steps:
from_secret: deploy_key
port: 22
script:
- cd /opt/glibredecision
- cd /opt/libredecision
- docker compose -f docker/docker-compose.yml pull
- docker compose -f docker/docker-compose.yml up -d --remove-orphans
- docker image prune -f

52
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,52 @@
# Contribuer à libreDecision
## Environnement
```bash
# Backend (Python 3.11+)
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
alembic upgrade head
uvicorn app.main:app --host 0.0.0.0 --port 8002 --reload
# Frontend (Node 20+)
cd frontend
npm install
npm run dev
```
## Conventions
- **UI** : français — **Code** : anglais (variables, commentaires, docstrings)
- **CSS** : scoped, sans bordures (`border: none`), profondeur via `box-shadow`
- **Composants** : `pathPrefix: false` — noms courts, auto-import
- **API** : versionnée `/api/v1/`, Pydantic v2, async partout
- **Ports stricts** : frontend=3002, backend=8002 — jamais de fallback
## Architecture toolbox
Chaque section expose une `<SectionLayout>` avec :
- Contenu principal (slot `#default`)
- Boîte à outils sticky (slot `#toolbox`) — 30rem, flottante, zéro scroll
Composants toolbox :
- `ToolboxSection` : accordéon collapsible générique
- `ToolboxVignette` : carte compacte avec bullets toggleables
- `toolbox/ContextMapper` : recommandeur de méthode (4 questions → méthode optimale)
- `toolbox/SocioElection` : guide élection sociocratique + advice process
- `toolbox/WorkflowMilestones` : jalons de protocole (Ostrom)
## Tests
```bash
cd backend && pytest tests/ -v
```
186 tests, zéro dette technique acceptée depuis le sprint 1.
## Formule de vote inertiel
`R = C + B^W + (M + (1-M)·(1-(T/W)^G))·max(0, T-C)`
Voir `docs/content/dev/` pour la documentation complète.

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# libreDecision
Plateforme de décisions collectives pour la communauté Duniter/G1.
Boîte à outils gouvernance multi-collectifs, architecture white-label.
## Stack
- **Frontend** : Nuxt 4 + Vue 3 + Pinia + UnoCSS (port 3002)
- **Backend** : Python FastAPI + SQLAlchemy async + SQLite (port 8002)
- **Auth** : Duniter V2 Ed25519 challenge-response
- **Sanctuaire** : IPFS (kubo) + hash on-chain (system.remark)
## Démarrage
```bash
# Backend
cd backend && .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8002 --reload
# Frontend
cd frontend && npm run dev
```
## Sections
- **Décisions** : processus de vote collectif avec boîte à outils (ContextMapper, consentement, advice process)
- **Mandats** : élection sociocratique, cycle de mandat, révocation
- **Documents** : documents de référence sous vote permanent, niveaux d'inertie, sanctuaire IPFS
- **Protocoles** : protocoles opérationnels, jalons de workflow, formules de vote
## Architecture
Voir `CLAUDE.md` pour les conventions et `docs/content/dev/` pour la documentation technique.

View File

@@ -1,4 +1,4 @@
"""Alembic async environment for Glibredecision.
"""Alembic async environment for libreDecision.
Uses asyncpg via SQLAlchemy's async engine.
All models are imported so that Base.metadata is fully populated

View File

@@ -3,7 +3,7 @@ from pathlib import Path
class Settings(BaseSettings):
APP_NAME: str = "Glibredecision"
APP_NAME: str = "libreDecision"
DEBUG: bool = True
# Environment
@@ -11,7 +11,7 @@ class Settings(BaseSettings):
LOG_LEVEL: str = "INFO"
# Database — SQLite by default for local dev, PostgreSQL for Docker/prod
DATABASE_URL: str = "sqlite+aiosqlite:///./glibredecision.db"
DATABASE_URL: str = "sqlite+aiosqlite:///./libredecision.db"
DATABASE_POOL_SIZE: int = 20
DATABASE_MAX_OVERFLOW: int = 10

View File

@@ -240,7 +240,7 @@ async def platform_status(
sanctuary_count = sanctuary_count_result.scalar() or 0
return {
"platform": "Glibredecision",
"platform": "libreDecision",
"documents_count": documents_count,
"decisions_count": decisions_count,
"active_votes_count": active_votes_count,

View File

@@ -1,6 +1,6 @@
"""Sanctuary service: immutable archival to IPFS + on-chain hash.
The sanctuary is the immutable layer of Glibredecision. Every adopted
The sanctuary is the immutable layer of libreDecision. Every adopted
document version, decision result, or vote tally is hashed (SHA-256),
stored on IPFS, and anchored on-chain via system.remark.
"""
@@ -241,7 +241,7 @@ async def _anchor_on_chain(content_hash: str) -> tuple[str | None, int | None]:
call = substrate.compose_call(
call_module="System",
call_function="remark",
call_params={"remark": f"glibredecision:sanctuary:{content_hash}"},
call_params={"remark": f"libredecision:sanctuary:{content_hash}"},
)
extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair)
receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)

View File

@@ -2149,7 +2149,7 @@ async def seed_votes_on_items(
async def run_seed():
print("=" * 60)
print("Glibredecision - Seed Database")
print("libreDecision - Seed Database")
print("=" * 60)
# Ensure tables exist

View File

@@ -7,15 +7,15 @@ services:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-glibredecision}
POSTGRES_USER: ${POSTGRES_USER:-glibredecision}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-glibredecision-dev}
POSTGRES_DB: ${POSTGRES_DB:-libredecision}
POSTGRES_USER: ${POSTGRES_USER:-libredecision}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-libredecision-dev}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-glibredecision}"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-libredecision}"]
interval: 5s
timeout: 3s
retries: 10
@@ -34,7 +34,7 @@ services:
ports:
- "8002:8002"
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-glibredecision}:${POSTGRES_PASSWORD:-glibredecision-dev}@postgres:5432/${POSTGRES_DB:-glibredecision}
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-libredecision}:${POSTGRES_PASSWORD:-libredecision-dev}@postgres:5432/${POSTGRES_DB:-libredecision}
SECRET_KEY: dev-secret-key-not-for-production
DEBUG: "true"
ENVIRONMENT: development

View File

@@ -5,19 +5,19 @@ services:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-glibredecision}
POSTGRES_USER: ${POSTGRES_USER:-glibredecision}
POSTGRES_DB: ${POSTGRES_DB:-libredecision}
POSTGRES_USER: ${POSTGRES_USER:-libredecision}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me-in-production}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-glibredecision} -d ${POSTGRES_DB:-glibredecision}"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-libredecision} -d ${POSTGRES_DB:-libredecision}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- glibredecision
- libredecision
backend:
build:
@@ -29,21 +29,21 @@ services:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-glibredecision}:${POSTGRES_PASSWORD:-change-me-in-production}@postgres:5432/${POSTGRES_DB:-glibredecision}
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-libredecision}:${POSTGRES_PASSWORD:-change-me-in-production}@postgres:5432/${POSTGRES_DB:-libredecision}
SECRET_KEY: ${SECRET_KEY:-change-me-in-production-with-a-real-secret-key}
DEBUG: "false"
CORS_ORIGINS: '["https://${DOMAIN:-glibredecision.org}"]'
CORS_ORIGINS: '["https://${DOMAIN:-libredecision.org}"]'
DUNITER_RPC_URL: ${DUNITER_RPC_URL:-wss://gdev.p2p.legal/ws}
IPFS_API_URL: http://ipfs:5001
IPFS_GATEWAY_URL: http://ipfs:8080
labels:
- "traefik.enable=true"
- "traefik.http.routers.glibredecision-api.rule=Host(`${DOMAIN:-glibredecision.org}`) && PathPrefix(`/api`)"
- "traefik.http.routers.glibredecision-api.entrypoints=websecure"
- "traefik.http.routers.glibredecision-api.tls.certresolver=letsencrypt"
- "traefik.http.services.glibredecision-api.loadbalancer.server.port=8002"
- "traefik.http.routers.libredecision-api.rule=Host(`${DOMAIN:-libredecision.org}`) && PathPrefix(`/api`)"
- "traefik.http.routers.libredecision-api.entrypoints=websecure"
- "traefik.http.routers.libredecision-api.tls.certresolver=letsencrypt"
- "traefik.http.services.libredecision-api.loadbalancer.server.port=8002"
networks:
- glibredecision
- libredecision
- traefik
frontend:
@@ -55,15 +55,15 @@ services:
depends_on:
- backend
environment:
NUXT_PUBLIC_API_BASE: https://${DOMAIN:-glibredecision.org}/api/v1
NUXT_PUBLIC_API_BASE: https://${DOMAIN:-libredecision.org}/api/v1
labels:
- "traefik.enable=true"
- "traefik.http.routers.glibredecision-front.rule=Host(`${DOMAIN:-glibredecision.org}`)"
- "traefik.http.routers.glibredecision-front.entrypoints=websecure"
- "traefik.http.routers.glibredecision-front.tls.certresolver=letsencrypt"
- "traefik.http.services.glibredecision-front.loadbalancer.server.port=3000"
- "traefik.http.routers.libredecision-front.rule=Host(`${DOMAIN:-libredecision.org}`)"
- "traefik.http.routers.libredecision-front.entrypoints=websecure"
- "traefik.http.routers.libredecision-front.tls.certresolver=letsencrypt"
- "traefik.http.services.libredecision-front.loadbalancer.server.port=3000"
networks:
- glibredecision
- libredecision
- traefik
ipfs:
@@ -72,14 +72,14 @@ services:
volumes:
- ipfs-data:/data/ipfs
networks:
- glibredecision
- libredecision
volumes:
postgres-data:
ipfs-data:
networks:
glibredecision:
libredecision:
driver: bridge
traefik:
external: true

View File

@@ -264,7 +264,7 @@ function toggleStatus(statusId: string) {
@media (min-width: 1024px) {
.section__body {
grid-template-columns: 1fr 17rem;
grid-template-columns: 1fr 30rem;
}
}
@@ -320,10 +320,12 @@ function toggleStatus(statusId: string) {
display: none;
position: sticky;
top: 4.5rem;
align-self: start;
flex-direction: column;
background: var(--mood-surface);
border-radius: 16px;
overflow: hidden;
max-height: calc(100vh - 5.5rem);
box-shadow: 0 4px 24px var(--mood-shadow);
}
@media (min-width: 1024px) {
@@ -340,6 +342,7 @@ function toggleStatus(statusId: string) {
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.section__toolbox-head-icon {
@@ -350,10 +353,8 @@ function toggleStatus(statusId: string) {
padding: 0 0.75rem 0.875rem;
display: flex;
flex-direction: column;
gap: 0.625rem;
max-height: calc(100vh - 8rem);
overflow-y: auto;
scrollbar-width: thin;
gap: 0.5rem;
overflow: hidden;
}
.section__toolbox-empty {

View File

@@ -1,318 +0,0 @@
<script setup lang="ts">
/**
* SectionLayout — Mise en page pour sections.
*
* Status pills inside the content block (not header).
* Toolbox sidebar with condensed content.
*/
export interface StatusFilter {
id: string
label: string
count: number
cssClass?: string
}
export interface ToolboxItem {
title: string
description: string
actions: Array<{
label: string
to?: string
onClick?: () => void
}>
}
const props = withDefaults(
defineProps<{
title: string
subtitle?: string
statuses: StatusFilter[]
toolboxItems?: ToolboxItem[]
activeStatus?: string | null
}>(),
{
subtitle: undefined,
toolboxItems: undefined,
activeStatus: null,
},
)
const emit = defineEmits<{
'update:activeStatus': [status: string | null]
}>()
const toolboxOpen = ref(false)
const statusCssMap: Record<string, string> = {
draft: 'status-prepa',
qualification: 'status-prepa',
candidacy: 'status-prepa',
voting: 'status-vote',
review: 'status-vote',
active: 'status-vigueur',
executed: 'status-vigueur',
completed: 'status-vigueur',
closed: 'status-clos',
archived: 'status-clos',
revoked: 'status-clos',
reporting: 'status-vote',
}
function getStatusClass(status: StatusFilter): string {
return status.cssClass || statusCssMap[status.id] || 'status-prepa'
}
function toggleStatus(statusId: string) {
if (props.activeStatus === statusId) {
emit('update:activeStatus', null)
}
else {
emit('update:activeStatus', statusId)
}
}
</script>
<template>
<div class="section">
<!-- Header: just title -->
<div class="section__header">
<h1 class="section__title">{{ title }}</h1>
<p v-if="subtitle" class="section__subtitle">{{ subtitle }}</p>
</div>
<!-- Body: content + toolbox -->
<div class="section__body">
<div class="section__main">
<!-- Status pills INSIDE the list block -->
<div v-if="statuses.length > 0" class="section__pills">
<button
v-for="status in statuses"
:key="status.id"
type="button"
class="status-pill"
:class="[getStatusClass(status), { active: activeStatus === status.id }]"
@click="toggleStatus(status.id)"
>
{{ status.label }}
<span v-if="status.count > 0" class="section__pill-count">{{ status.count }}</span>
</button>
</div>
<div v-if="$slots.search" class="section__search">
<slot name="search" />
</div>
<div class="section__content">
<slot />
</div>
</div>
<aside class="section__toolbox">
<button class="section__toolbox-head" @click="toolboxOpen = !toolboxOpen">
<div class="section__toolbox-head-left">
<UIcon name="i-lucide-wrench" />
<span>Boite a outils</span>
</div>
<UIcon
:name="toolboxOpen ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
class="section__toolbox-toggle"
/>
</button>
<div class="section__toolbox-content" :class="{ 'section__toolbox-content--open': toolboxOpen }">
<div v-if="$slots.toolbox" class="section__toolbox-body">
<slot name="toolbox" />
</div>
<div v-else-if="toolboxItems && toolboxItems.length > 0" class="section__toolbox-body">
<ToolboxVignette
v-for="(item, idx) in toolboxItems"
:key="idx"
:title="item.title"
/>
</div>
<div v-else class="section__toolbox-empty">
Aucun outil disponible
</div>
</div>
</aside>
</div>
</div>
</template>
<style scoped>
.section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section__header {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.section__title {
font-size: 1.375rem;
font-weight: 800;
color: var(--mood-text);
letter-spacing: -0.02em;
}
@media (min-width: 640px) {
.section__title {
font-size: 1.75rem;
}
}
.section__subtitle {
font-size: 0.875rem;
color: var(--mood-text-muted);
font-weight: 500;
}
@media (min-width: 640px) {
.section__subtitle {
font-size: 1rem;
}
}
.section__body {
display: grid;
grid-template-columns: 1fr 16rem;
gap: 1.5rem;
align-items: start;
}
.section__main {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0;
}
.section__pills {
display: flex;
gap: 0.5rem;
align-items: center;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 2px;
}
.section__pills::-webkit-scrollbar {
display: none;
}
@media (min-width: 640px) {
.section__pills {
flex-wrap: wrap;
overflow-x: visible;
}
}
.section__pill-count {
margin-left: 0.25rem;
font-size: 0.6875rem;
font-weight: 800;
opacity: 0.7;
}
.section__search {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: stretch;
}
@media (max-width: 639px) {
.section__search {
flex-direction: column;
}
}
.section__content {
min-height: 12rem;
}
.section__toolbox {
position: sticky;
top: 4.5rem;
display: flex;
flex-direction: column;
background: var(--mood-surface);
border-radius: 16px;
overflow: hidden;
}
.section__toolbox-head {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 1rem;
cursor: pointer;
background: none;
}
.section__toolbox-head-left {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
font-weight: 800;
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.section__toolbox-toggle {
color: var(--mood-text-muted);
font-size: 0.875rem;
}
.section__toolbox-content {
display: none;
padding: 0 1rem 1rem;
}
.section__toolbox-content--open {
display: block;
}
.section__toolbox-body {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.section__toolbox-empty {
font-size: 0.875rem;
color: var(--mood-text-muted);
text-align: center;
padding: 1rem 0;
}
/* Desktop: toolbox always open, no toggle */
@media (min-width: 1024px) {
.section__toolbox-head {
cursor: default;
}
.section__toolbox-toggle {
display: none;
}
.section__toolbox-content {
display: block;
}
}
@media (max-width: 1023px) {
.section__body {
grid-template-columns: 1fr;
}
.section__toolbox {
position: static;
order: 2;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
/**
* ToolboxVignette — Carte compacte, bullet points, bouton Demarrer.
* ToolboxVignette — Carte compacte, collapsible, bullet points + actions.
*/
export interface ToolboxAction {
@@ -16,10 +16,12 @@ const props = withDefaults(
title: string
bullets?: string[]
actions?: ToolboxAction[]
defaultOpen?: boolean
}>(),
{
bullets: undefined,
actions: undefined,
defaultOpen: false,
},
)
@@ -27,6 +29,8 @@ const emit = defineEmits<{
action: [actionEmit: string]
}>()
const open = ref(props.defaultOpen)
const defaultActions: ToolboxAction[] = [
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
{ label: 'Formules', icon: 'i-lucide-calculator', emit: 'formules' },
@@ -46,22 +50,27 @@ function handleAction(action: ToolboxAction) {
</script>
<template>
<div class="vignette">
<h4 class="vignette__title">{{ title }}</h4>
<ul v-if="bullets && bullets.length > 0" class="vignette__bullets">
<li v-for="(b, i) in bullets" :key="i">{{ b }}</li>
</ul>
<div class="vignette__actions">
<button
v-for="action in resolvedActions"
:key="action.label"
class="vignette__btn"
:class="{ 'vignette__btn--primary': action.primary }"
@click="handleAction(action)"
>
<UIcon v-if="action.icon" :name="action.icon" />
<span>{{ action.label }}</span>
</button>
<div class="vignette" :class="{ 'vignette--open': open }">
<button class="vignette__header" @click="open = !open">
<h4 class="vignette__title">{{ title }}</h4>
<UIcon name="i-lucide-chevron-down" class="vignette__chevron" />
</button>
<div v-show="open" class="vignette__content">
<ul v-if="bullets && bullets.length > 0" class="vignette__bullets">
<li v-for="(b, i) in bullets" :key="i">{{ b }}</li>
</ul>
<div class="vignette__actions">
<button
v-for="action in resolvedActions"
:key="action.label"
class="vignette__btn"
:class="{ 'vignette__btn--primary': action.primary }"
@click="handleAction(action)"
>
<UIcon v-if="action.icon" :name="action.icon" />
<span>{{ action.label }}</span>
</button>
</div>
</div>
</div>
</template>
@@ -70,19 +79,53 @@ function handleAction(action: ToolboxAction) {
.vignette {
background: var(--mood-accent-soft);
border-radius: 12px;
padding: 0.75rem;
overflow: hidden;
}
.vignette__header {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
width: 100%;
padding: 0.625rem 0.75rem;
cursor: pointer;
text-align: left;
gap: 0.375rem;
transition: background 0.12s ease;
}
.vignette__header:hover {
background: color-mix(in srgb, var(--mood-accent) 8%, transparent);
}
.vignette__title {
flex: 1;
font-size: 0.9375rem;
font-weight: 700;
color: var(--mood-text);
margin: 0;
}
.vignette__chevron {
font-size: 0.875rem;
color: var(--mood-text-muted);
opacity: 0.5;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.vignette--open .vignette__chevron {
transform: rotate(180deg);
opacity: 1;
color: var(--mood-accent);
}
.vignette__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0 0.75rem 0.625rem;
}
.vignette__bullets {
margin: 0;
padding: 0 0 0 1rem;

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
/**
* ToolboxSection — Wrapper accordéon pour la boîte à outils.
* Toggle le contenu pour économiser la hauteur visible.
*/
const props = withDefaults(
defineProps<{
title: string
icon?: string
defaultOpen?: boolean
}>(),
{
icon: undefined,
defaultOpen: false,
},
)
const open = ref(props.defaultOpen)
</script>
<template>
<div class="tsection" :class="{ 'tsection--open': open }">
<button class="tsection__header" @click="open = !open">
<UIcon v-if="icon" :name="icon" class="tsection__icon" />
<span class="tsection__title">{{ title }}</span>
<UIcon name="i-lucide-chevron-down" class="tsection__chevron" />
</button>
<div v-show="open" class="tsection__body">
<slot />
</div>
</div>
</template>
<style scoped>
.tsection {
background: var(--mood-accent-soft);
border-radius: 14px;
overflow: hidden;
}
.tsection__header {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 0.875rem;
cursor: pointer;
text-align: left;
transition: background 0.12s ease;
}
.tsection__header:hover {
background: color-mix(in srgb, var(--mood-accent) 10%, transparent);
}
.tsection__icon {
font-size: 0.9375rem;
color: var(--mood-accent);
flex-shrink: 0;
}
.tsection__title {
flex: 1;
font-size: 0.8125rem;
font-weight: 800;
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.tsection__chevron {
font-size: 0.875rem;
color: var(--mood-accent);
opacity: 0.6;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.tsection--open .tsection__chevron {
transform: rotate(180deg);
}
.tsection__body {
padding: 0 0.875rem 0.875rem;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
</style>

View File

@@ -1,5 +1,5 @@
/**
* Composable for making authenticated API calls to the Glibredecision backend.
* Composable for making authenticated API calls to the libreDecision backend.
*
* Uses the runtime config `apiBase` and automatically injects the Bearer token
* from the auth store when available.

View File

@@ -9,7 +9,7 @@ export interface Mood {
isDark: boolean
}
const STORAGE_KEY = 'glibredecision_mood'
const STORAGE_KEY = 'libredecision_mood'
const moods: Mood[] = [
{ id: 'peps', label: 'Peps', description: 'Chaud et tonique', icon: 'i-lucide-sun', color: '#d44a10', isDark: false },

View File

@@ -238,13 +238,9 @@ function formatDate(dateStr: string): string {
<!-- Toolbox sidebar -->
<template #toolbox>
<!-- Context mapper -->
<div class="toolbox-block">
<div class="toolbox-block__head">
<UIcon name="i-lucide-compass" />
<span>Quelle méthode ?</span>
</div>
<ToolboxSection title="Quelle méthode ?" icon="i-lucide-compass">
<ContextMapper @use="handleMethodSelect" />
</div>
</ToolboxSection>
<!-- Vote inertiel WoT -->
<ToolboxVignette
@@ -532,26 +528,6 @@ function formatDate(dateStr: string): string {
transform: translateY(0);
}
.toolbox-block {
background: var(--mood-accent-soft);
border-radius: 14px;
padding: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.toolbox-block__head {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 800;
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* Decision modal */
.decision-modal {
padding: 1.25rem;

View File

@@ -287,11 +287,7 @@ async function createDocument() {
<!-- Toolbox sidebar -->
<template #toolbox>
<!-- Inertia guide -->
<div class="toolbox-block">
<div class="toolbox-block__head">
<UIcon name="i-lucide-sliders-horizontal" />
<span>Niveaux d'inertie</span>
</div>
<ToolboxSection title="Niveaux d'inertie" icon="i-lucide-sliders-horizontal">
<div class="inertia-guide">
<div v-for="level in inertiaLevels" :key="level.id" class="inertia-level">
<div class="inertia-level__header">
@@ -308,7 +304,7 @@ async function createDocument() {
<UIcon name="i-lucide-calculator" />
Simuler les formules
</NuxtLink>
</div>
</ToolboxSection>
<!-- Structure document -->
<ToolboxVignette
@@ -534,27 +530,6 @@ async function createDocument() {
}
}
/* Toolbox blocks */
.toolbox-block {
background: var(--mood-accent-soft);
border-radius: 14px;
padding: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.toolbox-block__head {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 800;
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* Inertia guide */
.inertia-guide {
display: flex;

View File

@@ -273,13 +273,9 @@ async function handleCreate() {
<!-- Toolbox sidebar -->
<template #toolbox>
<!-- Sociocratic election guide -->
<div class="toolbox-block">
<div class="toolbox-block__head">
<UIcon name="i-lucide-users" />
<span>Nomination & Élection</span>
</div>
<ToolboxSection title="Nomination & Élection" icon="i-lucide-users">
<SocioElection />
</div>
</ToolboxSection>
<!-- Mandat cycle -->
<ToolboxVignette
@@ -558,26 +554,6 @@ async function handleCreate() {
margin-top: 0.5rem;
}
.toolbox-block {
background: var(--mood-accent-soft);
border-radius: 14px;
padding: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.toolbox-block__head {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 800;
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.mandate-card__type-badge {
font-size: 0.6875rem;
font-weight: 700;

View File

@@ -410,13 +410,9 @@ const n8nWorkflows = [
<!-- Toolbox sidebar -->
<template #toolbox>
<!-- Workflow milestones -->
<div class="toolbox-block">
<div class="toolbox-block__head">
<UIcon name="i-lucide-git-branch" />
<span>Jalons de protocole</span>
</div>
<ToolboxSection title="Jalons de protocole" icon="i-lucide-git-branch">
<WorkflowMilestones />
</div>
</ToolboxSection>
<!-- Simulateur -->
<ToolboxVignette
@@ -428,11 +424,7 @@ const n8nWorkflows = [
/>
<!-- n8n Workflows -->
<div class="n8n-section">
<div class="n8n-section__head">
<UIcon name="i-lucide-workflow" class="text-xs" />
<span>Automatisations</span>
</div>
<ToolboxSection title="Automatisations" icon="i-lucide-workflow">
<div class="n8n-workflows">
<div
v-for="wf in n8nWorkflows"
@@ -456,7 +448,7 @@ const n8nWorkflows = [
</div>
</div>
</div>
</div>
</ToolboxSection>
<!-- Meta-gouvernance -->
<ToolboxVignette
@@ -850,52 +842,6 @@ const n8nWorkflows = [
font-family: inherit !important;
}
/* Toolbox blocks */
.toolbox-block {
background: var(--mood-accent-soft);
border-radius: 14px;
padding: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.toolbox-block__head {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 800;
color: var(--mood-accent);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* --- n8n Section --- */
.n8n-section {
background: var(--mood-accent-soft);
border-radius: 12px;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.n8n-section__head {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 700;
color: var(--mood-tertiary, var(--mood-accent));
}
.n8n-section__desc {
font-size: 0.75rem;
color: var(--mood-text-muted);
margin: 0;
}
.n8n-workflows {
display: flex;
flex-direction: column;

View File

@@ -153,7 +153,7 @@ export const useAuthStore = defineStore('auth', {
*/
hydrateFromStorage() {
if (import.meta.client) {
const stored = localStorage.getItem('glibredecision_token')
const stored = localStorage.getItem('libredecision_token')
if (stored) {
this.token = stored
}
@@ -163,14 +163,14 @@ export const useAuthStore = defineStore('auth', {
/** @internal Persist token to localStorage */
_persistToken() {
if (import.meta.client && this.token) {
localStorage.setItem('glibredecision_token', this.token)
localStorage.setItem('libredecision_token', this.token)
}
},
/** @internal Clear token from localStorage */
_clearToken() {
if (import.meta.client) {
localStorage.removeItem('glibredecision_token')
localStorage.removeItem('libredecision_token')
}
},
},

View File

@@ -21,11 +21,11 @@ export default defineNuxtConfig({
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' },
{ name: 'mobile-web-app-capable', content: 'yes' },
{ property: 'og:title', content: 'Glibredecision' },
{ property: 'og:title', content: 'libreDecision' },
{ property: 'og:description', content: 'Decisions collectives pour la communaute Duniter/G1' },
{ property: 'og:type', content: 'website' },
],
title: 'Glibredecision',
title: 'libreDecision',
link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },

View File

@@ -1,5 +1,5 @@
{
"name": "glibredecision",
"name": "libredecision",
"version": "0.1.0",
"type": "module",
"private": true,

View File

@@ -1,4 +1,4 @@
# Recherche Forum Duniter -- Donnees de reference pour Glibredecision
# Recherche Forum Duniter -- Donnees de reference pour libreDecision
Date de recherche : 2026-02-28