Files
decision/frontend/app/pages/protocols/index.vue
Yvv ed9ed11cd4 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>
2026-03-17 00:48:20 +01:00

1207 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* Protocoles & Fonctionnement — Boîte à outils de vote.
*
* Liste les protocoles de vote avec SectionLayout,
* sidebar n8n workflow + simulateur de formules.
*/
const protocols = useProtocolsStore()
const auth = useAuthStore()
const activeStatus = ref<string | null>(null)
const searchQuery = ref('')
const showCreateModal = ref(false)
const creating = ref(false)
const newProtocol = reactive({
name: '',
description: '',
vote_type: 'binary',
formula_config_id: '',
})
onMounted(async () => {
await Promise.all([
protocols.fetchProtocols(),
protocols.fetchFormulas(),
])
})
const voteTypeLabel = (voteType: string) => {
switch (voteType) {
case 'binary': return 'Binaire'
case 'nuanced': return 'Nuancé'
default: return voteType
}
}
const voteTypeOptions = [
{ label: 'Binaire (Pour/Contre)', value: 'binary' },
{ label: 'Nuancé (6 niveaux)', value: 'nuanced' },
]
const formulaOptions = computed(() => {
return protocols.formulas.map(f => ({
label: f.name,
value: f.id,
}))
})
/** Status pills. */
const statuses = computed(() => [
{
id: 'binary',
label: 'Binaire',
count: protocols.protocols.filter(p => p.vote_type === 'binary').length,
cssClass: 'status-vote',
},
{
id: 'nuanced',
label: 'Nuancé',
count: protocols.protocols.filter(p => p.vote_type === 'nuanced').length,
cssClass: 'status-prepa',
},
])
/** Filtered protocols. */
const filteredProtocols = computed(() => {
let list = [...protocols.protocols]
if (activeStatus.value) {
list = list.filter(p => p.vote_type === activeStatus.value)
}
if (searchQuery.value.trim()) {
const q = searchQuery.value.toLowerCase()
list = list.filter(p =>
p.name.toLowerCase().includes(q)
|| (p.description && p.description.toLowerCase().includes(q)),
)
}
return list
})
function openCreateModal() {
newProtocol.name = ''
newProtocol.description = ''
newProtocol.vote_type = 'binary'
newProtocol.formula_config_id = ''
showCreateModal.value = true
}
async function createProtocol() {
if (!newProtocol.name.trim() || !newProtocol.formula_config_id) return
creating.value = true
try {
await protocols.createProtocol({
name: newProtocol.name.trim(),
description: newProtocol.description.trim() || null,
vote_type: newProtocol.vote_type,
formula_config_id: newProtocol.formula_config_id,
})
showCreateModal.value = false
await protocols.fetchProtocols()
}
finally {
creating.value = false
}
}
/** Operational protocols (workflow templates). */
interface WorkflowStep {
label: string
actor: string
icon: string
type: string
}
interface LinkedRef {
label: string
icon: string
to: string
kind: 'document' | 'decision'
}
interface OperationalProtocol {
slug: string
name: string
description: string
category: string
icon: string
instancesLabel: string
linkedRefs: LinkedRef[]
steps: WorkflowStep[]
}
const operationalProtocols: OperationalProtocol[] = [
{
slug: 'election-sociocratique',
name: 'Élection sociocratique',
description: 'Processus d\'élection d\'un rôle par consentement : clarification du rôle, nominations silencieuses, argumentaire, levée d\'objections. Garantit légitimité et clarté.',
category: 'gouvernance',
icon: 'i-lucide-users',
instancesLabel: 'Tout renouvellement de rôle',
linkedRefs: [
{ label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', kind: 'decision' },
],
steps: [
{ label: 'Clarifier le rôle', actor: 'Cercle', icon: 'i-lucide-clipboard-list', type: 'checklist' },
{ label: 'Nominations silencieuses', actor: 'Tous les membres', icon: 'i-lucide-pencil', type: 'checklist' },
{ label: 'Recueil & argumentaire', actor: 'Facilitateur', icon: 'i-lucide-list-checks', type: 'checklist' },
{ label: 'Objections & consentement', actor: 'Cercle', icon: 'i-lucide-shield-check', type: 'certification' },
{ label: 'Proclamation', actor: 'Facilitateur', icon: 'i-lucide-star', type: 'on_chain' },
],
},
{
slug: 'embarquement-forgeron',
name: 'Embarquement Forgeron',
description: 'Processus complet d\'intégration d\'un nouveau forgeron dans le réseau Duniter. Parcours en 5 jalons de la candidature à la mise en ligne du nœud validateur.',
category: 'onboarding',
icon: 'i-lucide-hammer',
instancesLabel: '~10-50 / an',
linkedRefs: [
{ label: 'Acte d\'engagement forgeron', icon: 'i-lucide-book-open', to: '/documents/engagement-forgeron', kind: 'document' },
],
steps: [
{ label: 'Candidature', actor: 'Aspirant forgeron', icon: 'i-lucide-user-plus', type: 'checklist' },
{ label: 'Nœud miroir', actor: 'Candidat', icon: 'i-lucide-server', type: 'on_chain' },
{ label: 'Évaluation technique', actor: 'Certificateur', icon: 'i-lucide-clipboard-check', type: 'checklist' },
{ label: 'Certification Smith (×3)', actor: 'Certificateurs', icon: 'i-lucide-stamp', type: 'certification' },
{ label: 'Go online', actor: 'Candidat', icon: 'i-lucide-wifi', type: 'on_chain' },
],
},
{
slug: 'soumission-runtime-upgrade',
name: 'Soumission Runtime Upgrade',
description: 'Protocole de soumission d\'une mise à jour du runtime Duniter V2 on-chain. Chaque upgrade suit un parcours strict en 5 étapes, de la qualification technique au suivi post-déploiement.',
category: 'on-chain',
icon: 'i-lucide-cpu',
instancesLabel: '~2-6 / an',
linkedRefs: [
{ label: 'Décision Runtime Upgrade', icon: 'i-lucide-scale', to: '/decisions', kind: 'decision' },
],
steps: [
{ label: 'Qualification', actor: 'Proposant', icon: 'i-lucide-file-check', type: 'checklist' },
{ label: 'Revue technique', actor: 'Comité technique', icon: 'i-lucide-search', type: 'checklist' },
{ label: 'Vote communautaire', actor: 'Communauté WoT', icon: 'i-lucide-vote', type: 'on_chain' },
{ label: 'Exécution on-chain', actor: 'Proposant', icon: 'i-lucide-zap', type: 'on_chain' },
{ label: 'Suivi post-upgrade', actor: 'Forgerons', icon: 'i-lucide-activity', type: 'checklist' },
],
},
]
/** n8n workflow demo items. */
const n8nWorkflows = [
{
name: 'Vote -> Notification',
description: 'Notifie les membres lorsqu\'un nouveau vote démarre ou se termine.',
icon: 'i-lucide-bell',
status: 'actif',
},
{
name: 'Seuil atteint -> Sanctuaire',
description: 'Archive automatiquement le document valide sur IPFS quand le seuil est atteint.',
icon: 'i-lucide-archive',
status: 'actif',
},
{
name: 'Décision → Étape suivante',
description: 'Avance automatiquement une décision à l\'étape suivante après validation.',
icon: 'i-lucide-git-branch',
status: 'demo',
},
{
name: 'Mandat expiré → Alerte',
description: 'Envoie une alerte 7 jours avant l\'expiration d\'un mandat.',
icon: 'i-lucide-alarm-clock',
status: 'demo',
},
]
</script>
<template>
<SectionLayout
title="Protocoles & Fonctionnement"
subtitle="Boîte à outils de vote, formules de seuil, workflows automatisés"
:statuses="statuses"
:active-status="activeStatus"
@update:active-status="activeStatus = $event"
>
<!-- Search slot -->
<template #search>
<input
v-model="searchQuery"
type="text"
class="proto-search"
placeholder="Rechercher un protocole..."
/>
<button
v-if="auth.isAuthenticated"
class="proto-add-btn"
@click="openCreateModal"
>
<UIcon name="i-lucide-plus" class="text-xs" />
<span>Nouveau</span>
</button>
</template>
<!-- Loading -->
<template v-if="protocols.loading">
<div class="proto-loading">
<LoadingSkeleton v-for="i in 3" :key="i" :lines="4" card />
</div>
</template>
<!-- Error -->
<template v-else-if="protocols.error">
<div class="proto-error">
<UIcon name="i-lucide-alert-circle" class="text-lg" />
<span>{{ protocols.error }}</span>
</div>
</template>
<!-- Protocol list -->
<template v-else>
<div v-if="filteredProtocols.length === 0" class="proto-empty">
<UIcon name="i-lucide-settings" class="text-2xl" />
<p>Aucun protocole trouvé</p>
</div>
<div v-else class="proto-list">
<NuxtLink
v-for="protocol in filteredProtocols"
:key="protocol.id"
:to="`/protocols/${protocol.id}`"
class="proto-item"
>
<div class="proto-item__left">
<div class="proto-item__icon">
<UIcon
:name="protocol.vote_type === 'nuanced' ? 'i-lucide-bar-chart-3' : 'i-lucide-check-circle'"
class="text-lg"
/>
</div>
</div>
<div class="proto-item__body">
<div class="proto-item__head">
<h3 class="proto-item__name">{{ protocol.name }}</h3>
<span class="proto-item__type" :class="protocol.vote_type === 'nuanced' ? 'proto-item__type--nuanced' : ''">
{{ voteTypeLabel(protocol.vote_type) }}
</span>
</div>
<p v-if="protocol.description" class="proto-item__desc">
{{ protocol.description }}
</p>
<div class="proto-item__meta">
<span v-if="protocol.mode_params" class="proto-item__params">
{{ protocol.mode_params }}
</span>
<span class="proto-item__formula">
{{ protocol.formula_config.name }}
</span>
<span class="proto-item__detail">
{{ protocol.formula_config.duration_days }}j · {{ protocol.formula_config.majority_pct }}%
</span>
</div>
</div>
<div class="proto-item__arrow">
<UIcon name="i-lucide-chevron-right" class="text-sm" />
</div>
</NuxtLink>
</div>
<!-- Formulas table -->
<div class="proto-formulas">
<h3 class="proto-formulas__title">
<UIcon name="i-lucide-calculator" class="text-sm" />
Configurations de formule
<span class="proto-formulas__count">{{ protocols.formulas.length }}</span>
</h3>
<div class="proto-formulas__table-wrap">
<table class="proto-formulas__table">
<thead>
<tr>
<th>Nom</th>
<th>Durée</th>
<th>Majorité</th>
<th>B</th>
<th>G</th>
<th>Smith</th>
<th>TechComm</th>
</tr>
</thead>
<tbody>
<tr v-for="formula in protocols.formulas" :key="formula.id">
<td class="proto-formulas__name">{{ formula.name }}</td>
<td>{{ formula.duration_days }}j</td>
<td>{{ formula.majority_pct }}%</td>
<td>{{ formula.base_exponent }}</td>
<td>{{ formula.gradient_exponent }}</td>
<td>{{ formula.smith_exponent ?? '-' }}</td>
<td>{{ formula.techcomm_exponent ?? '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- Operational protocols (always visible, frontend-only data) -->
<div class="proto-ops">
<h3 class="proto-ops__title">
<UIcon name="i-lucide-git-branch" class="text-sm" />
Protocoles opérationnels
<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>
<!-- Linked references -->
<div v-if="op.linkedRefs.length > 0" class="proto-ops__refs">
<NuxtLink
v-for="ref in op.linkedRefs"
:key="ref.to"
:to="ref.to"
class="proto-ops__ref"
:class="`proto-ops__ref--${ref.kind}`"
>
<UIcon :name="ref.icon" class="text-xs" />
<span>{{ ref.label }}</span>
<UIcon name="i-lucide-arrow-right" class="text-xs proto-ops__ref-arrow" />
</NuxtLink>
</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>
<!-- Toolbox sidebar -->
<template #toolbox>
<!-- Workflow milestones -->
<ToolboxSection title="Jalons de protocole" icon="i-lucide-git-branch">
<WorkflowMilestones />
</ToolboxSection>
<!-- Simulateur -->
<ToolboxVignette
title="Simulateur de formules"
:bullets="['WoT, Smith, TechComm', 'Paramètres en temps réel', 'Visualise les seuils']"
:actions="[
{ label: 'Ouvrir', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
]"
/>
<!-- n8n Workflows -->
<ToolboxSection title="Automatisations" icon="i-lucide-workflow">
<div class="n8n-workflows">
<div
v-for="wf in n8nWorkflows"
:key="wf.name"
class="n8n-wf"
>
<div class="n8n-wf__icon">
<UIcon :name="wf.icon" class="text-xs" />
</div>
<div class="n8n-wf__body">
<div class="n8n-wf__head">
<span class="n8n-wf__name">{{ wf.name }}</span>
<span
class="n8n-wf__status"
:class="wf.status === 'actif' ? 'n8n-wf__status--active' : 'n8n-wf__status--demo'"
>
{{ wf.status }}
</span>
</div>
<p class="n8n-wf__desc">{{ wf.description }}</p>
</div>
</div>
</div>
</ToolboxSection>
<!-- Meta-gouvernance -->
<ToolboxVignette
title="Méta-gouvernance"
:bullets="['Les formules sont soumises au vote', 'Seuils modifiables collectivement', 'Transparence totale']"
:actions="[
{ label: 'Démarrer', icon: 'i-lucide-play', emit: 'meta', primary: true },
]"
/>
</template>
</SectionLayout>
<!-- Create protocol modal -->
<UModal v-model:open="showCreateModal">
<template #content>
<div class="proto-modal">
<h3 class="proto-modal__title">Nouveau protocole de vote</h3>
<div class="proto-modal__fields">
<div class="proto-modal__field">
<label class="proto-modal__label">Nom du protocole</label>
<input
v-model="newProtocol.name"
type="text"
class="proto-modal__input"
placeholder="Ex: Vote standard G1"
/>
</div>
<div class="proto-modal__field">
<label class="proto-modal__label">Description</label>
<textarea
v-model="newProtocol.description"
class="proto-modal__textarea"
placeholder="Description du protocole..."
rows="2"
/>
</div>
<div class="proto-modal__field">
<label class="proto-modal__label">Type de vote</label>
<USelect
v-model="newProtocol.vote_type"
:items="voteTypeOptions"
value-key="value"
/>
</div>
<div class="proto-modal__field">
<label class="proto-modal__label">Configuration de formule</label>
<USelect
v-model="newProtocol.formula_config_id"
:items="formulaOptions"
placeholder="Sélectionnez une formule..."
value-key="value"
/>
</div>
</div>
<div class="proto-modal__actions">
<button class="proto-modal__cancel" @click="showCreateModal = false">
Annuler
</button>
<button
class="proto-modal__submit"
:disabled="!newProtocol.name.trim() || !newProtocol.formula_config_id || creating"
@click="createProtocol"
>
<UIcon v-if="creating" name="i-lucide-loader-2" class="animate-spin text-xs" />
<span>Créer</span>
</button>
</div>
</div>
</template>
</UModal>
</template>
<style scoped>
/* --- Search --- */
.proto-search {
flex: 1;
min-width: 10rem;
padding: 0.625rem 1rem;
font-size: 0.9375rem;
color: var(--mood-text);
background: var(--mood-accent-soft);
border-radius: 12px;
outline: none;
transition: box-shadow 0.15s ease;
}
.proto-search:focus {
box-shadow: 0 0 0 3px var(--mood-accent-soft);
}
.proto-search::placeholder {
color: var(--mood-text-muted);
opacity: 0.4;
}
.proto-add-btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 700;
color: var(--mood-accent-text);
background: var(--mood-accent);
border-radius: 20px;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.proto-add-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--mood-shadow);
}
/* --- States --- */
.proto-loading { display: flex; flex-direction: column; gap: 0.75rem; }
.proto-error {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 1.25rem;
font-size: 0.9375rem;
font-weight: 600;
color: var(--mood-error);
background: rgba(196, 43, 43, 0.08);
border-radius: 16px;
}
.proto-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 3rem;
text-align: center;
color: var(--mood-text-muted);
font-size: 1rem;
}
/* --- Protocol list --- */
.proto-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.proto-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
background: var(--mood-surface);
border-radius: 16px;
text-decoration: none;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
@media (min-width: 640px) {
.proto-item {
gap: 1rem;
padding: 1.25rem;
}
}
.proto-item:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px var(--mood-shadow);
}
.proto-item:active {
transform: translateY(0);
}
.proto-item__left { flex-shrink: 0; }
.proto-item__icon {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: var(--mood-accent-soft);
color: var(--mood-accent);
font-size: 1rem;
}
@media (min-width: 640px) {
.proto-item__icon {
width: 2.75rem;
height: 2.75rem;
font-size: 1.25rem;
}
}
.proto-item__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
@media (min-width: 640px) {
.proto-item__body {
gap: 0.375rem;
}
}
.proto-item__head {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.375rem;
}
@media (min-width: 640px) {
.proto-item__head {
gap: 0.625rem;
}
}
.proto-item__name {
font-size: 0.9375rem;
font-weight: 700;
color: var(--mood-text);
margin: 0;
}
@media (min-width: 640px) {
.proto-item__name {
font-size: 1.0625rem;
}
}
.proto-item__type {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 3px 10px;
border-radius: 20px;
background: var(--mood-status-vote-bg);
color: var(--mood-status-vote);
}
.proto-item__type--nuanced {
background: var(--mood-status-prepa-bg);
color: var(--mood-status-prepa);
}
.proto-item__desc {
font-size: 0.875rem;
color: var(--mood-text-muted);
line-height: 1.5;
margin: 0;
}
.proto-item__meta {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-top: 0.125rem;
font-size: 0.75rem;
color: var(--mood-text-muted);
}
@media (min-width: 640px) {
.proto-item__meta {
gap: 0.5rem;
margin-top: 0.25rem;
font-size: 0.8125rem;
}
}
.proto-item__params {
font-family: ui-monospace, SFMono-Regular, monospace;
font-weight: 700;
color: var(--mood-accent);
background: var(--mood-accent-soft);
padding: 2px 8px;
border-radius: 20px;
font-size: 0.75rem;
}
.proto-item__formula {
font-weight: 600;
}
.proto-item__detail {
opacity: 0.7;
}
.proto-item__arrow {
flex-shrink: 0;
color: var(--mood-text-muted);
opacity: 0.3;
margin-top: 0.375rem;
transition: opacity 0.12s;
}
.proto-item:hover .proto-item__arrow {
opacity: 1;
color: var(--mood-accent);
}
/* --- Formulas --- */
.proto-formulas {
margin-top: 0.75rem;
}
@media (min-width: 640px) {
.proto-formulas {
margin-top: 1rem;
}
}
.proto-formulas__title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9375rem;
font-weight: 700;
color: var(--mood-text);
margin: 0 0 0.5rem;
}
@media (min-width: 640px) {
.proto-formulas__title {
font-size: 1rem;
margin: 0 0 0.75rem;
}
}
.proto-formulas__count {
font-size: 0.6875rem;
font-weight: 700;
background: var(--mood-accent-soft);
color: var(--mood-accent);
padding: 2px 8px;
border-radius: 20px;
}
.proto-formulas__table-wrap {
background: var(--mood-surface);
border-radius: 16px;
overflow-x: auto;
}
.proto-formulas__table {
width: 100%;
font-size: 0.875rem;
border-collapse: collapse;
}
.proto-formulas__table th {
text-align: left;
padding: 0.5rem 0.625rem;
font-weight: 700;
font-size: 0.6875rem;
color: var(--mood-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
@media (min-width: 640px) {
.proto-formulas__table th {
padding: 0.75rem 1rem;
font-size: 0.75rem;
}
}
.proto-formulas__table td {
padding: 0.5rem 0.625rem;
color: var(--mood-text-muted);
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 0.75rem;
}
@media (min-width: 640px) {
.proto-formulas__table td {
padding: 0.75rem 1rem;
font-size: 0.8125rem;
}
}
.proto-formulas__name {
font-weight: 700;
color: var(--mood-text) !important;
font-family: inherit !important;
}
.n8n-workflows {
display: flex;
flex-direction: column;
gap: 0.375rem;
margin-top: 0.25rem;
}
.n8n-wf {
display: flex;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 10px;
transition: background 0.1s;
}
.n8n-wf:hover {
background: var(--mood-surface);
}
.n8n-wf__icon {
width: 1.75rem;
height: 1.75rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: var(--mood-surface);
color: var(--mood-tertiary, var(--mood-accent));
}
.n8n-wf__body {
min-width: 0;
flex: 1;
}
.n8n-wf__head {
display: flex;
align-items: center;
gap: 0.375rem;
}
.n8n-wf__name {
font-size: 0.75rem;
font-weight: 700;
color: var(--mood-text);
}
.n8n-wf__status {
font-size: 0.5625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px 6px;
border-radius: 20px;
}
.n8n-wf__status--active {
background: var(--mood-status-vigueur-bg);
color: var(--mood-success);
}
.n8n-wf__status--demo {
background: var(--mood-surface);
color: var(--mood-text-muted);
}
.n8n-wf__desc {
font-size: 0.6875rem;
color: var(--mood-text-muted);
line-height: 1.4;
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;
}
/* Linked references */
.proto-ops__refs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.proto-ops__ref {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 20px;
text-decoration: none;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.proto-ops__ref:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px var(--mood-shadow);
}
.proto-ops__ref--document {
background: color-mix(in srgb, var(--mood-accent) 12%, transparent);
color: var(--mood-accent);
}
.proto-ops__ref--decision {
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 12%, transparent);
color: var(--mood-secondary, var(--mood-accent));
}
.proto-ops__ref-arrow {
opacity: 0.4;
transition: opacity 0.12s;
}
.proto-ops__ref:hover .proto-ops__ref-arrow {
opacity: 1;
}
/* 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;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
@media (min-width: 640px) {
.proto-modal {
padding: 2rem;
gap: 1.5rem;
}
}
.proto-modal__title {
font-size: 1.125rem;
font-weight: 800;
color: var(--mood-text);
margin: 0;
}
@media (min-width: 640px) {
.proto-modal__title {
font-size: 1.375rem;
}
}
.proto-modal__fields {
display: flex;
flex-direction: column;
gap: 1rem;
}
.proto-modal__field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.proto-modal__label {
font-size: 0.8125rem;
font-weight: 700;
color: var(--mood-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.proto-modal__input,
.proto-modal__textarea {
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.9375rem;
color: var(--mood-text);
background: var(--mood-accent-soft);
border-radius: 12px;
outline: none;
transition: box-shadow 0.15s ease;
font-family: inherit;
resize: vertical;
}
.proto-modal__input:focus,
.proto-modal__textarea:focus {
box-shadow: 0 0 0 3px var(--mood-accent-soft);
}
.proto-modal__actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.proto-modal__cancel {
padding: 0.625rem 1.25rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--mood-text-muted);
background: none;
border-radius: 12px;
cursor: pointer;
}
.proto-modal__cancel:hover { color: var(--mood-text); background: var(--mood-accent-soft); }
.proto-modal__submit {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.625rem 1.5rem;
font-size: 0.9375rem;
font-weight: 700;
color: var(--mood-accent-text);
background: var(--mood-accent);
border-radius: 16px;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.proto-modal__submit:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--mood-shadow);
}
.proto-modal__submit:disabled { opacity: 0.4; cursor: not-allowed; }
</style>