diff --git a/Cargo.lock b/Cargo.lock
index 86e8b4a3..5b716822 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "PMOMusic"
-version = "0.3.28"
+version = "0.3.29"
dependencies = [
"axum 0.8.7",
"console-subscriber",
diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml
index bd17d142..e254a3fe 100644
--- a/PMOMusic/Cargo.toml
+++ b/PMOMusic/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "PMOMusic"
-version = "0.3.28"
+version = "0.3.29"
edition = "2024"
[dependencies]
diff --git a/pmoapp/webapp/src/assets/styles/drawers.css b/pmoapp/webapp/src/assets/styles/drawers.css
new file mode 100644
index 00000000..977c3b58
--- /dev/null
+++ b/pmoapp/webapp/src/assets/styles/drawers.css
@@ -0,0 +1,272 @@
+/**
+ * Styles communs pour les drawers (ServerDrawer, RendererDrawer)
+ *
+ * Chaque drawer ajoute uniquement ce qui le différencie :
+ * - position (left/right)
+ * - border (border-right / border-left)
+ * - box-shadow (direction)
+ * - backdrop spécifique (position)
+ */
+
+/* ========================================
+ BACKDROP
+ ======================================== */
+
+.drawer-backdrop {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ z-index: 200;
+}
+
+@media (max-width: 768px) and (orientation: portrait) {
+ .drawer-backdrop {
+ display: none;
+ }
+}
+
+/* ========================================
+ PANNEAU DRAWER
+ ======================================== */
+
+.app-drawer {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: 50vw;
+ /* Fond opaque — évite la transparence empilée sur d'autres éléments glass */
+ background: rgba(22, 22, 32, 0.96);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ z-index: 201;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+@media (prefers-color-scheme: light) {
+ .app-drawer {
+ background: rgba(245, 245, 250, 0.97);
+ }
+}
+
+@media (max-width: 768px) and (orientation: portrait) {
+ .app-drawer {
+ width: 100vw;
+ }
+}
+
+/* ========================================
+ HEADER
+ ======================================== */
+
+.drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-lg);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ flex-shrink: 0;
+}
+
+@media (prefers-color-scheme: light) {
+ .drawer-header {
+ border-bottom-color: rgba(0, 0, 0, 0.1);
+ }
+}
+
+.drawer-title-section {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+ color: var(--color-text);
+ flex: 1;
+ min-width: 0;
+}
+
+.drawer-title {
+ font-size: var(--text-xl);
+ font-weight: 700;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.drawer-title.small {
+ font-size: var(--text-base);
+}
+
+/* Bouton fermer */
+.drawer-close-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ flex-shrink: 0;
+ padding: 0;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ cursor: pointer;
+ transition: all var(--transition-fast) ease;
+ color: var(--color-text);
+}
+
+.drawer-close-btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+ transform: scale(1.1);
+}
+
+.drawer-close-btn:active {
+ transform: scale(0.95);
+}
+
+/* Bouton retour (ServerDrawer navigation) */
+.drawer-back-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ flex-shrink: 0;
+ padding: 0;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ cursor: pointer;
+ transition: all var(--transition-fast) ease;
+ color: var(--color-text);
+}
+
+.drawer-back-btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+ transform: scale(1.05);
+}
+
+/* ========================================
+ CONTENU
+ ======================================== */
+
+/* Add class="scrollbar-thin" on elements needing thin scrollbar (see pmocontrol.css) */
+.drawer-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: var(--spacing-md);
+}
+
+.drawer-section {
+ margin-bottom: var(--spacing-lg);
+}
+
+/* TODO: remplacer par la classe globale .section-title de pmocontrol.css */
+.drawer-section-title {
+ font-size: var(--text-sm);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--color-text-secondary);
+ margin: 0 0 var(--spacing-sm) 0;
+ padding: 0 var(--spacing-sm);
+}
+
+/* Liste d'items générique */
+.drawer-item-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.drawer-item {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ padding: var(--spacing-md);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ transition: all var(--transition-fast) ease;
+}
+
+.drawer-item.clickable {
+ cursor: pointer;
+}
+
+.drawer-item.clickable:hover {
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+.drawer-item.clickable:active {
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.drawer-item.offline {
+ opacity: var(--opacity-disabled, 0.5);
+ cursor: not-allowed;
+}
+
+/* Icône dans un item */
+.drawer-item-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ flex-shrink: 0;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--color-text-secondary);
+}
+
+/* Info texte dans un item */
+.drawer-item-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.drawer-item-name {
+ font-size: var(--text-base);
+ font-weight: 600;
+ color: var(--color-text);
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.drawer-item-sub {
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+ margin: 2px 0 0 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* ========================================
+ FOOTER
+ ======================================== */
+
+.drawer-footer {
+ padding: var(--spacing-md);
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ flex-shrink: 0;
+}
+
+@media (prefers-color-scheme: light) {
+ .drawer-footer {
+ border-top-color: rgba(0, 0, 0, 0.1);
+ }
+}
+
+/* Les animations d'entrée/sortie sont définies dans chaque composant (scoped)
+ car elles diffèrent selon la direction (gauche/droite/bas) */
diff --git a/pmoapp/webapp/src/assets/styles/pmocontrol.css b/pmoapp/webapp/src/assets/styles/pmocontrol.css
index 00cd4dbb..dbd85493 100644
--- a/pmoapp/webapp/src/assets/styles/pmocontrol.css
+++ b/pmoapp/webapp/src/assets/styles/pmocontrol.css
@@ -434,3 +434,122 @@ input[type="range"]::-moz-range-thumb:hover {
white-space: nowrap;
border-width: 0;
}
+
+/* ========================================
+ UTILITIES
+ ======================================== */
+
+/* Section title (used in drawers and panels) */
+.section-title {
+ font-size: var(--text-sm);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--color-text-secondary);
+ margin: 0 0 var(--spacing-sm) 0;
+}
+
+/* Icon-only circular button */
+.icon-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding: 0;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ cursor: pointer;
+ color: var(--color-text);
+ transition: background var(--transition-fast) ease, transform var(--transition-fast) ease;
+}
+
+.icon-btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.icon-btn:active {
+ transform: scale(0.92);
+}
+
+@media (prefers-color-scheme: light) {
+ .icon-btn {
+ background: rgba(0, 0, 0, 0.06);
+ border-color: rgba(0, 0, 0, 0.12);
+ }
+ .icon-btn:hover {
+ background: rgba(0, 0, 0, 0.12);
+ }
+}
+
+/* ========================================
+ PROTOCOL BADGES (global, used in BottomTabBar, RendererDrawer, RendererCard)
+ ======================================== */
+
+.protocol-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.2rem 0.5rem;
+ border-radius: var(--radius-full);
+ font-size: var(--text-xs);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border: 1px solid transparent;
+}
+
+.protocol-upnp {
+ background-color: rgba(59, 130, 246, 0.15);
+ color: #3b82f6;
+ border-color: rgba(59, 130, 246, 0.3);
+}
+
+.protocol-openhome {
+ background-color: rgba(139, 92, 246, 0.15);
+ color: #8b5cf6;
+ border-color: rgba(139, 92, 246, 0.3);
+}
+
+.protocol-hybrid {
+ background-color: rgba(16, 185, 129, 0.15);
+ color: #10b981;
+ border-color: rgba(16, 185, 129, 0.3);
+}
+
+.protocol-chromecast {
+ background-color: rgba(245, 158, 11, 0.15);
+ color: #f59e0b;
+ border-color: rgba(245, 158, 11, 0.3);
+}
+
+/* ========================================
+ SCROLLBAR (webkit)
+ ======================================== */
+
+.scrollbar-thin::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 3px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 3px;
+}
+
+.scrollbar-thin::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+/* ========================================
+ ANIMATIONS
+ ======================================== */
+
+@keyframes pulse-opacity {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
diff --git a/pmoapp/webapp/src/assets/styles/variables.css b/pmoapp/webapp/src/assets/styles/variables.css
index e3569395..f9c0c982 100644
--- a/pmoapp/webapp/src/assets/styles/variables.css
+++ b/pmoapp/webapp/src/assets/styles/variables.css
@@ -78,6 +78,8 @@
/* Primary color (brand) */
--color-primary: #667eea;
--color-primary-hover: #5568d3;
+ /* Primary color RGB components (for rgba() usage) */
+ --color-primary-rgb: 102, 126, 234;
/* ========================================
Transitions
diff --git a/pmoapp/webapp/src/components/PlayListManager.vue b/pmoapp/webapp/src/components/PlayListManager.vue
index d4385c6f..7b2debe3 100644
--- a/pmoapp/webapp/src/components/PlayListManager.vue
+++ b/pmoapp/webapp/src/components/PlayListManager.vue
@@ -1412,17 +1412,17 @@ onMounted(() => {
-.detail-header-main { display: flex; flex-direction: column; gap: 0.25rem; }
diff --git a/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue b/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue
index c665b17a..9c88bda0 100644
--- a/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue
+++ b/pmoapp/webapp/src/components/pmocontrol/MediaBrowser.vue
@@ -257,14 +257,7 @@ async function handleQueueItem(itemId: string, rendererId: string) {
animation: spin 1s linear infinite;
}
-@keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
+/* @keyframes spin is now global in pmocontrol.css */
/* Error */
.browser-error {
diff --git a/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue b/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
index d3d38072..3dfdf581 100644
--- a/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
+++ b/pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
@@ -253,23 +253,7 @@ function goToRenderer() {
letter-spacing: 0.05em;
}
-.protocol-upnp {
- background-color: rgba(59, 130, 246, 0.1);
- color: #3b82f6;
- border: 1px solid #3b82f6;
-}
-
-.protocol-openhome {
- background-color: rgba(139, 92, 246, 0.1);
- color: #8b5cf6;
- border: 1px solid #8b5cf6;
-}
-
-.protocol-hybrid {
- background-color: rgba(16, 185, 129, 0.1);
- color: #10b981;
- border: 1px solid #10b981;
-}
+/* Protocol badge colors (.protocol-upnp, .protocol-openhome, etc.) → pmocontrol.css */
/* Cover */
.card-cover {
diff --git a/pmoapp/webapp/src/components/unified/BottomTabBar.vue b/pmoapp/webapp/src/components/unified/BottomTabBar.vue
index 67d616a2..48d6383e 100644
--- a/pmoapp/webapp/src/components/unified/BottomTabBar.vue
+++ b/pmoapp/webapp/src/components/unified/BottomTabBar.vue
@@ -148,18 +148,18 @@ function handleRendererDrawerClick() {
gap: var(--spacing-md);
height: 72px;
padding: 0 var(--spacing-md);
- background: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(30px) saturate(180%);
- -webkit-backdrop-filter: blur(30px) saturate(180%);
- border-top: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.1);
+ background: rgba(22, 22, 32, 0.96);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border-top: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.3);
z-index: 100;
}
-@media (prefers-color-scheme: dark) {
+@media (prefers-color-scheme: light) {
.bottom-bar {
- background: rgba(0, 0, 0, 0.25);
- border-top: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(245, 245, 250, 0.97);
+ border-top-color: rgba(0, 0, 0, 0.1);
}
}
@@ -293,40 +293,7 @@ function handleRendererDrawerClick() {
margin: 0;
}
-/* Protocol badge */
-.protocol-badge {
- padding: 2px 8px;
- border-radius: 4px;
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- flex-shrink: 0;
-}
-
-.protocol-upnp {
- background-color: rgba(59, 130, 246, 0.15);
- color: #3b82f6;
- border: 1px solid rgba(59, 130, 246, 0.3);
-}
-
-.protocol-openhome {
- background-color: rgba(139, 92, 246, 0.15);
- color: #8b5cf6;
- border: 1px solid rgba(139, 92, 246, 0.3);
-}
-
-.protocol-hybrid {
- background-color: rgba(16, 185, 129, 0.15);
- color: #10b981;
- border: 1px solid rgba(16, 185, 129, 0.3);
-}
-
-.protocol-chromecast {
- background-color: rgba(244, 114, 182, 0.15);
- color: #f472b6;
- border: 1px solid rgba(244, 114, 182, 0.3);
-}
+/* Protocol badge classes (.protocol-badge, .protocol-upnp, etc.) are now global in pmocontrol.css */
.status-badge {
flex-shrink: 0;
diff --git a/pmoapp/webapp/src/components/unified/EmptyState.vue b/pmoapp/webapp/src/components/unified/EmptyState.vue
index 7904367c..36ef09b8 100644
--- a/pmoapp/webapp/src/components/unified/EmptyState.vue
+++ b/pmoapp/webapp/src/components/unified/EmptyState.vue
@@ -153,14 +153,7 @@ async function handleRefresh() {
animation: spin 1s linear infinite;
}
-@keyframes spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
+/* @keyframes spin is now global in pmocontrol.css */
/* Responsive mobile */
@media (max-width: 768px) {
diff --git a/pmoapp/webapp/src/components/unified/RendererDrawer.vue b/pmoapp/webapp/src/components/unified/RendererDrawer.vue
index 8eb0a19b..acff5f53 100644
--- a/pmoapp/webapp/src/components/unified/RendererDrawer.vue
+++ b/pmoapp/webapp/src/components/unified/RendererDrawer.vue
@@ -138,7 +138,7 @@ function handleRendererClick(renderer: RendererSummary) {
}
function handleSettingsClick() {
- router.push("/debug/api-dashboard");
+ router.push("/debug");
close();
}
@@ -210,7 +210,7 @@ async function handleTransferQueue(event: Event, targetRendererId: string) {
-