v2 : nettoyage des orphelins v1 + documentation livrable

- 43 fichiers v1 supprimés (composants documents/protocols/sanctuary/toolbox,
  stores auth/documents/groups/mandates/organizations/protocols/votes,
  composables api/notifications/formula/websocket, utils doublons du moteur)
- nuxt.config épuré (polkadot retiré, KaTeX et apiBase gardés), meta v2
- README.md, CONTRIBUTING.md, CLAUDE.md réécrits pour la v2
- Build zéro erreur, 342/342 tests verts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-08-11 14:09:05 +02:00
co-authored by Claude Fable 5
parent e164b5f6c1
commit 7cfc7ea352
49 changed files with 236 additions and 8885 deletions
@@ -1,51 +0,0 @@
<script setup lang="ts">
const props = defineProps<{
diff: string
}>()
interface DiffLine {
text: string
type: 'added' | 'removed' | 'header' | 'context'
}
const parsedLines = computed((): DiffLine[] => {
if (!props.diff) return []
return props.diff.split('\n').map((line) => {
if (line.startsWith('@@')) {
return { text: line, type: 'header' as const }
}
if (line.startsWith('+')) {
return { text: line, type: 'added' as const }
}
if (line.startsWith('-')) {
return { text: line, type: 'removed' as const }
}
return { text: line, type: 'context' as const }
})
})
function lineClass(type: DiffLine['type']): string {
switch (type) {
case 'added':
return 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-300'
case 'removed':
return 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-300'
case 'header':
return 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-semibold'
default:
return 'text-gray-700 dark:text-gray-300'
}
}
</script>
<template>
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<pre class="text-xs font-mono leading-relaxed overflow-x-auto"><template
v-for="(line, index) in parsedLines"
:key="index"
><div
:class="['px-4 py-0.5', lineClass(line.type)]"
>{{ line.text }}</div></template></pre>
</div>
</template>
@@ -1,70 +0,0 @@
<script setup lang="ts">
/**
* Error boundary component.
*
* Wraps slot content with NuxtErrorBoundary and displays a user-friendly
* error message in French when child components crash.
* Logs error details to console and emits error event for monitoring.
*/
const emit = defineEmits<{
error: [error: any]
}>()
const hasError = ref(false)
const errorDetails = ref<string | null>(null)
function handleError(error: any) {
hasError.value = true
errorDetails.value = error?.message || error?.toString() || 'Erreur inconnue'
// Log to console for debugging
console.error('[ErrorBoundary] Erreur capturee:', error)
// Emit for external monitoring
emit('error', error)
}
function retry() {
hasError.value = false
errorDetails.value = null
}
</script>
<template>
<NuxtErrorBoundary @error="handleError">
<template v-if="!hasError">
<slot />
</template>
<template v-else>
<div class="flex flex-col items-center justify-center p-8 text-center">
<div class="max-w-md space-y-4">
<UIcon
name="i-lucide-alert-triangle"
class="text-5xl text-warning mx-auto"
/>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Une erreur est survenue
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
Un probleme inattendu s'est produit lors du chargement de ce contenu.
Vous pouvez essayer de recharger cette section.
</p>
<p
v-if="errorDetails"
class="text-xs text-gray-400 dark:text-gray-500 font-mono bg-gray-100 dark:bg-gray-800 p-2 rounded"
>
{{ errorDetails }}
</p>
<UButton
icon="i-lucide-refresh-cw"
label="Reessayer"
color="primary"
variant="soft"
@click="retry"
/>
</div>
</div>
</template>
</NuxtErrorBoundary>
</template>
@@ -1,81 +0,0 @@
<script setup lang="ts">
/**
* Reusable skeleton loader component.
*
* Provides multiple skeleton variants for loading states:
* - Card: card layout with title and content lines
* - List: multiple rows with optional avatar
* - Detail: detailed view with mixed content
*
* Uses Nuxt UI USkeleton components.
*/
const props = withDefaults(
defineProps<{
/** Number of skeleton lines to display (default: 3). */
lines?: number
/** Show an avatar circle placeholder. */
avatar?: boolean
/** Render as a card skeleton with border and padding. */
card?: boolean
}>(),
{
lines: 3,
avatar: false,
card: false,
},
)
/** Generate varying line widths for a natural appearance. */
const lineWidths = computed(() => {
const widths = ['w-full', 'w-3/4', 'w-5/6', 'w-2/3', 'w-4/5']
return Array.from({ length: props.lines }, (_, i) => widths[i % widths.length])
})
</script>
<template>
<!-- Card variant -->
<UCard v-if="card" class="animate-pulse">
<div class="space-y-4">
<!-- Optional avatar row -->
<div v-if="avatar" class="flex items-center gap-3">
<USkeleton class="h-10 w-10 rounded-full" />
<div class="flex-1 space-y-2">
<USkeleton class="h-4 w-1/3" />
<USkeleton class="h-3 w-1/4" />
</div>
</div>
<!-- Title line -->
<USkeleton class="h-5 w-2/3" />
<!-- Content lines -->
<div class="space-y-2">
<USkeleton
v-for="(width, i) in lineWidths"
:key="i"
class="h-3"
:class="width"
/>
</div>
</div>
</UCard>
<!-- List / default variant -->
<div v-else class="space-y-3 animate-pulse">
<div
v-for="(width, i) in lineWidths"
:key="i"
class="flex items-center gap-3"
>
<!-- Optional avatar per line -->
<USkeleton v-if="avatar" class="h-8 w-8 rounded-full flex-shrink-0" />
<!-- Line content -->
<div class="flex-1 space-y-1">
<USkeleton class="h-3" :class="width" />
<USkeleton v-if="i === 0" class="h-2 w-1/4" />
</div>
</div>
</div>
</template>
@@ -1,53 +0,0 @@
<script setup lang="ts">
const { currentMood, moods, setMood } = useLibreMood()
</script>
<template>
<div class="mood-switcher" role="radiogroup" aria-label="Ambiance visuelle">
<UTooltip
v-for="mood in moods"
:key="mood.id"
:text="mood.label"
>
<button
type="button"
role="radio"
:aria-checked="currentMood === mood.id"
:aria-label="`Ambiance ${mood.label}`"
class="mood-dot"
:class="{ 'mood-dot--active': currentMood === mood.id }"
:style="{ '--dot-color': mood.color }"
@click="setMood(mood.id)"
/>
</UTooltip>
</div>
</template>
<style scoped>
.mood-switcher {
display: flex;
align-items: center;
gap: 0.375rem;
}
.mood-dot {
width: 1rem;
height: 1rem;
border-radius: 50%;
border: 2px solid transparent;
background: var(--dot-color, var(--mood-text-muted));
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
}
.mood-dot:hover {
transform: scale(1.25);
}
.mood-dot--active {
border-color: var(--mood-text);
box-shadow: 0 0 0 2px var(--mood-bg), 0 0 0 3px var(--mood-text-muted);
transform: scale(1.15);
}
</style>
@@ -1,95 +0,0 @@
<script setup lang="ts">
/**
* Offline detection banner.
*
* Uses navigator.onLine and online/offline events to detect
* network connectivity changes. Shows a warning banner when
* offline and a brief success message when back online.
*/
const isOnline = ref(true)
const showReconnected = ref(false)
let reconnectedTimer: ReturnType<typeof setTimeout> | null = null
function handleOffline() {
isOnline.value = false
showReconnected.value = false
if (reconnectedTimer) {
clearTimeout(reconnectedTimer)
reconnectedTimer = null
}
}
function handleOnline() {
isOnline.value = true
showReconnected.value = true
// Show "reconnected" message briefly then hide
reconnectedTimer = setTimeout(() => {
showReconnected.value = false
reconnectedTimer = null
}, 3000)
}
onMounted(() => {
isOnline.value = navigator.onLine
window.addEventListener('offline', handleOffline)
window.addEventListener('online', handleOnline)
})
onUnmounted(() => {
window.removeEventListener('offline', handleOffline)
window.removeEventListener('online', handleOnline)
if (reconnectedTimer) {
clearTimeout(reconnectedTimer)
}
})
</script>
<template>
<Transition name="slide-down">
<div
v-if="!isOnline"
class="bg-warning-100 dark:bg-warning-900/50 border-b border-warning-300 dark:border-warning-700 px-4 py-2 text-center"
role="alert"
>
<div class="flex items-center justify-center gap-2 text-sm text-warning-800 dark:text-warning-200">
<UIcon name="i-lucide-wifi-off" class="text-lg flex-shrink-0" />
<span>Vous etes hors ligne. Certaines fonctionnalites sont indisponibles.</span>
</div>
</div>
</Transition>
<Transition name="slide-down">
<div
v-if="showReconnected && isOnline"
class="bg-success-100 dark:bg-success-900/50 border-b border-success-300 dark:border-success-700 px-4 py-2 text-center"
role="status"
>
<div class="flex items-center justify-center gap-2 text-sm text-success-800 dark:text-success-200">
<UIcon name="i-lucide-wifi" class="text-lg flex-shrink-0" />
<span>Connexion retablie</span>
</div>
</div>
</Transition>
</template>
<style scoped>
.slide-down-enter-active,
.slide-down-leave-active {
transition: all 0.3s ease;
}
.slide-down-enter-from,
.slide-down-leave-to {
transform: translateY(-100%);
opacity: 0;
}
.slide-down-enter-to,
.slide-down-leave-from {
transform: translateY(0);
opacity: 1;
}
</style>
@@ -1,73 +0,0 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
status: string
type?: 'document' | 'decision' | 'mandate' | 'vote' | 'version'
clickable?: boolean
active?: boolean
}>(), {
clickable: true,
active: false,
})
const emit = defineEmits<{
click: []
}>()
const STATUS_MAP: Record<string, { label: string; cssClass: string }> = {
// Universal statuses
draft: { label: 'En prepa', cssClass: 'status-prepa' },
active: { label: 'En vigueur', cssClass: 'status-vigueur' },
closed: { label: 'Clos', cssClass: 'status-clos' },
// Decision/vote specific
qualification: { label: 'En prepa', cssClass: 'status-prepa' },
review: { label: 'En prepa', cssClass: 'status-prepa' },
voting: { label: 'En vote', cssClass: 'status-vote' },
open: { label: 'En vote', cssClass: 'status-vote' },
executed: { label: 'En vigueur', cssClass: 'status-vigueur' },
// Version specific
pending: { label: 'En prepa', cssClass: 'status-prepa' },
accepted: { label: 'En vigueur', cssClass: 'status-vigueur' },
rejected: { label: 'Clos', cssClass: 'status-clos' },
// Mandate specific
formulation: { label: 'En prepa', cssClass: 'status-prepa' },
candidature: { label: 'En prepa', cssClass: 'status-prepa' },
candidacy: { label: 'En prepa', cssClass: 'status-prepa' },
investiture: { label: 'En vote', cssClass: 'status-vote' },
revoked: { label: 'Clos', cssClass: 'status-clos' },
completed: { label: 'Clos', cssClass: 'status-clos' },
archived: { label: 'Clos', cssClass: 'status-clos' },
reporting: { label: 'En vote', cssClass: 'status-vote' },
}
const resolved = computed(() => {
return STATUS_MAP[props.status] ?? { label: props.status, cssClass: 'status-prepa' }
})
function handleClick() {
if (props.clickable) {
emit('click')
}
}
</script>
<template>
<button
v-if="clickable"
type="button"
class="status-pill"
:class="[resolved.cssClass, { active: active }]"
@click="handleClick"
>
{{ resolved.label }}
</button>
<span
v-else
class="status-pill"
:class="[resolved.cssClass]"
>
{{ resolved.label }}
</span>
</template>
@@ -1,183 +0,0 @@
<script setup lang="ts">
/**
* ToolboxVignette — Carte compacte, collapsible, bullet points + actions.
*/
export interface ToolboxAction {
label: string
icon?: string
to?: string
emit?: string
primary?: boolean
}
const props = withDefaults(
defineProps<{
title: string
bullets?: string[]
actions?: ToolboxAction[]
defaultOpen?: boolean
}>(),
{
bullets: undefined,
actions: undefined,
defaultOpen: false,
},
)
const emit = defineEmits<{
action: [actionEmit: string]
}>()
const open = ref(props.defaultOpen)
const defaultActions: ToolboxAction[] = [
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
{ label: 'Formules', icon: 'i-lucide-calculator', emit: 'formules' },
{ label: 'Demarrer', icon: 'i-lucide-play', emit: 'demarrer', primary: true },
]
const resolvedActions = computed(() => props.actions ?? defaultActions)
function handleAction(action: ToolboxAction) {
if (action.to) {
navigateTo(action.to)
}
else if (action.emit) {
emit('action', action.emit)
}
}
</script>
<template>
<div class="vignette" :class="{ 'vignette--open': open }">
<button class="vignette__header" @click="open = !open">
<h4 class="vignette__title">{{ title }}</h4>
<UIcon name="i-lucide-chevron-down" class="vignette__chevron" />
</button>
<div v-show="open" class="vignette__content">
<ul v-if="bullets && bullets.length > 0" class="vignette__bullets">
<li v-for="(b, i) in bullets" :key="i">{{ b }}</li>
</ul>
<div class="vignette__actions">
<button
v-for="action in resolvedActions"
:key="action.label"
class="vignette__btn"
:class="{ 'vignette__btn--primary': action.primary }"
@click="handleAction(action)"
>
<UIcon v-if="action.icon" :name="action.icon" />
<span>{{ action.label }}</span>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.vignette {
background: var(--mood-accent-soft);
border-radius: 12px;
overflow: hidden;
}
.vignette__header {
display: flex;
align-items: center;
width: 100%;
padding: 0.625rem 0.75rem;
cursor: pointer;
text-align: left;
gap: 0.375rem;
transition: background 0.12s ease;
}
.vignette__header:hover {
background: color-mix(in srgb, var(--mood-accent) 8%, transparent);
}
.vignette__title {
flex: 1;
font-size: 0.9375rem;
font-weight: 700;
color: var(--mood-text);
margin: 0;
}
.vignette__chevron {
font-size: 0.875rem;
color: var(--mood-text-muted);
opacity: 0.5;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.vignette--open .vignette__chevron {
transform: rotate(180deg);
opacity: 1;
color: var(--mood-accent);
}
.vignette__content {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0 0.75rem 0.625rem;
}
.vignette__bullets {
margin: 0;
padding: 0 0 0 1rem;
font-size: 0.8125rem;
color: var(--mood-text-muted);
line-height: 1.5;
list-style-type: disc;
}
.vignette__bullets li::marker {
color: var(--mood-accent);
}
.vignette__actions {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-top: 0.125rem;
}
.vignette__btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.875rem;
font-size: 0.8125rem;
font-weight: 700;
color: var(--mood-accent);
background: var(--mood-surface);
border-radius: 20px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
min-height: 2.25rem;
}
@media (min-width: 640px) {
.vignette__btn {
padding: 0.375rem 0.75rem;
min-height: auto;
}
}
.vignette__btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px var(--mood-shadow);
}
.vignette__btn:active {
transform: translateY(0);
}
.vignette__btn--primary {
color: var(--mood-accent-text);
background: var(--mood-accent);
}
</style>