Boîtes à outils enrichies : ContextMapper, SocioElection, WorkflowMilestones

- ContextMapper : 4 questions contexte → méthode de décision optimale
  (advice process Laloux, vote inertiel WoT, consentement sociocratique, Smith…)
- SocioElection : guide élection sociocratique 6 étapes + advice process + clarté de rôle
- WorkflowMilestones : 11 jalons de protocole (7 essentiels), durées recommandées, principes Ostrom
- WorkspaceSelector : sélecteur de collectif multi-site dans le header
- SectionLayout : toolbox en USlideover droit sur mobile, sidebar sticky desktop
- Décisions : ContextMapper intégré + guide consentement
- Mandats : SocioElection intégré + cycle de mandat
- Documents : guide inertie 4 niveaux + structure + IPFS
- Protocoles : WorkflowMilestones + protocole élection sociocratique ajouté
- Renommage projet Glibredecision → libreDecision (dossier + sources)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-17 00:13:08 +01:00
parent 316d205593
commit 290548703d
29 changed files with 4174 additions and 168 deletions

View File

@@ -0,0 +1,272 @@
<script setup lang="ts">
/**
* WorkspaceSelector — Sélecteur de collectif / espace de travail.
* Compartimentage multi-collectifs, multi-sites.
* UI-only pour l'instant, prêt pour le backend (collective_id sur toutes les entités).
*/
interface Workspace {
id: string
name: string
slug: string
icon: string
role?: string
color?: string
}
// Mock data — sera remplacé par le store collectifs
const workspaces: Workspace[] = [
{
id: 'g1-main',
name: 'Duniter G1',
slug: 'duniter-g1',
icon: 'i-lucide-coins',
role: 'Membre',
color: 'accent',
},
{
id: 'axiom',
name: 'Axiom Team',
slug: 'axiom-team',
icon: 'i-lucide-layers',
role: 'Admin',
color: 'secondary',
},
]
const activeId = ref('g1-main')
const isOpen = ref(false)
const active = computed(() => workspaces.find(w => w.id === activeId.value) ?? workspaces[0])
function selectWorkspace(id: string) {
activeId.value = id
isOpen.value = false
// TODO: store.setActiveCollective(id) + refetch all data
}
// Close on outside click
const containerRef = ref<HTMLElement | null>(null)
onMounted(() => {
document.addEventListener('click', (e) => {
if (containerRef.value && !containerRef.value.contains(e.target as Node)) {
isOpen.value = false
}
})
})
</script>
<template>
<div ref="containerRef" class="ws">
<button class="ws__trigger" :class="{ 'ws__trigger--open': isOpen }" @click="isOpen = !isOpen">
<div class="ws__icon" :class="`ws__icon--${active.color}`">
<UIcon :name="active.icon" />
</div>
<span class="ws__name">{{ active.name }}</span>
<UIcon name="i-lucide-chevrons-up-down" class="ws__caret" />
</button>
<Transition name="dropdown">
<div v-if="isOpen" class="ws__dropdown">
<div class="ws__dropdown-header">
Espace de travail
</div>
<div class="ws__items">
<button
v-for="ws in workspaces"
:key="ws.id"
class="ws__item"
:class="{ 'ws__item--active': ws.id === activeId }"
@click="selectWorkspace(ws.id)"
>
<div class="ws__item-icon" :class="`ws__icon--${ws.color}`">
<UIcon :name="ws.icon" />
</div>
<div class="ws__item-info">
<span class="ws__item-name">{{ ws.name }}</span>
<span v-if="ws.role" class="ws__item-role">{{ ws.role }}</span>
</div>
<UIcon v-if="ws.id === activeId" name="i-lucide-check" class="ws__item-check" />
</button>
</div>
<div class="ws__dropdown-footer">
<button class="ws__new-btn" disabled>
<UIcon name="i-lucide-plus" />
Nouveau collectif
</button>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.ws {
position: relative;
}
.ws__trigger {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.625rem;
background: var(--mood-accent-soft);
border-radius: 10px;
cursor: pointer;
transition: all 0.12s ease;
min-height: 2rem;
max-width: 11rem;
}
.ws__trigger:hover {
background: color-mix(in srgb, var(--mood-accent-soft) 80%, var(--mood-accent) 20%);
}
.ws__trigger--open {
background: color-mix(in srgb, var(--mood-accent-soft) 60%, var(--mood-accent) 40%);
}
.ws__icon {
width: 1.375rem;
height: 1.375rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
font-size: 0.75rem;
}
.ws__icon--accent { background: var(--mood-accent); color: var(--mood-accent-text); }
.ws__icon--secondary {
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 20%, transparent);
color: var(--mood-secondary, var(--mood-accent));
}
.ws__name {
font-size: 0.8125rem;
font-weight: 700;
color: var(--mood-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.ws__caret {
font-size: 0.75rem;
color: var(--mood-text-muted);
flex-shrink: 0;
}
/* Dropdown */
.ws__dropdown {
position: absolute;
top: calc(100% + 0.375rem);
left: 0;
min-width: 13rem;
background: var(--mood-surface);
border-radius: 14px;
box-shadow: 0 8px 32px var(--mood-shadow);
z-index: 100;
overflow: hidden;
}
.ws__dropdown-header {
padding: 0.625rem 0.875rem 0.375rem;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--mood-text-muted);
}
.ws__items {
padding: 0.25rem 0.5rem;
display: flex;
flex-direction: column;
gap: 2px;
}
.ws__item {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.625rem;
border-radius: 10px;
cursor: pointer;
transition: background 0.1s ease;
text-align: left;
width: 100%;
}
.ws__item:hover { background: var(--mood-accent-soft); }
.ws__item--active { background: var(--mood-accent-soft); }
.ws__item-icon {
width: 1.75rem;
height: 1.75rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
font-size: 0.875rem;
}
.ws__item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.ws__item-name {
font-size: 0.875rem;
font-weight: 600;
color: var(--mood-text);
}
.ws__item-role {
font-size: 0.6875rem;
color: var(--mood-text-muted);
}
.ws__item-check {
color: var(--mood-accent);
font-size: 0.875rem;
flex-shrink: 0;
}
.ws__dropdown-footer {
padding: 0.5rem;
border-top: 1px solid var(--mood-accent-soft);
}
.ws__new-btn {
display: flex;
align-items: center;
gap: 0.375rem;
width: 100%;
padding: 0.5rem 0.625rem;
border-radius: 10px;
font-size: 0.8125rem;
font-weight: 600;
color: var(--mood-text-muted);
background: none;
cursor: not-allowed;
opacity: 0.5;
}
/* Transition */
.dropdown-enter-active, .dropdown-leave-active {
transition: all 0.15s ease;
transform-origin: top left;
}
.dropdown-enter-from, .dropdown-leave-to {
opacity: 0;
transform: scale(0.95) translateY(-4px);
}
</style>