Files
decision/frontend/app/components/documents/EngagementCard.vue
Yvv 21ceae4866 Français soigné, labels signalétiques, formule N1.1 visuelle, F1.2 lisible
- Accents français partout (seed + composants Vue)
- Labels discrets: Engagements, Préambule, Application, Variables
- N1.1: présentation visuelle des niveaux d'inertie avec formule
- F1.2: paramètres + lecture du curseur d'inertie
- MarkdownRenderer: espacement resserré, support code inline
- Toutes descriptions et meta en bon français

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:29:50 +01:00

301 lines
7.1 KiB
Vue

<script setup lang="ts">
/**
* EngagementCard — Enhanced item card with inline mini vote board,
* inertia indicator, and action buttons.
*
* Replaces the basic ItemCard for the document detail view.
*/
import type { DocumentItem } from '~/stores/documents'
const props = withDefaults(defineProps<{
item: DocumentItem
documentSlug: string
showActions?: boolean
showVoteBoard?: boolean
}>(), {
showActions: false,
showVoteBoard: true,
})
const emit = defineEmits<{
propose: [item: DocumentItem]
}>()
const isSection = computed(() => props.item.item_type === 'section')
const isPreamble = computed(() => props.item.item_type === 'preamble')
const itemTypeIcon = computed(() => {
switch (props.item.item_type) {
case 'clause': return 'i-lucide-shield-check'
case 'rule': return 'i-lucide-scale'
case 'verification': return 'i-lucide-check-circle'
case 'preamble': return 'i-lucide-scroll-text'
case 'section': return 'i-lucide-layout-list'
default: return 'i-lucide-file-text'
}
})
const itemTypeLabel = computed(() => {
switch (props.item.item_type) {
case 'clause': return 'Engagement'
case 'rule': return 'Variables'
case 'verification': return 'Application'
case 'preamble': return 'Préambule'
case 'section': return 'Titre'
default: return props.item.item_type
}
})
// Mock vote data varies by item for demo — items in "bonnes pratiques" (E8-E11) get lower/mixed votes
const mockVotes = computed(() => {
const order = props.item.sort_order
const pos = props.item.position
// Conseils et bonnes pratiques: varied votes, some non-adopted
if (pos === 'E8') return { votesFor: 4, votesAgainst: 3 } // contested
if (pos === 'E9') return { votesFor: 2, votesAgainst: 5 } // rejected
if (pos === 'E10') return { votesFor: 6, votesAgainst: 2 } // borderline
if (pos === 'E11') return { votesFor: 3, votesAgainst: 4 } // rejected
// Default: well-adopted items
const base = ((order * 7 + 13) % 5) + 8 // 8-12
const against = (order % 3) // 0-2
return { votesFor: base, votesAgainst: against }
})
function navigateToItem() {
navigateTo(`/documents/${props.documentSlug}/items/${props.item.id}`)
}
</script>
<template>
<!-- Section header (visual separator, not a card) -->
<div v-if="isSection" class="engagement-section">
<div class="engagement-section__line" />
<div class="engagement-section__content">
<h3 class="engagement-section__title">
{{ item.title }}
</h3>
<p class="engagement-section__text">
{{ item.current_text }}
</p>
<InertiaSlider
:preset="item.inertia_preset"
compact
class="mt-2 max-w-xs"
/>
</div>
</div>
<!-- Regular item card -->
<div
v-else
class="engagement-card"
:class="{
'engagement-card--preamble': isPreamble,
}"
>
<!-- Card header -->
<div class="engagement-card__header" @click="navigateToItem">
<div class="flex items-center gap-2.5 min-w-0">
<div class="engagement-card__position">
{{ item.position }}
</div>
<UIcon :name="itemTypeIcon" class="text-sm shrink-0" style="color: var(--mood-accent)" />
<span v-if="item.title" class="engagement-card__title">
{{ item.title }}
</span>
<span class="engagement-card__type-label">
{{ itemTypeLabel }}
</span>
</div>
</div>
<!-- Item text -->
<div class="engagement-card__body" @click="navigateToItem">
<MarkdownRenderer :content="item.current_text" />
</div>
<!-- Mini vote board -->
<div v-if="showVoteBoard" class="engagement-card__vote">
<MiniVoteBoard
:votes-for="mockVotes.votesFor"
:votes-against="mockVotes.votesAgainst"
:wot-size="7224"
:is-permanent="item.is_permanent_vote"
:inertia-preset="item.inertia_preset"
/>
</div>
<!-- Inertia indicator -->
<div class="engagement-card__inertia">
<InertiaSlider
:preset="item.inertia_preset"
compact
/>
</div>
<!-- Actions -->
<div v-if="showActions" class="engagement-card__actions">
<UButton
label="Proposer une alternative"
icon="i-lucide-pen-line"
variant="soft"
color="primary"
size="xs"
@click.stop="emit('propose', item)"
/>
<UButton
label="Voter"
icon="i-lucide-vote"
variant="soft"
color="success"
size="xs"
@click.stop="navigateToItem"
/>
</div>
</div>
</template>
<style scoped>
/* Section separator */
.engagement-section {
display: flex;
gap: 1rem;
padding: 1.5rem 0 0.5rem;
}
.engagement-section__line {
width: 4px;
background: var(--mood-accent);
border-radius: 2px;
flex-shrink: 0;
}
.engagement-section__content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.engagement-section__title {
font-size: 1.125rem;
font-weight: 800;
color: var(--mood-text);
letter-spacing: -0.01em;
}
.engagement-section__text {
font-size: 0.8125rem;
color: var(--mood-text-muted);
line-height: 1.5;
}
/* Card */
.engagement-card {
display: flex;
flex-direction: column;
background: var(--mood-surface);
border-radius: 14px;
overflow: hidden;
transition: box-shadow 0.15s, transform 0.15s;
}
.engagement-card:hover {
box-shadow: 0 2px 12px color-mix(in srgb, var(--mood-accent) 12%, transparent);
transform: translateY(-1px);
}
.engagement-card--preamble {
border-left: 4px solid color-mix(in srgb, var(--mood-accent) 40%, transparent);
}
.engagement-card__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1rem 0;
cursor: pointer;
}
.engagement-card__position {
display: flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 1.625rem;
padding: 0 0.5rem;
border-radius: 8px;
background: var(--mood-accent);
color: white;
font-size: 0.6875rem;
font-weight: 800;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.engagement-card__title {
font-size: 0.875rem;
font-weight: 700;
color: var(--mood-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.engagement-card__type-label {
font-size: 0.5625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--mood-text-muted);
opacity: 0.6;
flex-shrink: 0;
}
.engagement-card__body {
padding: 0.75rem 1rem 1rem;
cursor: pointer;
font-size: 0.9375rem;
line-height: 1.7;
color: var(--mood-text);
}
@media (min-width: 640px) {
.engagement-card__body {
font-size: 1rem;
line-height: 1.75;
}
}
.engagement-card__vote {
padding: 0 1rem;
opacity: 0.7;
transform: scale(0.92);
transform-origin: left center;
transition: opacity 0.2s;
}
.engagement-card:hover .engagement-card__vote {
opacity: 1;
}
.engagement-card__inertia {
padding: 0.375rem 1rem;
opacity: 0.6;
transition: opacity 0.2s;
}
.engagement-card:hover .engagement-card__inertia {
opacity: 1;
}
.engagement-card__actions {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
border-top: 1px solid color-mix(in srgb, var(--mood-text) 6%, transparent);
}
</style>