feat(frontend): Service Worker WS, mode édition dashboard, sidebar click-to-toggle

- WebSocket via Service Worker (ws.sw.js) : connexions persistantes entre navigations
  Swup, reconnexion exponentielle, protocole WS_SUBSCRIBE/WS_UNSUBSCRIBE/WS_SEND
- WsProxy dans app.js : abstraction SW + fallback WebSocket direct
- proxmoxPage migré vers WsProxy (identique au dashboardPage)
- Dashboard : mode édition toggle — DnD, resize (x1/x2), masquer/afficher widget
  uniquement actifs en mode édition ; preview drag (is-dragging/drag-over)
- Sidebar : suppression bouton hamburger, clic sur sidebar-header pour replier
- pages.css : targets-grid 350px, styles edit mode, widget-size-2, drag preview
- neu.css : sidebar-header cursor pointer, suppression .sidebar-toggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-21 19:09:58 +01:00
parent b6d6355c6c
commit cbfb20505d
11 changed files with 359 additions and 128 deletions

View file

@ -18,12 +18,9 @@
<body x-data x-init="$store.ui.init()">
<aside class="sidebar" x-data="sidebar()" :class="{ collapsed: collapsed }" x-cloak>
<div class="sidebar-header">
<div class="sidebar-header" @click="toggle()">
<i class="sidebar-logo lnid-layout-1"></i>
<span class="sidebar-title" x-show="!collapsed">ProxmoxPanel</span>
<button class="sidebar-toggle neu-btn neu-btn--sm" @click="toggle()">
<i class="lnid-menu-hamburger-1"></i>
</button>
</div>
<nav class="sidebar-nav">
<template x-for="item in navItems" :key="item.id">
@ -62,47 +59,68 @@
<div class="page-header">
<h2 class="page-title" x-text="t('dashboard.welcome').replace('{name}', $store.auth.user?.username || '')"></h2>
<button class="neu-btn neu-btn--sm" @click="configOpen = !configOpen" :title="t('dashboard.addWidget')">
<i class="lnid-layout-1"></i>
<span x-show="!configOpen" x-text="t('dashboard.addWidget')"></span>
<span x-show="configOpen"></span>
<button class="neu-btn neu-btn--sm" :class="{ 'neu-btn--primary': editMode }" @click="toggleEdit()">
<i class="lnid-pen-1"></i>
<span x-text="editMode ? '✓ Terminer' : t('dashboard.addWidget')"></span>
</button>
</div>
<!-- Config panel widgets -->
<div class="widget-config neu-card" x-show="configOpen" x-transition>
<h4 class="widget-config-title">Configurer les widgets</h4>
<template x-for="(w, idx) in widgets" :key="w.id">
<div class="widget-config-row"
draggable="true"
@dragstart="onDragStart(idx)"
@dragover.prevent
@drop.prevent="onDrop(idx)">
<i class="lnid-menu-hamburger-1 drag-handle"></i>
<span x-text="w.label"></span>
<label class="widget-toggle-label">
<input type="checkbox" x-model="w.visible" @change="saveWidgets()" />
<span x-text="w.visible ? 'Visible' : 'Masqué'"></span>
</label>
<!-- Mode édition — bannière + widgets masqués -->
<div x-show="editMode" x-transition>
<div class="edit-mode-banner">
<i class="lnid-pen-1"></i>
<span>Mode édition — glissez les widgets pour les réordonner, redimensionnez ou masquez-les.</span>
</div>
<!-- Widgets masqués (à ajouter) -->
<div x-show="hiddenWidgets.length > 0">
<div class="widget-add-grid">
<template x-for="w in hiddenWidgets" :key="w.id">
<button class="widget-add-btn" @click="showWidget(w.id)">
<i class="lnid-add-circle"></i>
<span x-text="w.label"></span>
</button>
</template>
</div>
</template>
</div>
</div>
<!-- WS status -->
<div class="ws-status" :class="wsStatus" x-show="wsStatus !== 'connected'">
<div class="ws-status" :class="wsStatus" x-show="wsStatus !== 'connected'" x-transition>
<span x-show="wsStatus === 'connecting'">⌛ Connexion…</span>
<span x-show="wsStatus === 'disconnected'">⚠ Déconnecté (reconnexion…)</span>
<span x-show="wsStatus === 'error'">✗ Erreur WebSocket</span>
</div>
<!-- Widgets -->
<!-- Widgets grid -->
<div class="widgets-grid">
<template x-for="(w, idx) in widgets.filter(w => w.visible)" :key="w.id">
<template x-for="(w, idx) in visibleWidgets" :key="w.id">
<div class="neu-card widget"
draggable="true"
@dragstart="onDragStart(widgets.indexOf(w))"
:class="{
'widget-size-2': w.size === 2,
'is-dragging': editMode && dragSrcIdx === widgets.indexOf(w),
'drag-over': editMode && dragOverIdx === widgets.indexOf(w),
}"
:draggable="editMode"
@dragstart="editMode && onDragStart(widgets.indexOf(w))"
@dragenter.prevent="editMode && onDragEnter(widgets.indexOf(w))"
@dragleave="editMode && onDragLeave(widgets.indexOf(w))"
@dragover.prevent
@drop.prevent="onDrop(widgets.indexOf(w))">
@dragend="onDragEnd()"
@drop.prevent="editMode && onDrop(widgets.indexOf(w))">
<!-- Barre d'édition (visible en mode édition) -->
<div class="widget-edit-bar" x-show="editMode">
<i class="lnid-menu-hamburger-1 drag-handle"></i>
<span class="widget-edit-bar-title" x-text="w.label"></span>
<button class="neu-btn neu-btn--sm" @click.stop="toggleSize(w)"
:title="w.size === 2 ? 'Réduire' : 'Agrandir'">
<i :class="w.size === 2 ? 'lnid-compress-arrows' : 'lnid-expand-arrows'"></i>
</button>
<button class="neu-btn neu-btn--sm" @click.stop="hideWidget(w)" title="Masquer">
<i class="lnid-close"></i>
</button>
</div>
<!-- Widget: status -->
<template x-if="w.id === 'status'">
@ -171,8 +189,8 @@
</div>
</template>
<p class="empty-state" x-show="widgets.filter(w => w.visible).length === 0">
Aucun widget actif. Cliquez sur "Configurer" pour en ajouter.
<p class="empty-state" x-show="visibleWidgets.length === 0 && !editMode">
Aucun widget actif — activez le mode édition pour en ajouter.
</p>
</div>
</main>