Files
decision/frontend/app/pages/protocols/index.vue
Yvv 8201e73d7c Accents FR, architecture modulaire, protocoles opérationnels
- Fix accents manquants dans 7 pages UI (décisions, boîte à outils, etc.)
- Titres accueil enrichis : Décisions structurantes, Documents de référence,
  Mandats et nominations, Protocoles et fonctionnement
- Retrait Embarquement Forgeron du seed (n'est pas une Decision)
- 2 protocoles opérationnels dans Protocoles : Embarquement Forgeron
  (lié à l'Acte d'engagement) + Soumission Runtime Upgrade (lié à la
  Décision Runtime Upgrade) avec timeline et liens croisés signalétiques
- Décision Runtime Upgrade : badge on-chain + lien protocole + contexte
- Document [slug] : lien protocole dans la section Qualification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:05:55 +01:00

1220 lines
29 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: '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>
<!-- Operational protocols (workflow templates) -->
<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>
<!-- 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>
<!-- Toolbox sidebar -->
<template #toolbox>
<!-- Simulateur -->
<ToolboxVignette
title="Simulateur de formules"
:bullets="['Testez WoT, Smith, TechComm', 'Ajustez les paramètres en temps réel', 'Visualisez les seuils']"
:actions="[
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
{ label: 'Ouvrir', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
]"
/>
<!-- n8n Workflows -->
<div class="n8n-section">
<div class="n8n-section__head">
<UIcon name="i-lucide-workflow" class="text-xs" />
<span>Workflows n8n</span>
</div>
<p class="n8n-section__desc">
Automatisations reliées via MCP
</p>
<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>
</div>
<!-- Meta-gouvernance -->
<ToolboxVignette
title="Méta-gouvernance"
:bullets="['Les formules sont soumises au vote', 'Modifier les seuils collectivement', 'Transparence totale']"
:actions="[
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
{ label: 'Formules', icon: 'i-lucide-calculator', emit: 'formules' },
{ 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 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;
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>