forked from yvv/decision
3ba9c43ce3
- Mandate model : champ origin (contexte/déclencheur) + migration
- MandateCreate schema : origin, starts_at, ends_at
- mandates/new.vue : wizard complet
· Étape 1 : type · nom · origine · description · durée (relative ou dates)
· Étape 2 : auto-désignation (ratification) ou désignation collective
· Étape 3 : boîte à outils — sans/avec candidature, 6 modalités,
paramètres configurables (durée, quorum, seuil, slider+input)
· Étape 4 : récap + génération automatique des étapes du mandat
- Tous les boutons "Nouveau mandat" pointent vers /mandates/new
- decisions/new.vue : "Demander un mandat" → /mandates/new
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1008 lines
40 KiB
Vue
1008 lines
40 KiB
Vue
<script setup lang="ts">
|
||
import type { MandateCreate, MandateStepCreate } from '~/stores/mandates'
|
||
|
||
const mandates = useMandatesStore()
|
||
const { $api } = useApi()
|
||
|
||
// ── Wizard steps ──────────────────────────────────────────────────────────────
|
||
type WizardStep = 'info' | 'nomination' | 'toolbox' | 'summary'
|
||
const wizardStep = ref<WizardStep>('info')
|
||
|
||
// ── Step 1 : Infos de base ────────────────────────────────────────────────────
|
||
const title = ref('')
|
||
const origin = ref('')
|
||
const description = ref('')
|
||
const mandateType = ref<'techcomm' | 'smith' | 'custom'>('custom')
|
||
|
||
// Durée
|
||
type DurationMode = 'relative' | 'dates'
|
||
const durationMode = ref<DurationMode>('relative')
|
||
const durationValue = ref(3)
|
||
const durationUnit = ref<'days' | 'weeks' | 'months'>('months')
|
||
const startsAt = ref('')
|
||
const endsAt = ref('')
|
||
|
||
function computedDates(): { starts_at: string | null; ends_at: string | null } {
|
||
if (durationMode.value === 'dates') {
|
||
return {
|
||
starts_at: startsAt.value || null,
|
||
ends_at: endsAt.value || null,
|
||
}
|
||
}
|
||
const now = new Date()
|
||
const end = new Date(now)
|
||
if (durationUnit.value === 'days') end.setDate(end.getDate() + durationValue.value)
|
||
else if (durationUnit.value === 'weeks') end.setDate(end.getDate() + durationValue.value * 7)
|
||
else end.setMonth(end.getMonth() + durationValue.value)
|
||
return {
|
||
starts_at: now.toISOString(),
|
||
ends_at: end.toISOString(),
|
||
}
|
||
}
|
||
|
||
const durationLabel = computed(() => {
|
||
if (durationMode.value === 'dates') {
|
||
if (!startsAt.value && !endsAt.value) return 'Dates non définies'
|
||
return `${startsAt.value ? new Date(startsAt.value).toLocaleDateString('fr-FR') : '?'} → ${endsAt.value ? new Date(endsAt.value).toLocaleDateString('fr-FR') : '?'}`
|
||
}
|
||
const u = durationUnit.value === 'days' ? 'jour' : durationUnit.value === 'weeks' ? 'semaine' : 'mois'
|
||
return `${durationValue.value} ${u}${durationValue.value > 1 && durationUnit.value !== 'months' ? 's' : ''}`
|
||
})
|
||
|
||
const canGoToNomination = computed(() =>
|
||
title.value.trim().length > 0 && (
|
||
durationMode.value === 'relative' ||
|
||
(startsAt.value !== '' && endsAt.value !== '')
|
||
)
|
||
)
|
||
|
||
// ── Step 2 : Nomination ───────────────────────────────────────────────────────
|
||
type NominationCase = 'self' | 'election'
|
||
const nominationCase = ref<NominationCase | null>(null)
|
||
|
||
// Case 1 — self : ratification vote params
|
||
const selfParams = reactive({ duration_days: 7, quorum_pct: 30, majority_pct: 51 })
|
||
|
||
// ── Step 3 : Boîte à outils élection ─────────────────────────────────────────
|
||
type ElectionMode = 'direct' | 'candidacy'
|
||
const electionMode = ref<ElectionMode>('direct')
|
||
const selectedModalityId = ref<string | null>(null)
|
||
|
||
interface ModalityParam {
|
||
id: string
|
||
label: string
|
||
unit: string
|
||
min: number
|
||
max: number
|
||
default: number
|
||
}
|
||
interface Modality {
|
||
id: string
|
||
icon: string
|
||
title: string
|
||
desc: string
|
||
recommended: boolean
|
||
recommendedReason?: string
|
||
params: ModalityParam[]
|
||
requiresCandidacy: boolean
|
||
}
|
||
|
||
const MODALITIES: Modality[] = [
|
||
// ── Sans candidature ──
|
||
{
|
||
id: 'vote_simple',
|
||
icon: 'i-lucide-check-square',
|
||
title: 'Vote de désignation',
|
||
desc: 'Vote direct parmi les membres du cercle concerné. Simple et rapide pour les petits groupes.',
|
||
recommended: mandateType.value !== 'smith',
|
||
recommendedReason: 'Adapté aux petits collectifs et décisions peu contestées.',
|
||
params: [
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'quorum_pct', label: 'Quorum', unit: '%', min: 10, max: 100, default: 30 },
|
||
{ id: 'majority_pct', label: 'Seuil de majorité', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: false,
|
||
},
|
||
{
|
||
id: 'vote_wot',
|
||
icon: 'i-lucide-network',
|
||
title: 'Vote WoT',
|
||
desc: 'Vote pondéré par la Toile de Confiance. Le poids de chaque voix dépend de la distance certificationnelle.',
|
||
recommended: true,
|
||
recommendedReason: 'Recommandé pour les mandats impliquant la communauté G1 au-delà des 10 personnes.',
|
||
params: [
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 60, default: 14 },
|
||
{ id: 'quorum_pct', label: 'Quorum WoT', unit: '% de la WoT', min: 5, max: 100, default: 20 },
|
||
{ id: 'majority_pct', label: 'Seuil', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: false,
|
||
},
|
||
{
|
||
id: 'vote_smith',
|
||
icon: 'i-lucide-hammer',
|
||
title: 'Vote Smith',
|
||
desc: 'Réservé aux Forgerons (critère de Smith). Utilisé pour les décisions techniques du réseau.',
|
||
recommended: mandateType.value === 'smith' || mandateType.value === 'techcomm',
|
||
recommendedReason: 'Standard pour les mandats Forgeron et Comité Technique.',
|
||
params: [
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 30, default: 14 },
|
||
{ id: 'smith_min', label: 'Forgerons minimum', unit: 'forgerons', min: 1, max: 20, default: 3 },
|
||
],
|
||
requiresCandidacy: false,
|
||
},
|
||
// ── Avec candidature ──
|
||
{
|
||
id: 'candidacy_vote_majoritaire',
|
||
icon: 'i-lucide-user-check',
|
||
title: 'Candidature + Vote majoritaire',
|
||
desc: 'Appel à candidature ouvert, suivi d\'un vote binaire (Pour/Contre) pour chaque candidat.',
|
||
recommended: false,
|
||
params: [
|
||
{ id: 'candidacy_days', label: 'Fenêtre de candidature', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'quorum_pct', label: 'Quorum', unit: '%', min: 10, max: 100, default: 30 },
|
||
{ id: 'majority_pct', label: 'Seuil', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: true,
|
||
},
|
||
{
|
||
id: 'candidacy_vote_preferentiel',
|
||
icon: 'i-lucide-list-ordered',
|
||
title: 'Candidature + Vote préférentiel',
|
||
desc: 'Candidature ouverte, puis classement des candidats par les votants (méthode Condorcet ou Borda).',
|
||
recommended: true,
|
||
recommendedReason: 'Recommandé dès 3 candidats pour éviter les effets de vote utile.',
|
||
params: [
|
||
{ id: 'candidacy_days', label: 'Fenêtre de candidature', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 60, default: 14 },
|
||
],
|
||
requiresCandidacy: true,
|
||
},
|
||
{
|
||
id: 'candidacy_vote_wot',
|
||
icon: 'i-lucide-network',
|
||
title: 'Candidature + Vote WoT',
|
||
desc: 'Candidature ouverte, puis vote pondéré par la Toile de Confiance.',
|
||
recommended: false,
|
||
params: [
|
||
{ id: 'candidacy_days', label: 'Fenêtre de candidature', unit: 'jours', min: 1, max: 30, default: 7 },
|
||
{ id: 'duration_days', label: 'Durée du vote', unit: 'jours', min: 1, max: 60, default: 14 },
|
||
{ id: 'majority_pct', label: 'Seuil WoT', unit: '%', min: 51, max: 100, default: 51 },
|
||
],
|
||
requiresCandidacy: true,
|
||
},
|
||
]
|
||
|
||
const visibleModalities = computed(() =>
|
||
MODALITIES.filter(m => m.requiresCandidacy === (electionMode.value === 'candidacy'))
|
||
)
|
||
|
||
// Paramètres par modalité (valeurs mutable)
|
||
const paramValues = reactive<Record<string, Record<string, number>>>({})
|
||
function getParam(modalityId: string, paramId: string): number {
|
||
if (!paramValues[modalityId]) {
|
||
const m = MODALITIES.find(x => x.id === modalityId)
|
||
if (m) paramValues[modalityId] = Object.fromEntries(m.params.map(p => [p.id, p.default]))
|
||
}
|
||
return paramValues[modalityId]?.[paramId] ?? 0
|
||
}
|
||
function setParam(modalityId: string, paramId: string, value: number) {
|
||
if (!paramValues[modalityId]) getParam(modalityId, paramId)
|
||
paramValues[modalityId][paramId] = value
|
||
}
|
||
|
||
// ── Génération des steps à créer ──────────────────────────────────────────────
|
||
interface StepDef { step_type: string; title: string; description: string }
|
||
|
||
const stepsToCreate = computed<StepDef[]>(() => {
|
||
const steps: StepDef[] = []
|
||
steps.push({ step_type: 'formulation', title: 'Rédaction du mandat', description: 'Définition du périmètre, des objectifs et des attendus.' })
|
||
|
||
if (nominationCase.value === 'self') {
|
||
steps.push({ step_type: 'vote', title: 'Ratification', description: `Vote de ratification — durée ${selfParams.duration_days}j, quorum ${selfParams.quorum_pct}%, seuil ${selfParams.majority_pct}%.` })
|
||
steps.push({ step_type: 'assignment', title: 'Prise de mandat', description: 'La personne entre en mandat après ratification.' })
|
||
} else if (nominationCase.value === 'election' && selectedModalityId.value) {
|
||
const m = MODALITIES.find(x => x.id === selectedModalityId.value)
|
||
if (m?.requiresCandidacy) {
|
||
const cdays = getParam(m.id, 'candidacy_days')
|
||
steps.push({ step_type: 'candidacy', title: 'Appel à candidature', description: `Fenêtre de candidature : ${cdays} jour${cdays > 1 ? 's' : ''}.` })
|
||
}
|
||
if (m) {
|
||
const pv = paramValues[m.id] ?? {}
|
||
const paramStr = Object.entries(pv).filter(([k]) => k !== 'candidacy_days').map(([k, v]) => {
|
||
const p = m.params.find(x => x.id === k)
|
||
return p ? `${p.label} : ${v} ${p.unit}` : ''
|
||
}).filter(Boolean).join(', ')
|
||
steps.push({ step_type: 'vote', title: `Élection — ${m.title}`, description: paramStr })
|
||
}
|
||
steps.push({ step_type: 'assignment', title: 'Désignation du mandataire', description: 'Le candidat élu entre en mandat.' })
|
||
}
|
||
|
||
steps.push({ step_type: 'reporting', title: 'Compte-rendu', description: 'Rapport d\'activité en fin de mandat.' })
|
||
steps.push({ step_type: 'completion', title: 'Clôture', description: 'Validation de la fin du mandat.' })
|
||
return steps
|
||
})
|
||
|
||
const canGoToSummary = computed(() => {
|
||
if (nominationCase.value === 'self') return true
|
||
if (nominationCase.value === 'election') return selectedModalityId.value !== null
|
||
return false
|
||
})
|
||
|
||
// ── Création ──────────────────────────────────────────────────────────────────
|
||
const submitting = ref(false)
|
||
const submitError = ref<string | null>(null)
|
||
|
||
async function createMandate() {
|
||
submitting.value = true
|
||
submitError.value = null
|
||
try {
|
||
const dates = computedDates()
|
||
const mandate = await mandates.create({
|
||
title: title.value.trim(),
|
||
origin: origin.value.trim() || null,
|
||
description: description.value.trim() || null,
|
||
mandate_type: mandateType.value,
|
||
starts_at: dates.starts_at,
|
||
ends_at: dates.ends_at,
|
||
})
|
||
if (!mandate) throw new Error('Erreur création mandat')
|
||
|
||
// Create steps
|
||
for (let i = 0; i < stepsToCreate.value.length; i++) {
|
||
const s = stepsToCreate.value[i]
|
||
await $api(`/mandates/${mandate.id}/steps`, {
|
||
method: 'POST',
|
||
body: { step_order: i, step_type: s.step_type, title: s.title, description: s.description },
|
||
})
|
||
}
|
||
|
||
navigateTo(`/mandates/${mandate.id}`)
|
||
} catch (e: any) {
|
||
submitError.value = e?.message ?? 'Erreur lors de la création'
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||
function goBack() {
|
||
if (wizardStep.value === 'summary') wizardStep.value = nominationCase.value === 'election' ? 'toolbox' : 'nomination'
|
||
else if (wizardStep.value === 'toolbox') wizardStep.value = 'nomination'
|
||
else if (wizardStep.value === 'nomination') wizardStep.value = 'info'
|
||
}
|
||
|
||
function goToNomination() { if (canGoToNomination.value) wizardStep.value = 'nomination' }
|
||
function goToToolbox() { wizardStep.value = 'toolbox' }
|
||
function goToSummary() { if (canGoToSummary.value) wizardStep.value = 'summary' }
|
||
|
||
// Typage
|
||
const MANDATE_TYPE_OPTIONS = [
|
||
{ value: 'custom', label: 'Mandat libre', icon: 'i-lucide-circle' },
|
||
{ value: 'techcomm', label: 'Comité Technique', icon: 'i-lucide-cpu' },
|
||
{ value: 'smith', label: 'Forgeron', icon: 'i-lucide-hammer' },
|
||
]
|
||
</script>
|
||
|
||
<template>
|
||
<div class="mwiz">
|
||
<!-- Nav -->
|
||
<div class="mwiz__nav">
|
||
<button v-if="wizardStep !== 'info'" class="mwiz__back" @click="goBack">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" />
|
||
<span>Retour</span>
|
||
</button>
|
||
<NuxtLink v-else to="/mandates" class="mwiz__back">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" />
|
||
<span>Mandats</span>
|
||
</NuxtLink>
|
||
|
||
<!-- Progress pills -->
|
||
<div class="mwiz__progress">
|
||
<span class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'info', 'mwiz__pill--done': wizardStep !== 'info' }">1 Infos</span>
|
||
<span class="mwiz__sep">›</span>
|
||
<span class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'nomination', 'mwiz__pill--done': ['toolbox','summary'].includes(wizardStep) }">2 Nomination</span>
|
||
<span class="mwiz__sep">›</span>
|
||
<span v-if="nominationCase === 'election'" class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'toolbox', 'mwiz__pill--done': wizardStep === 'summary' }">3 Modalités</span>
|
||
<span v-if="nominationCase === 'election'" class="mwiz__sep">›</span>
|
||
<span class="mwiz__pill" :class="{ 'mwiz__pill--active': wizardStep === 'summary' }">{{ nominationCase === 'election' ? '4' : '3' }} Récap</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Transition name="slide-fade" mode="out-in">
|
||
|
||
<!-- ─── ÉTAPE 1 : Infos de base ──────────────────────────────────── -->
|
||
<div v-if="wizardStep === 'info'" key="info" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Nouveau mandat</h1>
|
||
<p class="mwiz__subtitle">Définissez le périmètre et la durée du mandat</p>
|
||
</div>
|
||
|
||
<div class="mwiz__form">
|
||
|
||
<!-- Type de mandat -->
|
||
<div class="mwiz__section">
|
||
<p class="mwiz__label">Type de mandat</p>
|
||
<div class="mwiz__type-grid">
|
||
<button
|
||
v-for="opt in MANDATE_TYPE_OPTIONS"
|
||
:key="opt.value"
|
||
class="mwiz__type-btn"
|
||
:class="{ 'mwiz__type-btn--active': mandateType === opt.value }"
|
||
@click="mandateType = opt.value as any"
|
||
>
|
||
<UIcon :name="opt.icon" class="text-base" />
|
||
<span>{{ opt.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Nom -->
|
||
<div class="mwiz__section">
|
||
<label class="mwiz__label">Nom du mandat <span class="mwiz__req">*</span></label>
|
||
<input
|
||
v-model="title"
|
||
type="text"
|
||
class="mwiz__input"
|
||
lang="fr"
|
||
spellcheck="true"
|
||
placeholder="Ex : Responsable communication, Délégué technique G1..."
|
||
/>
|
||
</div>
|
||
|
||
<!-- Origine -->
|
||
<div class="mwiz__section">
|
||
<label class="mwiz__label">Origine <span class="mwiz__optional">(optionnel)</span></label>
|
||
<textarea
|
||
v-model="origin"
|
||
class="mwiz__textarea"
|
||
rows="2"
|
||
lang="fr"
|
||
spellcheck="true"
|
||
placeholder="Qui propose ce mandat, dans quel contexte, suite à quelle décision ou besoin ?"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Description -->
|
||
<div class="mwiz__section">
|
||
<label class="mwiz__label">Description du mandat <span class="mwiz__optional">(optionnel)</span></label>
|
||
<textarea
|
||
v-model="description"
|
||
class="mwiz__textarea"
|
||
rows="3"
|
||
lang="fr"
|
||
spellcheck="true"
|
||
placeholder="Périmètre, responsabilités, attendus du mandataire..."
|
||
/>
|
||
</div>
|
||
|
||
<!-- Durée -->
|
||
<div class="mwiz__section">
|
||
<p class="mwiz__label">Durée du mandat</p>
|
||
<div class="mwiz__dur-toggle">
|
||
<button class="mwiz__dur-btn" :class="{ 'mwiz__dur-btn--active': durationMode === 'relative' }" @click="durationMode = 'relative'">
|
||
<UIcon name="i-lucide-clock" class="text-sm" /> Durée
|
||
</button>
|
||
<button class="mwiz__dur-btn" :class="{ 'mwiz__dur-btn--active': durationMode === 'dates' }" @click="durationMode = 'dates'">
|
||
<UIcon name="i-lucide-calendar" class="text-sm" /> Dates fixes
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="durationMode === 'relative'" class="mwiz__dur-relative">
|
||
<input v-model.number="durationValue" type="number" class="mwiz__dur-number" min="1" max="999" />
|
||
<div class="mwiz__type-grid mwiz__type-grid--sm">
|
||
<button class="mwiz__type-btn" :class="{ 'mwiz__type-btn--active': durationUnit === 'days' }" @click="durationUnit = 'days'">Jours</button>
|
||
<button class="mwiz__type-btn" :class="{ 'mwiz__type-btn--active': durationUnit === 'weeks' }" @click="durationUnit = 'weeks'">Semaines</button>
|
||
<button class="mwiz__type-btn" :class="{ 'mwiz__type-btn--active': durationUnit === 'months' }" @click="durationUnit = 'months'">Mois</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="mwiz__dur-dates">
|
||
<div class="mwiz__dur-date-field">
|
||
<label class="mwiz__label-sm">Début</label>
|
||
<input v-model="startsAt" type="date" class="mwiz__input" />
|
||
</div>
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm mwiz__dur-arrow" />
|
||
<div class="mwiz__dur-date-field">
|
||
<label class="mwiz__label-sm">Fin</label>
|
||
<input v-model="endsAt" type="date" class="mwiz__input" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="mwiz__submit" :disabled="!canGoToNomination" @click="goToNomination">
|
||
Étape suivante — Nomination
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 2 : Nomination ─────────────────────────────────────── -->
|
||
<div v-else-if="wizardStep === 'nomination'" key="nomination" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Processus de nomination</h1>
|
||
<p class="mwiz__subtitle">Ce mandat est-il demandé par son futur titulaire, ou faut-il désigner quelqu'un ?</p>
|
||
</div>
|
||
|
||
<div class="mwiz__nom-grid">
|
||
<!-- Cas 1 : soi-même -->
|
||
<button
|
||
class="mwiz__nom-card"
|
||
:class="{ 'mwiz__nom-card--active': nominationCase === 'self' }"
|
||
@click="nominationCase = 'self'"
|
||
>
|
||
<div class="mwiz__nom-icon">
|
||
<UIcon name="i-lucide-user-check" class="text-2xl" />
|
||
</div>
|
||
<div class="mwiz__nom-body">
|
||
<p class="mwiz__nom-title">Auto-désignation</p>
|
||
<p class="mwiz__nom-desc">La personne à l'origine du mandat le demande pour elle-même. Un vote de ratification valide la prise de mandat.</p>
|
||
</div>
|
||
</button>
|
||
|
||
<!-- Cas 2 : élection -->
|
||
<button
|
||
class="mwiz__nom-card"
|
||
:class="{ 'mwiz__nom-card--active': nominationCase === 'election' }"
|
||
@click="nominationCase = 'election'"
|
||
>
|
||
<div class="mwiz__nom-icon">
|
||
<UIcon name="i-lucide-users" class="text-2xl" />
|
||
</div>
|
||
<div class="mwiz__nom-body">
|
||
<p class="mwiz__nom-title">Désignation par le collectif</p>
|
||
<p class="mwiz__nom-desc">Le mandataire est élu ou désigné par le groupe. La boîte à outils propose les modalités d'élection adaptées.</p>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Cas 1 : paramètres ratification -->
|
||
<Transition name="slide-down">
|
||
<div v-if="nominationCase === 'self'" class="mwiz__self-params">
|
||
<p class="mwiz__label">Paramètres du vote de ratification</p>
|
||
<div class="mwiz__param-row">
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Durée du vote</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfParams.duration_days" type="number" class="mwiz__param-input" min="1" max="30" />
|
||
<span class="mwiz__param-unit">jours</span>
|
||
</div>
|
||
</div>
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Quorum</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfParams.quorum_pct" type="number" class="mwiz__param-input" min="10" max="100" />
|
||
<span class="mwiz__param-unit">%</span>
|
||
</div>
|
||
</div>
|
||
<div class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">Seuil de majorité</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input v-model.number="selfParams.majority_pct" type="number" class="mwiz__param-input" min="51" max="100" />
|
||
<span class="mwiz__param-unit">%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button class="mwiz__submit" @click="goToSummary">
|
||
Récapitulatif
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm" />
|
||
</button>
|
||
</div>
|
||
</Transition>
|
||
|
||
<Transition name="slide-down">
|
||
<div v-if="nominationCase === 'election'" class="mwiz__election-cta">
|
||
<button class="mwiz__submit" @click="goToToolbox">
|
||
Choisir les modalités d'élection
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm" />
|
||
</button>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 3 : Boîte à outils élection ───────────────────────── -->
|
||
<div v-else-if="wizardStep === 'toolbox'" key="toolbox" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Modalités d'élection</h1>
|
||
<p class="mwiz__subtitle">Choisissez le mode de désignation et configurez les paramètres</p>
|
||
</div>
|
||
|
||
<!-- Mode toggle -->
|
||
<div class="mwiz__mode-toggle">
|
||
<button class="mwiz__mode-btn" :class="{ 'mwiz__mode-btn--active': electionMode === 'direct' }" @click="electionMode = 'direct'">
|
||
<UIcon name="i-lucide-vote" class="text-sm" />
|
||
Sans candidature
|
||
</button>
|
||
<button class="mwiz__mode-btn" :class="{ 'mwiz__mode-btn--active': electionMode === 'candidacy' }" @click="electionMode = 'candidacy'">
|
||
<UIcon name="i-lucide-user-plus" class="text-sm" />
|
||
Avec candidature
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Modalités -->
|
||
<div class="mwiz__modalities">
|
||
<div
|
||
v-for="m in visibleModalities"
|
||
:key="m.id"
|
||
class="mwiz__modality"
|
||
:class="{
|
||
'mwiz__modality--selected': selectedModalityId === m.id,
|
||
'mwiz__modality--recommended': m.recommended,
|
||
}"
|
||
@click="selectedModalityId = m.id"
|
||
>
|
||
<div class="mwiz__modality-head">
|
||
<div class="mwiz__modality-icon">
|
||
<UIcon :name="m.icon" class="text-lg" />
|
||
</div>
|
||
<div class="mwiz__modality-info">
|
||
<p class="mwiz__modality-title">{{ m.title }}</p>
|
||
<p class="mwiz__modality-desc">{{ m.desc }}</p>
|
||
</div>
|
||
<div class="mwiz__modality-check">
|
||
<UIcon v-if="selectedModalityId === m.id" name="i-lucide-circle-check" class="text-lg" />
|
||
<UIcon v-else name="i-lucide-circle" class="text-lg" />
|
||
</div>
|
||
</div>
|
||
|
||
<span v-if="m.recommended" class="mwiz__modality-badge">
|
||
<UIcon name="i-lucide-sparkles" class="text-xs" />
|
||
Recommandé
|
||
</span>
|
||
<p v-if="m.recommended && m.recommendedReason" class="mwiz__modality-reason">{{ m.recommendedReason }}</p>
|
||
|
||
<!-- Paramètres (visibles si sélectionné) -->
|
||
<Transition name="slide-down">
|
||
<div v-if="selectedModalityId === m.id" class="mwiz__modality-params">
|
||
<div v-for="p in m.params" :key="p.id" class="mwiz__param-field">
|
||
<label class="mwiz__label-sm">{{ p.label }}</label>
|
||
<div class="mwiz__param-input-row">
|
||
<input
|
||
:value="getParam(m.id, p.id)"
|
||
type="number"
|
||
class="mwiz__param-input"
|
||
:min="p.min"
|
||
:max="p.max"
|
||
@input="setParam(m.id, p.id, +($event.target as HTMLInputElement).value)"
|
||
/>
|
||
<span class="mwiz__param-unit">{{ p.unit }}</span>
|
||
<input
|
||
:value="getParam(m.id, p.id)"
|
||
type="range"
|
||
class="mwiz__param-slider"
|
||
:min="p.min"
|
||
:max="p.max"
|
||
@input="setParam(m.id, p.id, +($event.target as HTMLInputElement).value)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="mwiz__submit" :disabled="!canGoToSummary" @click="goToSummary">
|
||
Récapitulatif
|
||
<UIcon name="i-lucide-arrow-right" class="text-sm" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ─── ÉTAPE 4 : Récapitulatif ──────────────────────────────────── -->
|
||
<div v-else-if="wizardStep === 'summary'" key="summary" class="mwiz__step">
|
||
<div class="mwiz__header">
|
||
<h1 class="mwiz__title">Récapitulatif</h1>
|
||
<p class="mwiz__subtitle">Vérifiez avant de créer le mandat</p>
|
||
</div>
|
||
|
||
<div class="mwiz__recap">
|
||
<!-- Infos -->
|
||
<div class="mwiz__recap-block">
|
||
<p class="mwiz__recap-label">Mandat</p>
|
||
<p class="mwiz__recap-title">{{ title }}</p>
|
||
<p v-if="origin" class="mwiz__recap-meta">Origine : {{ origin }}</p>
|
||
<p v-if="description" class="mwiz__recap-meta">{{ description }}</p>
|
||
<p class="mwiz__recap-meta">
|
||
<UIcon name="i-lucide-clock" class="text-xs" />
|
||
{{ durationLabel }}
|
||
</p>
|
||
<p class="mwiz__recap-meta">
|
||
<UIcon :name="MANDATE_TYPE_OPTIONS.find(o => o.value === mandateType)?.icon ?? 'i-lucide-circle'" class="text-xs" />
|
||
{{ MANDATE_TYPE_OPTIONS.find(o => o.value === mandateType)?.label }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Nomination -->
|
||
<div class="mwiz__recap-block">
|
||
<p class="mwiz__recap-label">Nomination</p>
|
||
<p class="mwiz__recap-value">
|
||
{{ nominationCase === 'self' ? 'Auto-désignation avec ratification' : `Élection — ${MODALITIES.find(m => m.id === selectedModalityId)?.title ?? ''}` }}
|
||
</p>
|
||
<p v-if="nominationCase === 'self'" class="mwiz__recap-meta">
|
||
Durée : {{ selfParams.duration_days }}j · Quorum : {{ selfParams.quorum_pct }}% · Seuil : {{ selfParams.majority_pct }}%
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Steps -->
|
||
<div class="mwiz__recap-block">
|
||
<p class="mwiz__recap-label">Étapes générées ({{ stepsToCreate.length }})</p>
|
||
<ol class="mwiz__steps-list">
|
||
<li v-for="(s, i) in stepsToCreate" :key="i" class="mwiz__steps-item">
|
||
<span class="mwiz__steps-num">{{ i + 1 }}</span>
|
||
<div>
|
||
<p class="mwiz__steps-title">{{ s.title }}</p>
|
||
<p class="mwiz__steps-desc">{{ s.description }}</p>
|
||
</div>
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="submitError" class="mwiz__error">{{ submitError }}</p>
|
||
|
||
<div class="mwiz__recap-actions">
|
||
<button class="mwiz__back-btn" @click="goBack">
|
||
<UIcon name="i-lucide-arrow-left" class="text-sm" /> Modifier
|
||
</button>
|
||
<button class="mwiz__create-btn" :disabled="submitting" @click="createMandate">
|
||
<UIcon v-if="submitting" name="i-lucide-loader-2" class="animate-spin text-sm" />
|
||
<UIcon v-else name="i-lucide-check" class="text-sm" />
|
||
{{ submitting ? 'Création…' : 'Créer le mandat' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</Transition>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.mwiz {
|
||
max-width: 720px;
|
||
margin: 0 auto;
|
||
padding: 1.5rem 1rem 4rem;
|
||
}
|
||
|
||
/* Nav */
|
||
.mwiz__nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 2rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mwiz__back {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
font-size: 0.875rem;
|
||
color: var(--mood-muted);
|
||
cursor: pointer;
|
||
transition: color 0.1s;
|
||
}
|
||
.mwiz__back:hover { color: var(--mood-text); }
|
||
.mwiz__progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
margin-left: auto;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mwiz__sep { color: var(--mood-muted); font-size: 0.75rem; }
|
||
.mwiz__pill {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
padding: 0.25rem 0.625rem;
|
||
border-radius: 20px;
|
||
color: var(--mood-muted);
|
||
background: var(--mood-surface);
|
||
}
|
||
.mwiz__pill--active {
|
||
background: var(--mood-accent);
|
||
color: var(--mood-accent-text);
|
||
}
|
||
.mwiz__pill--done {
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
}
|
||
|
||
/* Header */
|
||
.mwiz__header { margin-bottom: 2rem; }
|
||
.mwiz__title { font-size: clamp(1.375rem, 3vw, 1.75rem); font-weight: 800; color: var(--mood-text); margin-bottom: 0.375rem; }
|
||
.mwiz__subtitle { font-size: 0.9375rem; color: var(--mood-muted); }
|
||
|
||
/* Form */
|
||
.mwiz__form { display: flex; flex-direction: column; gap: 1.5rem; }
|
||
.mwiz__section { display: flex; flex-direction: column; gap: 0.5rem; }
|
||
.mwiz__label { font-size: 0.9375rem; font-weight: 600; color: var(--mood-text); }
|
||
.mwiz__label-sm { font-size: 0.8125rem; font-weight: 600; color: var(--mood-muted); }
|
||
.mwiz__req { color: var(--mood-accent); }
|
||
.mwiz__optional { font-weight: 400; font-size: 0.8125rem; color: var(--mood-muted); }
|
||
.mwiz__input {
|
||
width: 100%;
|
||
padding: 0.6875rem 1rem;
|
||
font-size: 0.9375rem;
|
||
color: var(--mood-text);
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 12px;
|
||
outline: none;
|
||
}
|
||
.mwiz__textarea {
|
||
width: 100%;
|
||
padding: 0.6875rem 1rem;
|
||
font-size: 0.9375rem;
|
||
color: var(--mood-text);
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 12px;
|
||
outline: none;
|
||
resize: vertical;
|
||
font-family: inherit;
|
||
}
|
||
|
||
/* Type grid */
|
||
.mwiz__type-grid {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mwiz__type-grid--sm .mwiz__type-btn { padding: 0.375rem 0.75rem; font-size: 0.875rem; }
|
||
.mwiz__type-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.5rem 1rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s, transform 0.1s;
|
||
}
|
||
.mwiz__type-btn:hover { transform: translateY(-1px); }
|
||
.mwiz__type-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
|
||
/* Duration */
|
||
.mwiz__dur-toggle {
|
||
display: inline-flex;
|
||
background: var(--mood-surface);
|
||
border-radius: 12px;
|
||
padding: 0.25rem;
|
||
gap: 0.25rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
.mwiz__dur-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
padding: 0.375rem 0.875rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s;
|
||
}
|
||
.mwiz__dur-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
.mwiz__dur-relative { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
|
||
.mwiz__dur-number {
|
||
width: 5rem;
|
||
padding: 0.5rem 0.75rem;
|
||
font-size: 1.125rem;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 10px;
|
||
outline: none;
|
||
color: var(--mood-text);
|
||
}
|
||
.mwiz__dur-dates { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
|
||
.mwiz__dur-date-field { display: flex; flex-direction: column; gap: 0.25rem; flex: 1; min-width: 140px; }
|
||
.mwiz__dur-arrow { color: var(--mood-muted); }
|
||
|
||
/* Submit */
|
||
.mwiz__submit {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.75rem;
|
||
font-size: 1rem;
|
||
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;
|
||
align-self: flex-start;
|
||
margin-top: 0.5rem;
|
||
}
|
||
.mwiz__submit:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 16px var(--mood-shadow); }
|
||
.mwiz__submit:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
/* Nomination cards */
|
||
.mwiz__nom-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem; }
|
||
@media (max-width: 480px) { .mwiz__nom-grid { grid-template-columns: 1fr; } }
|
||
.mwiz__nom-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.875rem;
|
||
padding: 1.25rem;
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
}
|
||
.mwiz__nom-card:hover { transform: translateY(-3px); box-shadow: 0 6px 20px var(--mood-shadow); }
|
||
.mwiz__nom-card--active { box-shadow: 0 0 0 2px var(--mood-accent), 0 6px 20px var(--mood-shadow); background: var(--mood-accent-soft); }
|
||
.mwiz__nom-icon { color: var(--mood-accent); }
|
||
.mwiz__nom-title { font-weight: 700; font-size: 1rem; color: var(--mood-text); margin-bottom: 0.25rem; }
|
||
.mwiz__nom-desc { font-size: 0.875rem; color: var(--mood-muted); line-height: 1.5; }
|
||
|
||
/* Params */
|
||
.mwiz__self-params, .mwiz__election-cta { display: flex; flex-direction: column; gap: 1rem; }
|
||
.mwiz__param-row { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||
.mwiz__param-field { display: flex; flex-direction: column; gap: 0.375rem; min-width: 120px; }
|
||
.mwiz__param-input-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
||
.mwiz__param-input {
|
||
width: 5rem;
|
||
padding: 0.375rem 0.5rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
background: var(--mood-accent-soft);
|
||
border-radius: 8px;
|
||
outline: none;
|
||
color: var(--mood-text);
|
||
}
|
||
.mwiz__param-unit { font-size: 0.8125rem; color: var(--mood-muted); white-space: nowrap; }
|
||
.mwiz__param-slider { flex: 1; min-width: 80px; accent-color: var(--mood-accent); }
|
||
|
||
/* Mode toggle (toolbox) */
|
||
.mwiz__mode-toggle {
|
||
display: inline-flex;
|
||
background: var(--mood-surface);
|
||
border-radius: 14px;
|
||
padding: 0.3125rem;
|
||
gap: 0.25rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.mwiz__mode-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.5rem 1.125rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.1s, color 0.1s;
|
||
}
|
||
.mwiz__mode-btn--active { background: var(--mood-accent); color: var(--mood-accent-text); }
|
||
|
||
/* Modalities */
|
||
.mwiz__modalities { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; }
|
||
.mwiz__modality {
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
padding: 1.125rem 1.25rem;
|
||
cursor: pointer;
|
||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
.mwiz__modality:hover { transform: translateY(-2px); box-shadow: 0 4px 14px var(--mood-shadow); }
|
||
.mwiz__modality--selected { border-left-color: var(--mood-accent); box-shadow: 0 4px 14px var(--mood-shadow); }
|
||
.mwiz__modality--recommended { border-left-color: var(--mood-success, var(--mood-accent)); }
|
||
.mwiz__modality--selected.mwiz__modality--recommended { border-left-color: var(--mood-accent); }
|
||
.mwiz__modality-head { display: flex; align-items: flex-start; gap: 0.875rem; }
|
||
.mwiz__modality-icon {
|
||
width: 2.5rem;
|
||
height: 2.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
border-radius: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.mwiz__modality-info { flex: 1; }
|
||
.mwiz__modality-title { font-weight: 700; font-size: 0.9375rem; color: var(--mood-text); margin-bottom: 0.25rem; }
|
||
.mwiz__modality-desc { font-size: 0.8125rem; color: var(--mood-muted); line-height: 1.5; }
|
||
.mwiz__modality-check { color: var(--mood-accent); margin-top: 0.125rem; }
|
||
.mwiz__modality-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
margin-top: 0.625rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: var(--mood-success, var(--mood-accent));
|
||
background: color-mix(in srgb, var(--mood-success, var(--mood-accent)) 12%, transparent);
|
||
border-radius: 12px;
|
||
padding: 0.1875rem 0.625rem;
|
||
}
|
||
.mwiz__modality-reason { font-size: 0.8125rem; color: var(--mood-muted); margin-top: 0.25rem; font-style: italic; }
|
||
.mwiz__modality-params {
|
||
margin-top: 1rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid var(--mood-border, rgba(0,0,0,0.06));
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
}
|
||
|
||
/* Recap */
|
||
.mwiz__recap { display: flex; flex-direction: column; gap: 1.25rem; margin-bottom: 2rem; }
|
||
.mwiz__recap-block {
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
padding: 1.25rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.375rem;
|
||
}
|
||
.mwiz__recap-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--mood-muted); margin-bottom: 0.25rem; }
|
||
.mwiz__recap-title { font-size: 1.125rem; font-weight: 800; color: var(--mood-text); }
|
||
.mwiz__recap-value { font-size: 0.9375rem; font-weight: 600; color: var(--mood-text); }
|
||
.mwiz__recap-meta { font-size: 0.875rem; color: var(--mood-muted); display: flex; align-items: center; gap: 0.375rem; }
|
||
|
||
/* Steps list */
|
||
.mwiz__steps-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.625rem; }
|
||
.mwiz__steps-item { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||
.mwiz__steps-num {
|
||
width: 1.5rem; height: 1.5rem;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 0.75rem; font-weight: 700;
|
||
background: var(--mood-accent-soft);
|
||
color: var(--mood-accent);
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
margin-top: 0.125rem;
|
||
}
|
||
.mwiz__steps-title { font-weight: 600; font-size: 0.875rem; color: var(--mood-text); }
|
||
.mwiz__steps-desc { font-size: 0.8125rem; color: var(--mood-muted); }
|
||
|
||
/* Recap actions */
|
||
.mwiz__recap-actions { display: flex; gap: 1rem; align-items: center; }
|
||
.mwiz__back-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.375rem;
|
||
padding: 0.625rem 1.25rem;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
color: var(--mood-muted);
|
||
background: var(--mood-surface);
|
||
border-radius: 16px;
|
||
cursor: pointer;
|
||
transition: transform 0.1s;
|
||
}
|
||
.mwiz__back-btn:hover { transform: translateY(-1px); }
|
||
.mwiz__create-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 1.75rem;
|
||
font-size: 1rem;
|
||
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;
|
||
}
|
||
.mwiz__create-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 16px var(--mood-shadow); }
|
||
.mwiz__create-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
.mwiz__error { color: var(--mood-danger, #e53e3e); font-size: 0.875rem; padding: 0.5rem 0; }
|
||
|
||
/* Transitions */
|
||
.slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.2s ease; }
|
||
.slide-fade-enter-from { opacity: 0; transform: translateX(20px); }
|
||
.slide-fade-leave-to { opacity: 0; transform: translateX(-20px); }
|
||
.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: 600px; }
|
||
</style>
|