Restructure Engagement Forgeron + fix GenesisBlock + InertiaSlider
- Seed: restructure Engagement Forgeron (51→59 items) avec 3 nouvelles sections: Engagements fondamentaux (EF1-EF3), Engagements techniques (ET1-ET3), Qualification (Q0-Q1) liée au protocole Embarquement - Seed: ajout protocole Embarquement Forgeron (5 jalons: candidature, miroir, évaluation, certification Smith, mise en ligne) - GenesisBlock: fix lisibilité — fond mood-surface teinté accent au lieu de mood-text inversé, texte mood-aware au lieu de rgba blanc hardcodé - InertiaSlider: mini affiche "Inertie" sous le curseur, compact en width:fit-content pour s'adapter au label - Frontend: ajout section qualification dans SECTION_META/SECTION_ORDER - Pages, composants et tests des sprints précédents Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -194,23 +194,27 @@ function formatDate(dateStr: string): string {
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<div class="toolbox-section-title">
|
||||
Modalites de vote
|
||||
</div>
|
||||
<template v-if="protocols.protocols.length > 0">
|
||||
<ToolboxVignette
|
||||
v-for="protocol in protocols.protocols"
|
||||
:key="protocol.id"
|
||||
:title="protocol.name"
|
||||
:bullets="['Applicable aux decisions', protocol.mode_params || 'Configuration standard']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-eye', to: `/protocols/${protocol.id}` },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<p v-else class="toolbox-empty-text">
|
||||
Aucun protocole configure
|
||||
</p>
|
||||
<ToolboxVignette
|
||||
title="Vote majoritaire WoT"
|
||||
:bullets="['Seuil adaptatif a la participation', 'Formule g1vote inertielle']"
|
||||
:actions="[
|
||||
{ label: 'Simuler', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Vote nuance"
|
||||
:bullets="['6 niveaux de preference', 'Seuil de satisfaction 80%']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-bar-chart-3', emit: 'nuance' },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Mandature"
|
||||
:bullets="['Election en binome', 'Transparence et revocation']"
|
||||
:actions="[
|
||||
{ label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', primary: true },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</SectionLayout>
|
||||
</template>
|
||||
|
||||
@@ -45,6 +45,9 @@ 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' },
|
||||
qualification: { label: 'Qualification', icon: 'i-lucide-graduation-cap' },
|
||||
aspirant: { label: 'Aspirant forgeron', icon: 'i-lucide-user-plus' },
|
||||
certificateur: { label: 'Certificateur forgeron', icon: 'i-lucide-stamp' },
|
||||
conclusion: { label: 'Conclusion', icon: 'i-lucide-bookmark' },
|
||||
annexe: { label: 'Annexes', icon: 'i-lucide-paperclip' },
|
||||
formule: { label: 'Formule de vote', icon: 'i-lucide-calculator' },
|
||||
@@ -52,7 +55,7 @@ const SECTION_META: Record<string, { label: string; icon: string }> = {
|
||||
ordonnancement: { label: 'Ordonnancement', icon: 'i-lucide-list-ordered' },
|
||||
}
|
||||
|
||||
const SECTION_ORDER = ['introduction', 'fondamental', 'technique', 'conclusion', 'annexe', 'formule', 'inertie', 'ordonnancement']
|
||||
const SECTION_ORDER = ['introduction', 'fondamental', 'technique', 'qualification', 'aspirant', 'certificateur', 'conclusion', 'annexe', 'formule', 'inertie', 'ordonnancement']
|
||||
|
||||
const sections = computed((): Section[] => {
|
||||
const grouped: Record<string, DocumentItem[]> = {}
|
||||
@@ -140,11 +143,36 @@ async function archiveToSanctuary() {
|
||||
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
|
||||
// Expand the section if collapsed
|
||||
if (collapsedSections.value[tag]) {
|
||||
collapsedSections.value[tag] = false
|
||||
}
|
||||
nextTick(() => {
|
||||
const el = document.getElementById(`section-${tag}`)
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
activeSection.value = tag
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Collapsible sections ────────────────────────────────────
|
||||
// First 2 sections open by default, rest collapsed
|
||||
|
||||
const collapsedSections = ref<Record<string, boolean>>({})
|
||||
|
||||
watch(sections, (newSections) => {
|
||||
if (newSections.length > 0 && Object.keys(collapsedSections.value).length === 0) {
|
||||
const map: Record<string, boolean> = {}
|
||||
newSections.forEach((s, i) => {
|
||||
map[s.tag] = i >= 2 // collapsed if index >= 2
|
||||
})
|
||||
collapsedSections.value = map
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function toggleSection(tag: string) {
|
||||
collapsedSections.value[tag] = !collapsedSections.value[tag]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -280,8 +308,11 @@ function scrollToSection(tag: string) {
|
||||
:id="`section-${section.tag}`"
|
||||
class="doc-page__section"
|
||||
>
|
||||
<!-- Section header -->
|
||||
<div class="doc-page__section-header">
|
||||
<!-- Section header (clickable toggle) -->
|
||||
<button
|
||||
class="doc-page__section-header"
|
||||
@click="toggleSection(section.tag)"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon :name="section.icon" style="color: var(--mood-accent)" />
|
||||
<h2 class="doc-page__section-title">
|
||||
@@ -291,20 +322,29 @@ function scrollToSection(tag: string) {
|
||||
{{ section.items.length }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<InertiaSlider :preset="section.inertiaPreset" compact class="max-w-48" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<InertiaSlider :preset="section.inertiaPreset" compact mini />
|
||||
<UIcon
|
||||
name="i-lucide-chevron-down"
|
||||
class="doc-page__section-chevron"
|
||||
:class="{ 'doc-page__section-chevron--open': !collapsedSections[section.tag] }"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Items (collapsible) -->
|
||||
<Transition name="section-collapse">
|
||||
<div v-show="!collapsedSections[section.tag]" 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>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -449,6 +489,27 @@ function scrollToSection(tag: string) {
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 2px solid color-mix(in srgb, var(--mood-accent) 15%, transparent);
|
||||
width: 100%;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.doc-page__section-header:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.doc-page__section-chevron {
|
||||
font-size: 1rem;
|
||||
color: var(--mood-text-muted);
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.25s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.doc-page__section-chevron--open {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.doc-page__section-title {
|
||||
@@ -469,4 +530,22 @@ function scrollToSection(tag: string) {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Section collapse transition */
|
||||
.section-collapse-enter-active,
|
||||
.section-collapse-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-collapse-enter-from,
|
||||
.section-collapse-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.section-collapse-enter-to,
|
||||
.section-collapse-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,7 +87,6 @@ const filteredDocuments = computed(() => {
|
||||
})
|
||||
|
||||
/** Toolbox vignettes from protocols. */
|
||||
const toolboxTitle = 'Modalites de vote'
|
||||
|
||||
const typeLabel = (docType: string): string => {
|
||||
switch (docType) {
|
||||
@@ -252,23 +251,27 @@ async function createDocument() {
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<div class="toolbox-section-title">
|
||||
{{ toolboxTitle }}
|
||||
</div>
|
||||
<template v-if="protocols.protocols.length > 0">
|
||||
<ToolboxVignette
|
||||
v-for="protocol in protocols.protocols"
|
||||
:key="protocol.id"
|
||||
:title="protocol.name"
|
||||
:bullets="['Applicable aux documents', protocol.mode_params || 'Configuration standard']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-eye', to: `/protocols/${protocol.id}` },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<p v-else class="toolbox-empty-text">
|
||||
Aucun protocole configure
|
||||
</p>
|
||||
<ToolboxVignette
|
||||
title="Modules"
|
||||
:bullets="['Structurer en sections et clauses', 'Vote independant par clause']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-puzzle', emit: 'modules' },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Votes permanents"
|
||||
:bullets="['Chaque clause est modifiable', 'Seuil adaptatif WoT']"
|
||||
:actions="[
|
||||
{ label: 'Formules', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Inertie de remplacement"
|
||||
:bullets="['4 niveaux de difficulte', 'Protege les textes fondamentaux']"
|
||||
:actions="[
|
||||
{ label: 'Simuler', icon: 'i-lucide-sliders-horizontal', to: '/protocols/formulas', primary: true },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</SectionLayout>
|
||||
|
||||
|
||||
@@ -21,17 +21,6 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const entryCards = computed(() => [
|
||||
{
|
||||
key: 'documents',
|
||||
title: 'Documents',
|
||||
icon: 'i-lucide-book-open',
|
||||
to: '/documents',
|
||||
count: documents.activeDocuments.length,
|
||||
countLabel: `${documents.activeDocuments.length} actif${documents.activeDocuments.length > 1 ? 's' : ''}`,
|
||||
totalLabel: `${documents.list.length} au total`,
|
||||
description: 'Textes fondateurs sous vote permanent',
|
||||
color: 'var(--mood-accent)',
|
||||
},
|
||||
{
|
||||
key: 'decisions',
|
||||
title: 'Decisions',
|
||||
@@ -44,15 +33,15 @@ const entryCards = computed(() => [
|
||||
color: 'var(--mood-secondary, var(--mood-accent))',
|
||||
},
|
||||
{
|
||||
key: 'protocoles',
|
||||
title: 'Protocoles',
|
||||
icon: 'i-lucide-settings',
|
||||
to: '/protocols',
|
||||
count: protocols.protocols.length,
|
||||
countLabel: `${protocols.protocols.length} modalite${protocols.protocols.length > 1 ? 's' : ''}`,
|
||||
totalLabel: 'Boite a outils de vote + workflows',
|
||||
description: 'Modalites de vote, formules, workflows n8n',
|
||||
color: 'var(--mood-tertiary, var(--mood-accent))',
|
||||
key: 'documents',
|
||||
title: 'Documents',
|
||||
icon: 'i-lucide-book-open',
|
||||
to: '/documents',
|
||||
count: documents.activeDocuments.length,
|
||||
countLabel: `${documents.activeDocuments.length} actif${documents.activeDocuments.length > 1 ? 's' : ''}`,
|
||||
totalLabel: `${documents.list.length} au total`,
|
||||
description: 'Textes fondateurs sous vote permanent',
|
||||
color: 'var(--mood-accent)',
|
||||
},
|
||||
{
|
||||
key: 'mandats',
|
||||
@@ -65,6 +54,17 @@ const entryCards = computed(() => [
|
||||
description: 'Missions deleguees avec nomination en binome',
|
||||
color: 'var(--mood-success)',
|
||||
},
|
||||
{
|
||||
key: 'protocoles',
|
||||
title: 'Protocoles',
|
||||
icon: 'i-lucide-settings',
|
||||
to: '/protocols',
|
||||
count: protocols.protocols.length,
|
||||
countLabel: `${protocols.protocols.length} modalite${protocols.protocols.length > 1 ? 's' : ''}`,
|
||||
totalLabel: 'Boite a outils de vote + workflows',
|
||||
description: 'Modalites de vote, formules, workflows',
|
||||
color: 'var(--mood-tertiary, var(--mood-accent))',
|
||||
},
|
||||
])
|
||||
|
||||
const recentDecisions = computed(() => {
|
||||
@@ -151,35 +151,27 @@ function formatDate(dateStr: string): string {
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Toolbox teaser -->
|
||||
<div class="dash__toolbox">
|
||||
<div class="dash__toolbox-head">
|
||||
<UIcon name="i-lucide-wrench" class="text-lg" />
|
||||
<h3>Boite a outils</h3>
|
||||
<span class="dash__toolbox-count">{{ protocols.protocols.length }}</span>
|
||||
<!-- Toolbox teaser (5th block, distinct look) -->
|
||||
<NuxtLink to="/tools" class="dash__toolbox-card">
|
||||
<div class="dash__toolbox-card-inner">
|
||||
<div class="dash__toolbox-card-icon">
|
||||
<UIcon name="i-lucide-wrench" class="text-xl" />
|
||||
</div>
|
||||
<div class="dash__toolbox-card-body">
|
||||
<h3 class="dash__toolbox-card-title">Boite a outils</h3>
|
||||
<p class="dash__toolbox-card-desc">
|
||||
Simulateur de formules, modules de vote, workflows
|
||||
</p>
|
||||
<div class="dash__toolbox-card-tags">
|
||||
<span class="dash__toolbox-card-tag">Vote WoT</span>
|
||||
<span class="dash__toolbox-card-tag">Inertie</span>
|
||||
<span class="dash__toolbox-card-tag">Smith</span>
|
||||
<span class="dash__toolbox-card-tag">Nuance</span>
|
||||
</div>
|
||||
</div>
|
||||
<UIcon name="i-lucide-arrow-right" class="dash__toolbox-card-arrow" />
|
||||
</div>
|
||||
<div class="dash__toolbox-tags">
|
||||
<template v-if="protocols.protocols.length > 0">
|
||||
<NuxtLink
|
||||
v-for="protocol in protocols.protocols"
|
||||
:key="protocol.id"
|
||||
:to="`/protocols/${protocol.id}`"
|
||||
class="dash__tag"
|
||||
>
|
||||
{{ protocol.name }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="dash__tag">Vote WoT</span>
|
||||
<span class="dash__tag">Vote nuance</span>
|
||||
<span class="dash__tag">Vote permanent</span>
|
||||
</template>
|
||||
</div>
|
||||
<NuxtLink to="/protocols" class="dash__toolbox-link">
|
||||
Voir la boite a outils
|
||||
<UIcon name="i-lucide-chevron-right" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Recent activity -->
|
||||
<div v-if="recentDecisions.length > 0" class="dash__activity">
|
||||
@@ -292,7 +284,7 @@ function formatDate(dateStr: string): string {
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dash__entries {
|
||||
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -460,73 +452,91 @@ function formatDate(dateStr: string): string {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* --- Toolbox teaser --- */
|
||||
.dash__toolbox {
|
||||
background: var(--mood-surface);
|
||||
/* --- Toolbox card (5th block, distinct) --- */
|
||||
.dash__toolbox-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 16px;
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border-left: 4px solid var(--mood-accent);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dash__toolbox {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.dash__toolbox-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px var(--mood-shadow);
|
||||
}
|
||||
|
||||
.dash__toolbox-head {
|
||||
.dash__toolbox-card-inner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dash__toolbox-card-icon {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--mood-accent);
|
||||
font-weight: 800;
|
||||
font-size: 1.0625rem;
|
||||
}
|
||||
.dash__toolbox-head h3 { margin: 0; }
|
||||
|
||||
.dash__toolbox-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
background: var(--mood-accent);
|
||||
color: var(--mood-accent-text);
|
||||
}
|
||||
|
||||
.dash__toolbox-tags {
|
||||
.dash__toolbox-card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.dash__toolbox-card-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dash__toolbox-card-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dash__toolbox-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.dash__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-accent);
|
||||
background: var(--mood-accent-soft);
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
.dash__tag:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dash__toolbox-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dash__toolbox-card-tag {
|
||||
display: inline-flex;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-accent);
|
||||
text-decoration: none;
|
||||
background: var(--mood-surface);
|
||||
border-radius: 20px;
|
||||
}
|
||||
.dash__toolbox-link:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
.dash__toolbox-card-arrow {
|
||||
flex-shrink: 0;
|
||||
color: var(--mood-text-muted);
|
||||
opacity: 0.3;
|
||||
margin-top: 0.375rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.dash__toolbox-card:hover .dash__toolbox-card-arrow {
|
||||
opacity: 1;
|
||||
color: var(--mood-accent);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* --- Activity --- */
|
||||
|
||||
@@ -272,23 +272,34 @@ async function handleCreate() {
|
||||
|
||||
<!-- Toolbox sidebar -->
|
||||
<template #toolbox>
|
||||
<div class="toolbox-section-title">
|
||||
Modalites de vote
|
||||
</div>
|
||||
<template v-if="protocols.protocols.length > 0">
|
||||
<ToolboxVignette
|
||||
v-for="protocol in protocols.protocols"
|
||||
:key="protocol.id"
|
||||
:title="protocol.name"
|
||||
:bullets="['Applicable aux mandats', protocol.mode_params || 'Configuration standard']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-eye', to: `/protocols/${protocol.id}` },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<p v-else class="toolbox-empty-text">
|
||||
Aucun protocole configure
|
||||
</p>
|
||||
<ToolboxVignette
|
||||
title="Ouverture"
|
||||
:bullets="['Definir mission et perimetre', 'Duree et objectifs clairs']"
|
||||
:actions="[
|
||||
{ label: 'Creer', icon: 'i-lucide-door-open', emit: 'create', primary: true },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Nomination"
|
||||
:bullets="['Election en binome', 'Titulaire + suppleant']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-users', emit: 'nomination' },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Transparence"
|
||||
:bullets="['Rapports d\'activite', 'Soumis au vote communautaire']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-eye', emit: 'transparence' },
|
||||
]"
|
||||
/>
|
||||
<ToolboxVignette
|
||||
title="Cloture"
|
||||
:bullets="['Fin de mandat ou revocation', 'Bilan et transmission']"
|
||||
:actions="[
|
||||
{ label: 'Voir', icon: 'i-lucide-lock', emit: 'cloture' },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</SectionLayout>
|
||||
|
||||
|
||||
@@ -109,6 +109,35 @@ async function createProtocol() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Operational protocols (workflow templates). */
|
||||
interface WorkflowStep {
|
||||
label: string
|
||||
actor: string
|
||||
icon: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const operationalProtocols = [
|
||||
{
|
||||
slug: 'embarquement-forgeron',
|
||||
name: 'Embarquement Forgeron',
|
||||
description: 'Processus complet d\'intégration d\'un nouveau forgeron dans le réseau Duniter.',
|
||||
category: 'onboarding',
|
||||
icon: 'i-lucide-hammer',
|
||||
instancesLabel: '~10-50 / an',
|
||||
steps: [
|
||||
{ label: 'Invitation on-chain', actor: 'Smith existant', icon: 'i-lucide-send', type: 'on_chain' },
|
||||
{ label: 'Acceptation', actor: 'Candidat', icon: 'i-lucide-check', type: 'on_chain' },
|
||||
{ label: 'Session keys', actor: 'Candidat', icon: 'i-lucide-key', type: 'on_chain' },
|
||||
{ label: 'Checklist aspirant', actor: 'Candidat', icon: 'i-lucide-clipboard-check', type: 'checklist' },
|
||||
{ label: 'Certification 1', actor: 'Certificateur', icon: 'i-lucide-stamp', type: 'certification' },
|
||||
{ label: 'Certification 2', actor: 'Certificateur', icon: 'i-lucide-stamp', type: 'certification' },
|
||||
{ label: 'Certification 3', actor: 'Certificateur', icon: 'i-lucide-stamp', type: 'certification' },
|
||||
{ label: 'Go online', actor: 'Candidat', icon: 'i-lucide-wifi', type: 'on_chain' },
|
||||
] as WorkflowStep[],
|
||||
},
|
||||
]
|
||||
|
||||
/** n8n workflow demo items. */
|
||||
const n8nWorkflows = [
|
||||
{
|
||||
@@ -229,6 +258,50 @@ const n8nWorkflows = [
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Operational protocols (workflow templates) -->
|
||||
<div class="proto-ops">
|
||||
<h3 class="proto-ops__title">
|
||||
<UIcon name="i-lucide-git-branch" class="text-sm" />
|
||||
Protocoles operationnels
|
||||
<span class="proto-ops__count">{{ operationalProtocols.length }}</span>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
v-for="op in operationalProtocols"
|
||||
:key="op.slug"
|
||||
class="proto-ops__card"
|
||||
>
|
||||
<div class="proto-ops__card-head">
|
||||
<div class="proto-ops__card-icon">
|
||||
<UIcon :name="op.icon" class="text-lg" />
|
||||
</div>
|
||||
<div class="proto-ops__card-info">
|
||||
<h4 class="proto-ops__card-name">{{ op.name }}</h4>
|
||||
<p class="proto-ops__card-desc">{{ op.description }}</p>
|
||||
<span class="proto-ops__card-meta">{{ op.instancesLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step timeline -->
|
||||
<div class="proto-ops__timeline">
|
||||
<div
|
||||
v-for="(step, idx) in op.steps"
|
||||
:key="idx"
|
||||
class="proto-ops__step"
|
||||
>
|
||||
<div class="proto-ops__step-dot" :class="`proto-ops__step-dot--${step.type}`">
|
||||
<UIcon :name="step.icon" class="text-xs" />
|
||||
</div>
|
||||
<div class="proto-ops__step-body">
|
||||
<span class="proto-ops__step-label">{{ step.label }}</span>
|
||||
<span class="proto-ops__step-actor">{{ step.actor }}</span>
|
||||
</div>
|
||||
<div v-if="idx < op.steps.length - 1" class="proto-ops__step-line" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulas table -->
|
||||
<div class="proto-formulas">
|
||||
<h3 class="proto-formulas__title">
|
||||
@@ -802,6 +875,152 @@ const n8nWorkflows = [
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- Operational protocols --- */
|
||||
.proto-ops {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.proto-ops__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.proto-ops__count {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.proto-ops__card {
|
||||
background: var(--mood-surface);
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.proto-ops__card-head {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.proto-ops__card-icon {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
|
||||
.proto-ops__card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.proto-ops__card-name {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.proto-ops__card-desc {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.4;
|
||||
margin: 0.125rem 0 0;
|
||||
}
|
||||
|
||||
.proto-ops__card-meta {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-accent);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
.proto-ops__timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.proto-ops__step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
position: relative;
|
||||
padding: 0.375rem 0;
|
||||
}
|
||||
|
||||
.proto-ops__step-dot {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-accent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.proto-ops__step-dot--on_chain {
|
||||
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
|
||||
color: var(--mood-success);
|
||||
}
|
||||
|
||||
.proto-ops__step-dot--checklist {
|
||||
background: color-mix(in srgb, var(--mood-warning) 15%, transparent);
|
||||
color: var(--mood-warning);
|
||||
}
|
||||
|
||||
.proto-ops__step-dot--certification {
|
||||
background: color-mix(in srgb, var(--mood-secondary) 15%, transparent);
|
||||
color: var(--mood-secondary, var(--mood-accent));
|
||||
}
|
||||
|
||||
.proto-ops__step-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.proto-ops__step-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.proto-ops__step-actor {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.proto-ops__step-line {
|
||||
position: absolute;
|
||||
left: calc(0.875rem - 1px);
|
||||
top: calc(0.375rem + 1.75rem);
|
||||
width: 2px;
|
||||
height: calc(100% - 1.75rem + 0.375rem);
|
||||
background: color-mix(in srgb, var(--mood-accent) 15%, transparent);
|
||||
}
|
||||
|
||||
/* --- Modal --- */
|
||||
.proto-modal {
|
||||
padding: 1.25rem;
|
||||
|
||||
332
frontend/app/pages/tools.vue
Normal file
332
frontend/app/pages/tools.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Tools page — lists tools grouped by main section.
|
||||
* Each section shows relevant tools for Documents, Decisions, Mandates, Protocols.
|
||||
*/
|
||||
|
||||
interface Tool {
|
||||
label: string
|
||||
icon: string
|
||||
description: string
|
||||
to?: string
|
||||
status: 'ready' | 'soon'
|
||||
}
|
||||
|
||||
interface ToolSection {
|
||||
key: string
|
||||
title: string
|
||||
icon: string
|
||||
color: string
|
||||
tools: Tool[]
|
||||
}
|
||||
|
||||
const sections: ToolSection[] = [
|
||||
{
|
||||
key: 'documents',
|
||||
title: 'Documents',
|
||||
icon: 'i-lucide-book-open',
|
||||
color: 'var(--mood-accent)',
|
||||
tools: [
|
||||
{ label: 'Modules', icon: 'i-lucide-puzzle', description: 'Structurer un document en sections et clauses modulaires', to: '/documents', status: 'ready' },
|
||||
{ label: 'Votes permanents', icon: 'i-lucide-infinity', description: 'Chaque clause est sous vote permanent, modifiable a tout moment', status: 'ready' },
|
||||
{ label: 'Inertie de remplacement', icon: 'i-lucide-sliders-horizontal', description: 'Regler la difficulte de modification par section (standard, haute, tres haute)', to: '/protocols/formulas', status: 'ready' },
|
||||
{ label: 'Contre-propositions', icon: 'i-lucide-pen-line', description: 'Soumettre un texte alternatif soumis au vote de la communaute', status: 'ready' },
|
||||
{ label: 'Ancrage IPFS', icon: 'i-lucide-hard-drive', description: 'Archiver les documents valides sur IPFS avec preuve on-chain', status: 'soon' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'decisions',
|
||||
title: 'Decisions',
|
||||
icon: 'i-lucide-scale',
|
||||
color: 'var(--mood-secondary, var(--mood-accent))',
|
||||
tools: [
|
||||
{ label: 'Vote majoritaire WoT', icon: 'i-lucide-check-circle', description: 'Seuil adaptatif par la toile de confiance, formule g1vote', to: '/protocols/formulas', status: 'ready' },
|
||||
{ label: 'Vote quadratique', icon: 'i-lucide-square-stack', description: 'Ponderation degresssive pour eviter la concentration de pouvoir', status: 'soon' },
|
||||
{ label: 'Vote nuance 6 niveaux', icon: 'i-lucide-bar-chart-3', description: 'De Tout a fait contre a Tout a fait pour, avec seuil de satisfaction', status: 'ready' },
|
||||
{ label: 'Mandature', icon: 'i-lucide-user-check', description: 'Election et nomination en binome avec transparence', status: 'ready' },
|
||||
{ label: 'Multi-criteres', icon: 'i-lucide-layers', description: 'Combinaison WoT + Smith + TechComm, tous doivent passer', to: '/protocols/formulas', status: 'ready' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'mandats',
|
||||
title: 'Mandats',
|
||||
icon: 'i-lucide-user-check',
|
||||
color: 'var(--mood-success)',
|
||||
tools: [
|
||||
{ label: 'Ouverture', icon: 'i-lucide-door-open', description: 'Definir une mission, son perimetre, sa duree et ses objectifs', status: 'ready' },
|
||||
{ label: 'Nomination', icon: 'i-lucide-users', description: 'Election en binome : un titulaire + un suppleant', status: 'ready' },
|
||||
{ label: 'Transparence', icon: 'i-lucide-eye', description: 'Rapports d\'activite periodiques soumis au vote', status: 'ready' },
|
||||
{ label: 'Cloture', icon: 'i-lucide-lock', description: 'Fin de mandat avec bilan ou revocation anticipee par vote', status: 'ready' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'protocoles',
|
||||
title: 'Protocoles',
|
||||
icon: 'i-lucide-settings',
|
||||
color: 'var(--mood-tertiary, var(--mood-accent))',
|
||||
tools: [
|
||||
{ label: 'Simulateur de formules', icon: 'i-lucide-calculator', description: 'Tester les parametres de seuil WoT en temps reel', to: '/protocols/formulas', status: 'ready' },
|
||||
{ label: 'Meta-gouvernance', icon: 'i-lucide-shield', description: 'Les formules elles-memes sont soumises au vote', status: 'ready' },
|
||||
{ label: 'Workflows n8n', icon: 'i-lucide-workflow', description: 'Automatisations optionnelles (notifications, alertes, relances)', status: 'soon' },
|
||||
{ label: 'Protocoles operationnels', icon: 'i-lucide-git-branch', description: 'Processus multi-etapes reutilisables (embarquement, upgrade)', to: '/protocols', status: 'ready' },
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools-page">
|
||||
<!-- Back link -->
|
||||
<div class="tools-page__nav">
|
||||
<UButton
|
||||
to="/"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
icon="i-lucide-arrow-left"
|
||||
label="Retour a l'accueil"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="tools-page__header">
|
||||
<h1 class="tools-page__title">
|
||||
<UIcon name="i-lucide-wrench" class="tools-page__title-icon" />
|
||||
Boite a outils
|
||||
</h1>
|
||||
<p class="tools-page__subtitle">
|
||||
Tous les outils de decision collective, organises par section
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tool sections -->
|
||||
<div class="tools-page__sections">
|
||||
<div
|
||||
v-for="section in sections"
|
||||
:key="section.key"
|
||||
class="tools-section"
|
||||
:style="{ '--section-color': section.color }"
|
||||
>
|
||||
<div class="tools-section__header">
|
||||
<UIcon :name="section.icon" class="tools-section__icon" />
|
||||
<h2 class="tools-section__title">{{ section.title }}</h2>
|
||||
<span class="tools-section__count">{{ section.tools.length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tools-section__grid">
|
||||
<NuxtLink
|
||||
v-for="tool in section.tools.filter(t => t.to)"
|
||||
:key="tool.label"
|
||||
:to="tool.to!"
|
||||
class="tool-card"
|
||||
>
|
||||
<div class="tool-card__icon">
|
||||
<UIcon :name="tool.icon" />
|
||||
</div>
|
||||
<div class="tool-card__body">
|
||||
<div class="tool-card__head">
|
||||
<span class="tool-card__label">{{ tool.label }}</span>
|
||||
</div>
|
||||
<p class="tool-card__desc">{{ tool.description }}</p>
|
||||
</div>
|
||||
<UIcon name="i-lucide-chevron-right" class="tool-card__arrow" />
|
||||
</NuxtLink>
|
||||
<div
|
||||
v-for="tool in section.tools.filter(t => !t.to)"
|
||||
:key="tool.label"
|
||||
class="tool-card"
|
||||
:class="{ 'tool-card--soon': tool.status === 'soon' }"
|
||||
>
|
||||
<div class="tool-card__icon">
|
||||
<UIcon :name="tool.icon" />
|
||||
</div>
|
||||
<div class="tool-card__body">
|
||||
<div class="tool-card__head">
|
||||
<span class="tool-card__label">{{ tool.label }}</span>
|
||||
<span v-if="tool.status === 'soon'" class="tool-card__badge">bientot</span>
|
||||
</div>
|
||||
<p class="tool-card__desc">{{ tool.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tools-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
max-width: 56rem;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.tools-page__nav {
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
.tools-page__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tools-page__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.tools-page__title {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-page__title-icon {
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
|
||||
.tools-page__subtitle {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--mood-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.tools-page__sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.tools-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tools-section__icon {
|
||||
font-size: 1.125rem;
|
||||
color: var(--section-color);
|
||||
}
|
||||
|
||||
.tools-section__title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 800;
|
||||
color: var(--mood-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tools-section__count {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
background: color-mix(in srgb, var(--section-color) 12%, transparent);
|
||||
color: var(--section-color);
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
/* Tool cards */
|
||||
.tools-section__grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
background: var(--mood-surface);
|
||||
border-radius: 14px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px var(--mood-shadow);
|
||||
}
|
||||
|
||||
.tool-card--soon {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tool-card--soon:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tool-card__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--section-color) 12%, transparent);
|
||||
color: var(--section-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tool-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.tool-card__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
|
||||
.tool-card__badge {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 2px 6px;
|
||||
border-radius: 20px;
|
||||
background: var(--mood-accent-soft);
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
|
||||
.tool-card__desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
line-height: 1.4;
|
||||
margin: 0.125rem 0 0;
|
||||
}
|
||||
|
||||
.tool-card__arrow {
|
||||
flex-shrink: 0;
|
||||
color: var(--mood-text-muted);
|
||||
opacity: 0.3;
|
||||
margin-top: 0.375rem;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
|
||||
.tool-card:hover .tool-card__arrow {
|
||||
opacity: 1;
|
||||
color: var(--section-color);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user