Files
decision/frontend/app/app.vue
Yvv 77dceb49c3 Refonte design : 4 humeurs, onboarding, sections avec boite a outils
- Systeme de themes adaptatifs : Peps (light chaud), Zen (light calme),
  Chagrine (dark violet), Grave (dark ambre) avec CSS custom properties
- Dashboard d'accueil orienté onboarding avec cartes-portes et teaser
  boite a outils
- SectionLayout reutilisable : liste + sidebar toolbox + status pills
  cliquables (En prepa / En vote / En vigueur / Clos)
- ToolboxVignette : vignettes Contexte / Tutos / Choisir / Demarrer
- Seed : Acte engagement certification + forgeron, Runtime Upgrade
  (decision on-chain), 3 modalites de vote (majoritaire, quadratique,
  permanent)
- Backend adapte SQLite (Uuid portable, 204 fix, pool conditionnel)
- Correction noms composants (pathPrefix: false), pinia/nuxt ^0.11

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:44:48 +01:00

280 lines
8.1 KiB
Vue

<script setup lang="ts">
const auth = useAuthStore()
const route = useRoute()
const { initMood } = useMood()
const navigationItems = [
{
label: 'Documents de reference',
icon: 'i-lucide-book-open',
to: '/documents',
},
{
label: 'Decisions',
icon: 'i-lucide-scale',
to: '/decisions',
},
{
label: 'Mandats',
icon: 'i-lucide-user-check',
to: '/mandates',
},
{
label: 'Protocoles',
icon: 'i-lucide-settings',
to: '/protocols',
},
{
label: 'Sanctuaire',
icon: 'i-lucide-archive',
to: '/sanctuary',
},
]
/** Mobile drawer state. */
const mobileMenuOpen = ref(false)
/** Close mobile menu on route change. */
watch(() => route.path, () => {
mobileMenuOpen.value = false
})
/** WebSocket connection and notifications. */
const ws = useWebSocket()
const { setupWsNotifications } = useNotifications()
onMounted(async () => {
// Apply saved mood / ambiance
initMood()
// Hydrate auth from localStorage
auth.hydrateFromStorage()
if (auth.token) {
try {
await auth.fetchMe()
} catch {
auth.logout()
}
}
// Connect WebSocket and setup notifications
ws.connect()
setupWsNotifications(ws)
})
onUnmounted(() => {
ws.disconnect()
})
</script>
<template>
<UApp>
<!-- Offline detection banner -->
<OfflineBanner />
<div
class="min-h-screen flex flex-col"
:style="{
backgroundColor: 'var(--mood-bg)',
color: 'var(--mood-text)',
}"
>
<!-- Header -->
<header class="sticky top-0 z-30 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<!-- Left: Hamburger (mobile) + Logo -->
<div class="flex items-center gap-3">
<!-- Hamburger menu button for mobile -->
<UButton
class="md:hidden"
icon="i-lucide-menu"
variant="ghost"
color="neutral"
size="sm"
aria-label="Ouvrir le menu"
@click="mobileMenuOpen = true"
/>
<NuxtLink to="/" class="flex items-center gap-2">
<UIcon name="i-lucide-vote" class="text-primary text-2xl" />
<span class="text-xl font-bold text-gray-900 dark:text-white">Glibredecision</span>
</NuxtLink>
</div>
<!-- Center: Mood switcher -->
<MoodSwitcher class="hidden sm:flex" />
<!-- Right: Auth controls -->
<div class="flex items-center gap-2 sm:gap-4">
<template v-if="auth.isAuthenticated">
<UBadge
:color="auth.identity?.is_smith ? 'success' : 'neutral'"
variant="subtle"
class="hidden sm:inline-flex"
>
{{ auth.identity?.display_name || auth.identity?.address?.slice(0, 12) + '...' }}
</UBadge>
<UBadge
v-if="auth.identity?.is_techcomm"
color="info"
variant="subtle"
class="hidden sm:inline-flex"
>
Comite Tech
</UBadge>
<UButton
icon="i-lucide-log-out"
variant="ghost"
color="neutral"
size="sm"
aria-label="Se deconnecter"
@click="auth.logout()"
/>
</template>
<template v-else>
<UButton
to="/login"
icon="i-lucide-log-in"
label="Se connecter"
variant="soft"
color="primary"
/>
</template>
</div>
</div>
</div>
</header>
<!-- Mobile navigation drawer (USlideover) -->
<USlideover
v-model:open="mobileMenuOpen"
side="left"
title="Navigation"
:ui="{ width: 'max-w-xs' }"
>
<template #body>
<nav class="py-2">
<NuxtLink
v-for="item in navigationItems"
:key="item.to"
:to="item.to"
class="flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-lg transition-colors"
:class="
route.path.startsWith(item.to)
? 'bg-primary-50 dark:bg-primary-900/20 text-primary'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
"
@click="mobileMenuOpen = false"
>
<UIcon :name="item.icon" class="text-lg" />
<span>{{ item.label }}</span>
</NuxtLink>
</nav>
<!-- Mobile user info -->
<div
v-if="auth.isAuthenticated"
class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 px-4"
>
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<UIcon name="i-lucide-user" class="text-lg" />
<span>{{ auth.identity?.display_name || auth.identity?.address?.slice(0, 16) + '...' }}</span>
</div>
<div class="flex gap-2 mt-2">
<UBadge
v-if="auth.identity?.is_smith"
color="success"
variant="subtle"
size="xs"
>
Forgeron
</UBadge>
<UBadge
v-if="auth.identity?.is_techcomm"
color="info"
variant="subtle"
size="xs"
>
Comite Tech
</UBadge>
</div>
</div>
</template>
</USlideover>
<!-- Main content with sidebar -->
<div class="flex flex-1">
<!-- Desktop sidebar navigation -->
<aside class="w-64 border-r border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50 hidden md:block flex-shrink-0">
<nav class="p-4 sticky top-16">
<UNavigationMenu
:items="navigationItems"
orientation="vertical"
class="w-full"
/>
</nav>
</aside>
<!-- Page content with error boundary -->
<main class="flex-1 min-w-0 p-4 sm:p-6 lg:p-8">
<ErrorBoundary>
<NuxtPage />
</ErrorBoundary>
</main>
</div>
<!-- WebSocket error banner -->
<Transition name="slide-up">
<div
v-if="ws.error.value"
class="fixed bottom-0 left-0 right-0 z-40 bg-error-100 dark:bg-error-900/50 border-t border-error-300 dark:border-error-700 px-4 py-3 text-center"
role="alert"
>
<div class="flex items-center justify-center gap-2 text-sm text-error-800 dark:text-error-200">
<UIcon name="i-lucide-plug-zap" class="text-lg flex-shrink-0" />
<span>{{ ws.error.value }}</span>
<UButton
size="xs"
variant="soft"
color="error"
label="Reconnecter"
icon="i-lucide-refresh-cw"
@click="ws.disconnect(); ws.connect()"
/>
</div>
</div>
</Transition>
<!-- Footer -->
<footer class="border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-gray-500">
<span>Glibredecision v0.1.0 - Decisions collectives pour Duniter/G1</span>
<span>Licence libre</span>
</div>
</div>
</footer>
</div>
</UApp>
</template>
<style scoped>
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
opacity: 0;
}
.slide-up-enter-to,
.slide-up-leave-from {
transform: translateY(0);
opacity: 1;
}
</style>