Corrections moteur (TDD) : - R2 : within_mandate → record_in_observatory=True (Observatoire des décisions) - R6 : >50 personnes → collective recommandé, pas obligatoire (confidence=recommended) - R3 supprimée : affected_count=1 hors périmètre de l'outil - R9-R12 renommés G1-G4 (garde-fous internes) - 23 tests, 213/213 verts Étape 1 — Router /api/v1/qualify : - POST / → qualify() avec config depuis DB ou defaults - GET /protocol → protocole actif - POST /protocol → créer/remplacer (auth requise) Étape 2 — Modèle QualificationProtocol : - Table qualification_protocols (seuils configurables via admin) - Migration Alembic + seed du protocole par défaut Étape 3 — Wizard frontend decisions/new.vue : - Étape 1 : formulaire de qualification (mandat, affected_count, structurant, contexte) - Étape 2 : résultat (type, raisons, modalités, observatoire, on-chain) - Étape 3 : formulaire de décision (titre, description, protocole si collectif) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
867 lines
25 KiB
Vue
867 lines
25 KiB
Vue
<script setup lang="ts">
|
||
import type { DecisionCreate } from '~/stores/decisions'
|
||
|
||
const decisions = useDecisionsStore()
|
||
const protocols = useProtocolsStore()
|
||
const { $api } = useApi()
|
||
|
||
// ── Wizard state ─────────────────────────────────────────────────────────────
|
||
type WizardStep = 'qualify' | 'result' | 'form'
|
||
|
||
const wizardStep = ref<WizardStep>('qualify')
|
||
const qualifying = ref(false)
|
||
const submitting = ref(false)
|
||
|
||
// ── Qualification inputs ──────────────────────────────────────────────────────
|
||
const withinMandate = ref<boolean | null>(null)
|
||
const affectedCount = ref<number | null>(null)
|
||
const isStructural = ref(false)
|
||
const contextDescription = ref('')
|
||
|
||
// ── Qualification result (from /api/v1/qualify) ───────────────────────────────
|
||
interface QualifyResult {
|
||
decision_type: 'individual' | 'collective'
|
||
process: string
|
||
recommended_modalities: string[]
|
||
recommend_onchain: boolean
|
||
onchain_reason: string | null
|
||
confidence: string
|
||
collective_available: boolean
|
||
record_in_observatory: boolean
|
||
reasons: string[]
|
||
}
|
||
|
||
const qualifyResult = ref<QualifyResult | null>(null)
|
||
const qualifyError = ref<string | null>(null)
|
||
|
||
// ── Decision form ─────────────────────────────────────────────────────────────
|
||
const chosenType = ref<'individual' | 'collective' | null>(null)
|
||
const formData = ref<DecisionCreate>({
|
||
title: '',
|
||
description: '',
|
||
context: '',
|
||
decision_type: 'other',
|
||
voting_protocol_id: null,
|
||
})
|
||
|
||
// ── Modality labels ───────────────────────────────────────────────────────────
|
||
const MODALITY_LABELS: Record<string, { icon: string; title: string; desc: string }> = {
|
||
vote_wot: {
|
||
icon: 'i-lucide-users',
|
||
title: 'Vote WoT',
|
||
desc: 'Seuil adaptatif selon la taille de la Toile de Confiance et la participation',
|
||
},
|
||
vote_smith: {
|
||
icon: 'i-lucide-hammer',
|
||
title: 'Vote Smith',
|
||
desc: 'Implique le sous-ensemble des Forgerons (Smith criterion)',
|
||
},
|
||
consultation_avis: {
|
||
icon: 'i-lucide-message-circle',
|
||
title: 'Consultation d\'avis',
|
||
desc: 'Recueil d\'avis non contraignant — la décision reste individuelle',
|
||
},
|
||
election: {
|
||
icon: 'i-lucide-vote',
|
||
title: 'Élection',
|
||
desc: 'Désignation d\'une personne par le collectif',
|
||
},
|
||
}
|
||
|
||
function modalityLabel(slug: string) {
|
||
return MODALITY_LABELS[slug] ?? { icon: 'i-lucide-circle', title: slug, desc: '' }
|
||
}
|
||
|
||
// ── Step 1 : Qualify ──────────────────────────────────────────────────────────
|
||
const canQualify = computed(() => withinMandate.value !== null)
|
||
|
||
async function runQualify() {
|
||
if (!canQualify.value) return
|
||
qualifying.value = true
|
||
qualifyError.value = null
|
||
try {
|
||
const payload: Record<string, unknown> = {
|
||
within_mandate: withinMandate.value,
|
||
is_structural: isStructural.value,
|
||
}
|
||
if (withinMandate.value === false && affectedCount.value !== null) {
|
||
payload.affected_count = affectedCount.value
|
||
}
|
||
if (contextDescription.value.trim()) {
|
||
payload.context_description = contextDescription.value.trim()
|
||
}
|
||
qualifyResult.value = await $api<QualifyResult>('/qualify/', {
|
||
method: 'POST',
|
||
body: payload,
|
||
})
|
||
chosenType.value = qualifyResult.value.decision_type
|
||
wizardStep.value = 'result'
|
||
} catch (err: any) {
|
||
qualifyError.value = err?.message ?? 'Erreur lors de la qualification'
|
||
} finally {
|
||
qualifying.value = false
|
||
}
|
||
}
|
||
|
||
// ── Step 2 : Result → user confirms or adjusts ───────────────────────────────
|
||
function chooseType(type: 'individual' | 'collective') {
|
||
chosenType.value = type
|
||
formData.value.decision_type = type === 'collective' ? 'other' : 'other'
|
||
protocols.fetchProtocols()
|
||
wizardStep.value = 'form'
|
||
}
|
||
|
||
// ── Step 3 : Submit ───────────────────────────────────────────────────────────
|
||
async function onSubmit() {
|
||
if (!formData.value.title.trim()) return
|
||
submitting.value = true
|
||
try {
|
||
const decision = await decisions.create({
|
||
...formData.value,
|
||
context: (contextDescription.value ? `[Contexte]\n${contextDescription.value}\n\n` : '')
|
||
+ (formData.value.context ?? ''),
|
||
})
|
||
if (decision) navigateTo(`/decisions/${decision.id}`)
|
||
} catch {
|
||
// handled by store
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||
function goBack() {
|
||
if (wizardStep.value === 'form') {
|
||
wizardStep.value = 'result'
|
||
} else if (wizardStep.value === 'result') {
|
||
wizardStep.value = 'qualify'
|
||
qualifyResult.value = null
|
||
}
|
||
}
|
||
|
||
const confidenceLabel: Record<string, string> = {
|
||
required: 'Obligatoire',
|
||
recommended: 'Recommandé',
|
||
optional: 'Optionnel',
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="decide">
|
||
<!-- Nav -->
|
||
<div class="decide__nav">
|
||
<button v-if="wizardStep !== 'qualify'" class="decide__back-btn" @click="goBack">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" />
|
||
<span>Retour</span>
|
||
</button>
|
||
<NuxtLink v-else to="/decisions" class="decide__back-btn">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" />
|
||
<span>Décisions</span>
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<Transition name="slide-fade" mode="out-in">
|
||
|
||
<!-- ─── ÉTAPE 1 : Qualification ──────────────────────────────────── -->
|
||
<div v-if="wizardStep === 'qualify'" key="qualify" class="decide__step">
|
||
<div class="decide__header">
|
||
<h1 class="decide__title">Quelle décision prendre ?</h1>
|
||
<p class="decide__subtitle">Qualifiez d'abord la situation — le parcours s'adapte</p>
|
||
</div>
|
||
|
||
<div class="qualify-form">
|
||
|
||
<!-- Mandat -->
|
||
<div class="qualify-section">
|
||
<p class="qualify-label">Cette décision entre-t-elle dans le périmètre d'un mandat existant ?</p>
|
||
<div class="qualify-toggle">
|
||
<button
|
||
class="qualify-toggle__btn"
|
||
:class="{ 'qualify-toggle__btn--active': withinMandate === true }"
|
||
@click="withinMandate = true"
|
||
>
|
||
<UIcon name="i-lucide-user-check" />
|
||
Oui, un mandat couvre ce sujet
|
||
</button>
|
||
<button
|
||
class="qualify-toggle__btn"
|
||
:class="{ 'qualify-toggle__btn--active': withinMandate === false }"
|
||
@click="withinMandate = false"
|
||
>
|
||
<UIcon name="i-lucide-circle-slash" />
|
||
Non, hors mandat
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Personnes concernées — seulement si hors mandat -->
|
||
<Transition name="slide-down">
|
||
<div v-if="withinMandate === false" class="qualify-section">
|
||
<p class="qualify-label">Combien de personnes sont concernées ou impactées ?</p>
|
||
<div class="qualify-count">
|
||
<input
|
||
v-model.number="affectedCount"
|
||
type="number"
|
||
min="2"
|
||
step="1"
|
||
placeholder="ex : 12"
|
||
class="qualify-count__input"
|
||
/>
|
||
<span class="qualify-count__hint">personnes (minimum 2 — si seulement vous, cette décision sort du périmètre de l'outil)</span>
|
||
</div>
|
||
<div class="qualify-presets">
|
||
<button class="qualify-preset" @click="affectedCount = 3">2–5</button>
|
||
<button class="qualify-preset" @click="affectedCount = 20">6–50</button>
|
||
<button class="qualify-preset" @click="affectedCount = 100">> 50</button>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
|
||
<!-- Caractère structurant -->
|
||
<div class="qualify-section">
|
||
<p class="qualify-label">Cette décision est-elle structurante ?</p>
|
||
<p class="qualify-hint">
|
||
Une décision structurante a valeur de loi au sein de la communauté,
|
||
ou déclenche une action machine (ex : runtime upgrade).
|
||
La gravure on-chain peut alors être recommandée.
|
||
</p>
|
||
<div class="qualify-toggle">
|
||
<button
|
||
class="qualify-toggle__btn"
|
||
:class="{ 'qualify-toggle__btn--active': isStructural === true }"
|
||
@click="isStructural = true"
|
||
>
|
||
<UIcon name="i-lucide-cpu" />
|
||
Oui, décision structurante
|
||
</button>
|
||
<button
|
||
class="qualify-toggle__btn"
|
||
:class="{ 'qualify-toggle__btn--active': isStructural === false }"
|
||
@click="isStructural = false"
|
||
>
|
||
<UIcon name="i-lucide-leaf" />
|
||
Non, décision ordinaire
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contexte optionnel -->
|
||
<div class="qualify-section">
|
||
<p class="qualify-label">Décrivez brièvement le contexte <span class="qualify-optional">(optionnel)</span></p>
|
||
<textarea
|
||
v-model="contextDescription"
|
||
class="qualify-textarea"
|
||
rows="3"
|
||
placeholder="En quelques mots : de quoi s'agit-il ? Les modalités proposées seront plus précises."
|
||
/>
|
||
</div>
|
||
|
||
<!-- Error -->
|
||
<p v-if="qualifyError" class="qualify-error">{{ qualifyError }}</p>
|
||
|
||
<!-- Submit -->
|
||
<button
|
||
class="qualify-submit"
|
||
:disabled="!canQualify || qualifying"
|
||
@click="runQualify"
|
||
>
|
||
<UIcon v-if="qualifying" name="i-lucide-loader-2" class="animate-spin" />
|
||
<UIcon v-else name="i-lucide-arrow-right" />
|
||
{{ qualifying ? 'Qualification en cours…' : 'Qualifier la décision' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 2 : Résultat de qualification ──────────────────────── -->
|
||
<div v-else-if="wizardStep === 'result' && qualifyResult" key="result" class="decide__step">
|
||
<div class="decide__header">
|
||
<h1 class="decide__title">Résultat de qualification</h1>
|
||
<p class="decide__subtitle">Voici ce que l'outil recommande — vous pouvez ajuster</p>
|
||
</div>
|
||
|
||
<!-- Recommandation principale -->
|
||
<div class="qresult">
|
||
<div class="qresult__type" :class="`qresult__type--${qualifyResult.decision_type}`">
|
||
<UIcon
|
||
:name="qualifyResult.decision_type === 'individual' ? 'i-lucide-user' : 'i-lucide-users'"
|
||
class="text-2xl"
|
||
/>
|
||
<div>
|
||
<span class="qresult__type-label">
|
||
{{ qualifyResult.decision_type === 'individual' ? 'Décision individuelle' : 'Décision collective' }}
|
||
</span>
|
||
<span class="qresult__confidence">
|
||
{{ confidenceLabel[qualifyResult.confidence] ?? qualifyResult.confidence }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Raisons -->
|
||
<ul class="qresult__reasons">
|
||
<li v-for="(r, i) in qualifyResult.reasons" :key="i">
|
||
<UIcon name="i-lucide-info" class="text-xs" />
|
||
{{ r }}
|
||
</li>
|
||
</ul>
|
||
|
||
<!-- Observatoire -->
|
||
<div v-if="qualifyResult.record_in_observatory" class="qresult__observatory">
|
||
<UIcon name="i-lucide-bookmark" />
|
||
Cette décision sera consignée dans l'<strong>Observatoire des décisions</strong>
|
||
</div>
|
||
|
||
<!-- On-chain -->
|
||
<div v-if="qualifyResult.recommend_onchain" class="qresult__onchain">
|
||
<UIcon name="i-lucide-cpu" class="text-base" />
|
||
<div>
|
||
<strong>Gravure on-chain recommandée</strong>
|
||
<p>{{ qualifyResult.onchain_reason }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modalités collectives -->
|
||
<div v-if="qualifyResult.recommended_modalities.length > 0" class="qresult__modalities">
|
||
<p class="qresult__modalities-title">Modalités disponibles</p>
|
||
<div class="qresult__modalities-list">
|
||
<div
|
||
v-for="slug in qualifyResult.recommended_modalities"
|
||
:key="slug"
|
||
class="qresult__modality"
|
||
>
|
||
<div class="qresult__modality-icon">
|
||
<UIcon :name="modalityLabel(slug).icon" />
|
||
</div>
|
||
<div class="qresult__modality-body">
|
||
<span class="qresult__modality-title">{{ modalityLabel(slug).title }}</span>
|
||
<span class="qresult__modality-desc">{{ modalityLabel(slug).desc }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Choix de validation / ajustement -->
|
||
<div class="qresult__actions">
|
||
<button
|
||
class="qresult__action qresult__action--primary"
|
||
@click="chooseType(qualifyResult.decision_type)"
|
||
>
|
||
<UIcon name="i-lucide-check" />
|
||
Confirmer et continuer
|
||
</button>
|
||
|
||
<button
|
||
v-if="qualifyResult.collective_available && qualifyResult.decision_type === 'individual'"
|
||
class="qresult__action qresult__action--alt"
|
||
@click="chooseType('collective')"
|
||
>
|
||
<UIcon name="i-lucide-users" />
|
||
Passer en collectif quand même
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 3 : Formulaire de décision ─────────────────────────── -->
|
||
<div v-else-if="wizardStep === 'form'" key="form" class="decide__step">
|
||
<div class="decide__header">
|
||
<h1 class="decide__title">
|
||
{{ chosenType === 'collective' ? 'Décision collective' : 'Décision individuelle' }}
|
||
</h1>
|
||
<p class="decide__subtitle">Renseignez les détails de votre décision</p>
|
||
</div>
|
||
|
||
<form class="decide-form" @submit.prevent="onSubmit">
|
||
<div class="decide-form__field">
|
||
<label class="decide-form__label">Titre <span class="decide-form__req">*</span></label>
|
||
<input
|
||
v-model="formData.title"
|
||
class="decide-form__input"
|
||
type="text"
|
||
placeholder="Nommez la décision en une phrase"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div class="decide-form__field">
|
||
<label class="decide-form__label">Description</label>
|
||
<textarea
|
||
v-model="formData.description"
|
||
class="decide-form__textarea"
|
||
rows="4"
|
||
placeholder="Exposé complet de la décision, des options, des enjeux…"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Protocole de vote — seulement si collectif -->
|
||
<div v-if="chosenType === 'collective'" class="decide-form__field">
|
||
<label class="decide-form__label">Protocole de vote</label>
|
||
<select v-model="formData.voting_protocol_id" class="decide-form__select">
|
||
<option :value="null">— Choisir un protocole —</option>
|
||
<option
|
||
v-for="p in protocols.protocols"
|
||
:key="p.id"
|
||
:value="p.id"
|
||
>
|
||
{{ p.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button type="submit" class="decide-form__submit" :disabled="submitting || !formData.title.trim()">
|
||
<UIcon v-if="submitting" name="i-lucide-loader-2" class="animate-spin" />
|
||
<UIcon v-else name="i-lucide-save" />
|
||
{{ submitting ? 'Enregistrement…' : 'Créer la décision' }}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
</Transition>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.decide {
|
||
max-width: 42rem;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
/* Nav */
|
||
.decide__nav {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.decide__back-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--mood-text-muted);
|
||
text-decoration: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
transition: color 0.12s;
|
||
}
|
||
.decide__back-btn:hover { color: var(--mood-text); }
|
||
|
||
/* Header */
|
||
.decide__header { padding: 0.25rem 0; }
|
||
.decide__title {
|
||
font-size: 1.5rem;
|
||
font-weight: 800;
|
||
color: var(--mood-text);
|
||
letter-spacing: -0.02em;
|
||
}
|
||
.decide__subtitle {
|
||
margin-top: 0.25rem;
|
||
font-size: 0.9375rem;
|
||
color: var(--mood-text-muted);
|
||
}
|
||
|
||
/* ── Qualification form ── */
|
||
.qualify-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.qualify-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.625rem;
|
||
}
|
||
|
||
.qualify-label {
|
||
font-size: 0.9375rem;
|
||
font-weight: 700;
|
||
color: var(--mood-text);
|
||
}
|
||
|
||
.qualify-hint {
|
||
font-size: 0.8125rem;
|
||
color: var(--mood-text-muted);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.qualify-optional {
|
||
font-weight: 400;
|
||
color: var(--mood-text-muted);
|
||
font-size: 0.8125rem;
|
||
}
|
||
|
||
.qualify-toggle {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
@media (min-width: 480px) {
|
||
.qualify-toggle { flex-direction: row; }
|
||
}
|
||
|
||
.qualify-toggle__btn {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1rem;
|
||
border-radius: 12px;
|
||
background: var(--mood-surface);
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--mood-text-muted);
|
||
cursor: pointer;
|
||
transition: all 0.12s ease;
|
||
}
|
||
.qualify-toggle__btn:hover {
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-text);
|
||
}
|
||
.qualify-toggle__btn--active {
|
||
background: var(--mood-accent);
|
||
color: var(--mood-accent-text);
|
||
}
|
||
|
||
.qualify-count {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.qualify-count__input {
|
||
width: 7rem;
|
||
padding: 0.625rem 0.875rem;
|
||
border-radius: 12px;
|
||
background: var(--mood-surface);
|
||
color: var(--mood-text);
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.qualify-count__hint {
|
||
font-size: 0.8125rem;
|
||
color: var(--mood-text-muted);
|
||
line-height: 1.5;
|
||
padding-top: 0.5rem;
|
||
}
|
||
|
||
.qualify-presets {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.qualify-preset {
|
||
padding: 0.375rem 0.875rem;
|
||
border-radius: 20px;
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
font-size: 0.8125rem;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: background 0.1s;
|
||
}
|
||
.qualify-preset:hover { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
|
||
.qualify-textarea {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border-radius: 12px;
|
||
background: var(--mood-surface);
|
||
color: var(--mood-text);
|
||
font-size: 0.9375rem;
|
||
resize: vertical;
|
||
min-height: 4.5rem;
|
||
}
|
||
|
||
.qualify-error {
|
||
font-size: 0.875rem;
|
||
color: var(--mood-danger, #e53e3e);
|
||
padding: 0.625rem 1rem;
|
||
background: color-mix(in srgb, var(--mood-danger, #e53e3e) 10%, transparent);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.qualify-submit {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.875rem 1.5rem;
|
||
border-radius: 20px;
|
||
background: var(--mood-accent);
|
||
color: var(--mood-accent-text);
|
||
font-size: 1rem;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
align-self: flex-start;
|
||
}
|
||
.qualify-submit:hover:not(:disabled) {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 16px var(--mood-shadow);
|
||
}
|
||
.qualify-submit:disabled { opacity: 0.5; cursor: default; }
|
||
|
||
/* ── Result ── */
|
||
.qresult {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
padding: 1.25rem;
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.qresult__type {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.875rem;
|
||
padding: 1rem;
|
||
border-radius: 12px;
|
||
background: var(--mood-accent-soft);
|
||
}
|
||
.qresult__type--collective { background: color-mix(in srgb, var(--mood-accent) 15%, transparent); }
|
||
|
||
.qresult__type-label {
|
||
display: block;
|
||
font-size: 1.125rem;
|
||
font-weight: 800;
|
||
color: var(--mood-text);
|
||
}
|
||
.qresult__confidence {
|
||
display: block;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--mood-accent);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
|
||
.qresult__reasons {
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.375rem;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
.qresult__reasons li {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.375rem;
|
||
font-size: 0.875rem;
|
||
color: var(--mood-text-muted);
|
||
}
|
||
|
||
.qresult__observatory {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.625rem 0.875rem;
|
||
border-radius: 10px;
|
||
background: color-mix(in srgb, var(--mood-success, #38a169) 12%, transparent);
|
||
color: var(--mood-success, #38a169);
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.qresult__onchain {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 0.875rem;
|
||
border-radius: 12px;
|
||
background: color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 12%, transparent);
|
||
color: var(--mood-tertiary, var(--mood-accent));
|
||
font-size: 0.875rem;
|
||
}
|
||
.qresult__onchain p {
|
||
margin-top: 0.25rem;
|
||
color: var(--mood-text-muted);
|
||
font-weight: 400;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.qresult__modalities-title {
|
||
font-size: 0.8125rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--mood-text-muted);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.qresult__modalities-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.qresult__modality {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 0.625rem 0.75rem;
|
||
border-radius: 10px;
|
||
background: var(--mood-accent-soft);
|
||
}
|
||
|
||
.qresult__modality-icon {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
background: var(--mood-accent);
|
||
color: var(--mood-accent-text);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.qresult__modality-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.125rem;
|
||
}
|
||
|
||
.qresult__modality-title {
|
||
font-size: 0.875rem;
|
||
font-weight: 700;
|
||
color: var(--mood-text);
|
||
}
|
||
|
||
.qresult__modality-desc {
|
||
font-size: 0.8125rem;
|
||
color: var(--mood-text-muted);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.qresult__actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.625rem;
|
||
}
|
||
|
||
@media (min-width: 480px) {
|
||
.qresult__actions { flex-direction: row; }
|
||
}
|
||
|
||
.qresult__action {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.25rem;
|
||
border-radius: 20px;
|
||
font-size: 0.9375rem;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
}
|
||
.qresult__action--primary {
|
||
background: var(--mood-accent);
|
||
color: var(--mood-accent-text);
|
||
}
|
||
.qresult__action--alt {
|
||
background: var(--mood-surface);
|
||
color: var(--mood-text);
|
||
}
|
||
.qresult__action:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px var(--mood-shadow);
|
||
}
|
||
|
||
/* ── Decision form ── */
|
||
.decide-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.25rem;
|
||
}
|
||
|
||
.decide-form__field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.decide-form__label {
|
||
font-size: 0.875rem;
|
||
font-weight: 700;
|
||
color: var(--mood-text);
|
||
}
|
||
|
||
.decide-form__req { color: var(--mood-accent); }
|
||
|
||
.decide-form__input,
|
||
.decide-form__textarea,
|
||
.decide-form__select {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border-radius: 12px;
|
||
background: var(--mood-surface);
|
||
color: var(--mood-text);
|
||
font-size: 0.9375rem;
|
||
}
|
||
|
||
.decide-form__textarea { resize: vertical; min-height: 7rem; }
|
||
|
||
.decide-form__submit {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.875rem 1.5rem;
|
||
border-radius: 20px;
|
||
background: var(--mood-accent);
|
||
color: var(--mood-accent-text);
|
||
font-size: 1rem;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
align-self: flex-start;
|
||
}
|
||
.decide-form__submit:hover:not(:disabled) {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 16px var(--mood-shadow);
|
||
}
|
||
.decide-form__submit:disabled { opacity: 0.5; cursor: default; }
|
||
|
||
/* ── Transitions ── */
|
||
.slide-fade-enter-active,
|
||
.slide-fade-leave-active {
|
||
transition: all 0.18s ease;
|
||
}
|
||
.slide-fade-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(16px);
|
||
}
|
||
.slide-fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(-16px);
|
||
}
|
||
|
||
.slide-down-enter-active,
|
||
.slide-down-leave-active {
|
||
transition: all 0.2s ease;
|
||
overflow: hidden;
|
||
}
|
||
.slide-down-enter-from,
|
||
.slide-down-leave-to {
|
||
opacity: 0;
|
||
max-height: 0;
|
||
}
|
||
.slide-down-enter-to,
|
||
.slide-down-leave-from {
|
||
max-height: 200px;
|
||
}
|
||
</style>
|