forked from yvv/decision
v2 : les 14 écrans — le tour complet de la démocratie d'exercice
- Aujourd'hui (Fil 13 sections + capture sticky) + Le chemin (tunnel 2 gestes, Q0 inline, 3 chips, dérogation asymétrique, alternatives réglage/consignation) - Registre + fiche décision (timeline, périmètre premier/second lieu auditable, affluence non-ignorable, S'instruire condensé, éléments + cartographie de clôture, épreuve du réel, Remettre en question, PV A4, gravure) - Salle de vote 5 modalités (consentement, nuancé+histogramme, binaire hérité avec jauge inertielle, Réglage collectif complet — faisceau, médiane basse, Pour moi, Explorer, cristallisation-geste —, élection à départage humain) - Textes (Pacte en clair, document vivant, diff, vue projetée, Atelier des formules porté du v1 sur le moteur unique) + Mandats (faits comptés, feux de la rampe, wizard 3 étapes) + Observatoire (consigner→observer→protocoliser) - Onboarding 7 gabarits + Données locales (export/import, attributs, atelier) - voting→framing gardé (Reformuler d'un réglage figé non cristallisé) - Pages v1 supprimées (login, documents, mandates, protocols, sanctuary, tools, decisions/new) — 342 tests verts, build zéro erreur Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
// One line of the collective's activity — route icon, title → fiche, status pill.
|
||||
import type { Decision } from '~/types/domain'
|
||||
import { ROUTE_ICONS, STATUS_LABELS } from '~/lexicon'
|
||||
|
||||
const props = defineProps<{ decision: Decision }>()
|
||||
|
||||
const when = computed(() =>
|
||||
new Date(props.decision.updatedAt).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
}),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<NuxtLink :to="`/decisions/${decision.id}`" class="ld-card ld-card--hover fac">
|
||||
<UIcon
|
||||
:name="ROUTE_ICONS[decision.route]"
|
||||
class="fac__icon"
|
||||
:style="{ color: `var(--route-${decision.route})` }"
|
||||
/>
|
||||
<span class="fac__title">{{ decision.title }}</span>
|
||||
<span class="status-pill fac__pill" :class="`status-${decision.status}`">
|
||||
{{ STATUS_LABELS[decision.status] }}
|
||||
</span>
|
||||
<span class="fac__when">{{ when }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fac {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.625rem 0.875rem;
|
||||
text-decoration: none;
|
||||
color: var(--mood-text);
|
||||
}
|
||||
.fac__icon { font-size: 1rem; flex-shrink: 0; }
|
||||
.fac__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 0.9063rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fac__pill { flex-shrink: 0; }
|
||||
.fac__when {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--mood-text-muted);
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 480px) {
|
||||
.fac__when { display: inline; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
// Advice request card — three inline positions, one tap each.
|
||||
import type { Advice, Decision } from '~/types/domain'
|
||||
import { ROUTE_ICONS } from '~/lexicon'
|
||||
import { useCollectiveStore } from '~/stores/collective'
|
||||
import { useDecisionsStore } from '~/stores/decisions'
|
||||
|
||||
const props = defineProps<{ decision: Decision }>()
|
||||
|
||||
const col = useCollectiveStore()
|
||||
const decisions = useDecisionsStore()
|
||||
const error = ref('')
|
||||
|
||||
const POSITIONS: { value: Advice['position']; label: string; icon: string }[] = [
|
||||
{ value: 'favorable', label: 'Favorable', icon: 'i-lucide-thumbs-up' },
|
||||
{ value: 'reserved', label: 'Réservé', icon: 'i-lucide-minus' },
|
||||
{ value: 'unfavorable', label: 'Défavorable', icon: 'i-lucide-thumbs-down' },
|
||||
]
|
||||
|
||||
const mine = computed(() =>
|
||||
col.advices.find(a => a.decisionId === props.decision.id && a.personId === col.me?.id),
|
||||
)
|
||||
const mineLabel = computed(
|
||||
() => POSITIONS.find(p => p.value === mine.value?.position)?.label ?? '',
|
||||
)
|
||||
|
||||
function give(position: Advice['position']) {
|
||||
const res = decisions.adviseOn(props.decision.id, position)
|
||||
error.value = 'ok' in res ? res.reason : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<FeedItemCard
|
||||
:title="decision.title"
|
||||
:icon="ROUTE_ICONS.advice"
|
||||
tint="var(--route-advice)"
|
||||
:to="`/decisions/${decision.id}`"
|
||||
>
|
||||
<template #meta>
|
||||
<LdCountdown :ends-at="decision.windowEndsAt" :suspended-at="decision.windowSuspendedAt" />
|
||||
</template>
|
||||
|
||||
<p v-if="error" class="fa-error">{{ error }}</p>
|
||||
|
||||
<template #actions>
|
||||
<span v-if="mine" class="fa-done">
|
||||
<UIcon name="i-lucide-check" /> Ton avis est déposé — {{ mineLabel }}
|
||||
</span>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="p in POSITIONS"
|
||||
:key="p.value"
|
||||
class="ld-btn ld-btn--ghost fa-btn"
|
||||
type="button"
|
||||
@click="give(p.value)"
|
||||
>
|
||||
<UIcon :name="p.icon" />
|
||||
<span>{{ p.label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</FeedItemCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fa-error { margin: 0; font-size: 0.8125rem; color: var(--mood-error); }
|
||||
.fa-done {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-status-vigueur);
|
||||
}
|
||||
.fa-btn { font-size: 0.875rem; padding: 0.4rem 0.875rem; }
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
// Sticky capture bar — the ONE entry of every decision (gesture 1 of 2).
|
||||
// Enter → /decider?titre=… ; nothing else is mandatory.
|
||||
import { CAPTURE_PLACEHOLDER } from '~/lexicon'
|
||||
|
||||
const text = ref('')
|
||||
|
||||
function go() {
|
||||
const title = text.value.trim()
|
||||
if (title.length === 0) return
|
||||
navigateTo({ path: '/decider', query: { titre: title } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<div class="feed-capture">
|
||||
<div class="ld-card feed-capture__card">
|
||||
<UIcon name="i-lucide-zap" class="feed-capture__icon" />
|
||||
<input
|
||||
v-model="text"
|
||||
type="text"
|
||||
class="feed-capture__input"
|
||||
:placeholder="CAPTURE_PLACEHOLDER"
|
||||
:aria-label="CAPTURE_PLACEHOLDER"
|
||||
enterkeyhint="go"
|
||||
@keydown.enter.prevent="go"
|
||||
>
|
||||
<button
|
||||
v-if="text.trim().length > 0"
|
||||
class="ld-btn feed-capture__go"
|
||||
type="button"
|
||||
@click="go"
|
||||
>
|
||||
<UIcon name="i-lucide-arrow-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feed-capture {
|
||||
position: sticky;
|
||||
top: 3.5rem;
|
||||
z-index: 20;
|
||||
padding: 0.5rem 0 0.75rem;
|
||||
background: var(--mood-bg);
|
||||
}
|
||||
.feed-capture__card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0.625rem 0.5rem 1rem;
|
||||
}
|
||||
.feed-capture__icon {
|
||||
font-size: 1.125rem;
|
||||
color: var(--mood-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.feed-capture__input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: none;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
.feed-capture__input:focus-visible { box-shadow: none; }
|
||||
.feed-capture__input::placeholder {
|
||||
color: var(--mood-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.feed-capture__go {
|
||||
padding: 0.5rem 0.875rem;
|
||||
min-height: 2.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
// Generic compact card of the Fil: icon container, title (optionally a link),
|
||||
// meta line (slot #meta), body (default slot), inline actions (slot #actions).
|
||||
defineProps<{
|
||||
title: string
|
||||
icon?: string
|
||||
tint?: string
|
||||
to?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<article class="ld-card feed-item">
|
||||
<div v-if="icon" class="feed-item__stamp" :style="tint ? { color: tint } : undefined">
|
||||
<UIcon :name="icon" />
|
||||
</div>
|
||||
<div class="feed-item__body">
|
||||
<NuxtLink v-if="to" :to="to" class="feed-item__title feed-item__title--link">
|
||||
{{ title }}
|
||||
</NuxtLink>
|
||||
<span v-else class="feed-item__title">{{ title }}</span>
|
||||
<div v-if="$slots.meta" class="feed-item__meta">
|
||||
<slot name="meta" />
|
||||
</div>
|
||||
<slot />
|
||||
<div v-if="$slots.actions" class="feed-item__actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feed-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 1rem;
|
||||
}
|
||||
.feed-item__stamp {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: var(--r-icon);
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
font-size: 1.125rem;
|
||||
color: var(--mood-accent);
|
||||
}
|
||||
.feed-item__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.feed-item__title {
|
||||
font-size: 0.9688rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: var(--mood-text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.feed-item__title--link { text-decoration: none; }
|
||||
.feed-item__title--link:hover { color: var(--mood-accent); }
|
||||
.feed-item__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
.feed-item__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
// « L'épreuve du réel » card — « Le réel a-t-il suivi ? », three inline answers.
|
||||
import type { Decision } from '~/types/domain'
|
||||
import { REVIEW_QUESTION, REVIEW_VERDICTS } from '~/lexicon'
|
||||
import { useDecisionsStore } from '~/stores/decisions'
|
||||
|
||||
const props = defineProps<{ decision: Decision }>()
|
||||
|
||||
const decisions = useDecisionsStore()
|
||||
const error = ref('')
|
||||
const answered = ref<'confirmed' | 'revise' | 'revoke' | null>(null)
|
||||
|
||||
// Aliased: the lexicon export name is a code identifier, kept out of templates.
|
||||
const ANSWER_LABELS = REVIEW_VERDICTS
|
||||
const ANSWERS = [
|
||||
{ value: 'confirmed', icon: 'i-lucide-check' },
|
||||
{ value: 'revise', icon: 'i-lucide-pencil' },
|
||||
{ value: 'revoke', icon: 'i-lucide-undo-2' },
|
||||
] as const
|
||||
|
||||
function answer(value: 'confirmed' | 'revise' | 'revoke') {
|
||||
const res = decisions.reviewVerdict(props.decision.id, value)
|
||||
if ('ok' in res) {
|
||||
error.value = res.reason
|
||||
return
|
||||
}
|
||||
error.value = ''
|
||||
answered.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<FeedItemCard
|
||||
:title="decision.title"
|
||||
icon="i-lucide-flask-conical"
|
||||
tint="var(--mood-tertiary)"
|
||||
:to="`/decisions/${decision.id}`"
|
||||
>
|
||||
<template #meta>
|
||||
<span class="fr-question">{{ REVIEW_QUESTION }}</span>
|
||||
</template>
|
||||
|
||||
<p v-if="error" class="fr-error">{{ error }}</p>
|
||||
|
||||
<template #actions>
|
||||
<span v-if="answered" class="fr-done">
|
||||
<UIcon name="i-lucide-check" /> {{ ANSWER_LABELS[answered] }} — c'est noté
|
||||
</span>
|
||||
<template v-else>
|
||||
<button
|
||||
v-for="a in ANSWERS"
|
||||
:key="a.value"
|
||||
class="ld-btn ld-btn--ghost fr-btn"
|
||||
type="button"
|
||||
@click="answer(a.value)"
|
||||
>
|
||||
<UIcon :name="a.icon" />
|
||||
<span>{{ ANSWER_LABELS[a.value] }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</FeedItemCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fr-question { font-weight: 700; font-style: italic; color: var(--mood-tertiary, var(--mood-accent)); }
|
||||
.fr-error { margin: 0; font-size: 0.8125rem; color: var(--mood-error); }
|
||||
.fr-done {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-status-vigueur);
|
||||
}
|
||||
.fr-btn { font-size: 0.875rem; padding: 0.4rem 0.875rem; }
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
// One section of the Fil — header + stacked items. Empty sections are hidden
|
||||
// by the PARENT (v-if on length), never here.
|
||||
defineProps<{
|
||||
title: string
|
||||
icon?: string
|
||||
discrete?: boolean
|
||||
priority?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<section
|
||||
class="feed-section"
|
||||
:class="{ 'feed-section--discrete': discrete, 'feed-section--priority': priority }"
|
||||
>
|
||||
<h2 class="feed-section__title">
|
||||
<UIcon v-if="icon" :name="icon" class="feed-section__icon" />
|
||||
<span>{{ title }}</span>
|
||||
</h2>
|
||||
<div class="feed-section__items">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.feed-section { margin-bottom: 1.75rem; }
|
||||
.feed-section__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 0.625rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--mood-text-muted);
|
||||
}
|
||||
.feed-section--priority .feed-section__title { color: var(--mood-status-fenetre); }
|
||||
.feed-section--discrete .feed-section__title {
|
||||
font-size: 0.8125rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.feed-section__icon { font-size: 1rem; }
|
||||
.feed-section__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
.feed-section--discrete .feed-section__items { opacity: 0.85; }
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
// Open vote card — miniature participation gauge + link to the vote room.
|
||||
// The gauge shows who already spoke, never a result (results live in the room).
|
||||
import type { Decision, VoteSession } from '~/types/domain'
|
||||
import { ROUTE_ICONS } from '~/lexicon'
|
||||
import { useCollectiveStore } from '~/stores/collective'
|
||||
import { useDecisionsStore } from '~/stores/decisions'
|
||||
|
||||
const props = defineProps<{ session: VoteSession; decision?: Decision }>()
|
||||
|
||||
const col = useCollectiveStore()
|
||||
const decisions = useDecisionsStore()
|
||||
|
||||
const voted = computed(() => decisions.activeVotes(props.session.id).length)
|
||||
const total = computed(() => Math.max(props.session.corpusSize, 1))
|
||||
const pct = computed(() => Math.min(100, Math.round((voted.value / total.value) * 100)))
|
||||
const iVoted = computed(() =>
|
||||
decisions.activeVotes(props.session.id).some(v => v.voterId === col.me?.id),
|
||||
)
|
||||
const title = computed(() => props.decision?.title ?? 'Décision en vote')
|
||||
const roomLink = computed(() =>
|
||||
props.decision ? `/decisions/${props.decision.id}/vote` : undefined,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<FeedItemCard
|
||||
:title="title"
|
||||
:icon="ROUTE_ICONS.collective"
|
||||
tint="var(--route-collective)"
|
||||
:to="roomLink"
|
||||
>
|
||||
<template #meta>
|
||||
<LdCountdown :ends-at="session.closesAt" />
|
||||
<span>{{ voted }} / {{ total }} se sont prononcés</span>
|
||||
</template>
|
||||
|
||||
<div class="fv-gauge" role="img" :aria-label="`Participation : ${pct} %`">
|
||||
<div class="fv-gauge__fill" :style="{ width: pct + '%' }" />
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NuxtLink v-if="roomLink" :to="roomLink" class="ld-btn fv-btn">
|
||||
<UIcon name="i-lucide-door-open" />
|
||||
<span>{{ iVoted ? 'Revoir mon vote' : 'Entrer dans la salle' }}</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</FeedItemCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fv-gauge {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--mood-status-vote-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.fv-gauge__fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: var(--mood-status-vote);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.fv-btn { font-size: 0.875rem; padding: 0.4rem 1rem; text-decoration: none; }
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
// Objection window card — « Ça me va » creates an Assent; « J'objecte » unfolds
|
||||
// a small argument form (content or boundary). The countdown pauses when the
|
||||
// boundary is contested; non-easy windows show the explicit-agreement mention.
|
||||
import type { Decision } from '~/types/domain'
|
||||
import { ASSENT_MISSING, ROUTE_ICONS, WINDOW_OBJECT, WINDOW_OK } from '~/lexicon'
|
||||
import { useCollectiveStore } from '~/stores/collective'
|
||||
import { useDecisionsStore } from '~/stores/decisions'
|
||||
|
||||
const props = defineProps<{ decision: Decision }>()
|
||||
|
||||
const col = useCollectiveStore()
|
||||
const decisions = useDecisionsStore()
|
||||
|
||||
const objecting = ref(false)
|
||||
const argument = ref('')
|
||||
const error = ref('')
|
||||
|
||||
const myAssent = computed(() =>
|
||||
col.assents.some(a => a.decisionId === props.decision.id && a.personId === col.me?.id),
|
||||
)
|
||||
const myObjection = computed(() =>
|
||||
col.objections.some(
|
||||
o => o.decisionId === props.decision.id && o.personId === col.me?.id && o.status === 'open',
|
||||
),
|
||||
)
|
||||
/** Non-easy windows adopt only on an explicit third-party agreement. */
|
||||
const needsExplicitAssent = computed(
|
||||
() =>
|
||||
props.decision.reversibility !== 'easy'
|
||||
&& !col.assents.some(
|
||||
a => a.decisionId === props.decision.id && a.personId !== props.decision.authorId,
|
||||
),
|
||||
)
|
||||
|
||||
function agree() {
|
||||
const res = decisions.assentTo(props.decision.id)
|
||||
error.value = 'ok' in res ? res.reason : ''
|
||||
}
|
||||
|
||||
function sendObjection(kind: 'content' | 'boundary') {
|
||||
const res = decisions.objectTo(props.decision.id, kind, argument.value)
|
||||
if ('ok' in res) {
|
||||
error.value = res.reason
|
||||
return
|
||||
}
|
||||
error.value = ''
|
||||
argument.value = ''
|
||||
objecting.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ld-v2 -->
|
||||
<FeedItemCard
|
||||
:title="decision.title"
|
||||
:icon="ROUTE_ICONS[decision.route]"
|
||||
:tint="`var(--route-${decision.route})`"
|
||||
:to="`/decisions/${decision.id}`"
|
||||
>
|
||||
<template #meta>
|
||||
<LdCountdown :ends-at="decision.windowEndsAt" :suspended-at="decision.windowSuspendedAt" />
|
||||
<span v-if="needsExplicitAssent" class="fw-mention">{{ ASSENT_MISSING }}</span>
|
||||
</template>
|
||||
|
||||
<p v-if="error" class="fw-error">{{ error }}</p>
|
||||
|
||||
<template #actions>
|
||||
<template v-if="myAssent">
|
||||
<span class="fw-done"><UIcon name="i-lucide-check" /> Ton accord est noté</span>
|
||||
</template>
|
||||
<template v-else-if="myObjection">
|
||||
<span class="fw-done"><UIcon name="i-lucide-hand" /> Ton objection est déposée</span>
|
||||
</template>
|
||||
<template v-else-if="!objecting">
|
||||
<button class="ld-btn fw-btn" type="button" @click="agree">{{ WINDOW_OK }}</button>
|
||||
<button class="ld-btn ld-btn--ghost fw-btn" type="button" @click="objecting = true">
|
||||
{{ WINDOW_OBJECT }}
|
||||
</button>
|
||||
</template>
|
||||
<div v-else class="fw-form">
|
||||
<textarea
|
||||
v-model="argument"
|
||||
class="fw-form__text"
|
||||
rows="2"
|
||||
placeholder="Une objection s'argumente — écris pourquoi."
|
||||
/>
|
||||
<div class="fw-form__row">
|
||||
<button class="ld-btn fw-btn" type="button" @click="sendObjection('content')">
|
||||
Sur le fond
|
||||
</button>
|
||||
<button class="ld-btn ld-btn--ghost fw-btn" type="button" @click="sendObjection('boundary')">
|
||||
Sur la frontière
|
||||
</button>
|
||||
<button class="ld-btn ld-btn--quiet fw-btn" type="button" @click="objecting = false">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FeedItemCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fw-mention {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--mood-status-fenetre);
|
||||
}
|
||||
.fw-error { margin: 0; font-size: 0.8125rem; color: var(--mood-error); }
|
||||
.fw-done {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--mood-status-vigueur);
|
||||
}
|
||||
.fw-btn { font-size: 0.875rem; padding: 0.4rem 1rem; }
|
||||
.fw-form { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; }
|
||||
.fw-form__text { width: 100%; padding: 0.625rem 0.75rem; font-size: 0.9375rem; resize: vertical; }
|
||||
.fw-form__row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user