Compare commits

..

2 Commits

Author SHA1 Message Date
benoit 7e42164c5c feat: intégration VU-mètre circulaire autour du bouton PTT
NOUVEAU DESIGN :
- VU-mètre transformé en anneau SVG autour du bouton PTT
- Cercle progressif (0-360°) selon le niveau audio
- Couleurs dynamiques selon le niveau :
  - Vert (0-75%) : audio normal
  - Orange (75-90%) : niveau élevé
  - Rouge (90-100%) : saturation/danger avec effet glow
  - Bleu : mode talking (micro actif)

AVANTAGES :
- Beaucoup plus discret et élégant
- Intégré visuellement au bouton principal
- Pas d'encombrement UI supplémentaire
- Contextuellement pertinent (niveau autour du contrôle)

TECHNIQUE :
- SVG avec stroke-dasharray pour l'arc progressif
- Transition fluide 0.1s sur le niveau
- Drop-shadow dynamique selon couleur
- Responsive (adapté mobile/paysage)
- z-index stratifié (anneau derrière, bouton devant)

SUPPRESSION :
- Ancien composant AudioIndicator.jsx retiré de App.jsx
- Garde plus d'espace pour UserList et GroupSelector

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-23 10:17:29 +02:00
benoit ed6d2e763d fix: augmentation hauteur UserList et amélioration scroll
CORRECTIONS :
- Hauteur max passée de 180px à 300px (desktop)
- Hauteur max passée de 150px à 250px (mobile)
- Ajout min-height: 100px pour garantir visibilité

AMÉLIORATIONS SCROLL :
- Scrollbar personnalisée fine et discrète
- Support -webkit-overflow-scrolling: touch (iOS)
- Scrollbar visible au hover (desktop)
- Gap réduit entre items pour densité optimale

RESPONSIVE :
- Portrait mobile : 200px max
- Paysage : 120px max (priorité au bouton PTT)
- Padding réduit pour maximiser l'espace visible

Résultat : Affichage de 4-5 utilisateurs au lieu de 1-2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-23 09:57:00 +02:00
4 changed files with 145 additions and 18 deletions
+2 -5
View File
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react';
import useLiveKit from './hooks/useLiveKit'; import useLiveKit from './hooks/useLiveKit';
import PTTButton from './components/PTTButton'; import PTTButton from './components/PTTButton';
import UserList from './components/UserList'; import UserList from './components/UserList';
import AudioIndicator from './components/AudioIndicator';
import GroupSelector from './components/GroupSelector'; import GroupSelector from './components/GroupSelector';
import './App.css'; import './App.css';
@@ -241,14 +240,12 @@ function App() {
{/* Liste des participants */} {/* Liste des participants */}
<UserList participants={participants} /> <UserList participants={participants} />
{/* Indicateur audio */} {/* Bouton PTT principal avec VU-mètre intégré */}
<AudioIndicator level={audioLevel} isTalking={isTalking} />
{/* Bouton PTT principal */}
<PTTButton <PTTButton
isTalking={isTalking} isTalking={isTalking}
onPressStart={startTalking} onPressStart={startTalking}
onPressEnd={stopTalking} onPressEnd={stopTalking}
audioLevel={audioLevel}
/> />
</main> </main>
</div> </div>
+29
View File
@@ -11,6 +11,24 @@
position: relative; position: relative;
} }
/* Wrapper bouton + anneau VU-mètre */
.ptt-button-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
/* Anneau VU-mètre (SVG) */
.audio-ring {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 0;
}
.ptt-button { .ptt-button {
width: 240px; width: 240px;
height: 240px; height: 240px;
@@ -27,6 +45,7 @@
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
z-index: 1; /* Au-dessus de l'anneau */
/* Mobile touch optimizations */ /* Mobile touch optimizations */
touch-action: none; touch-action: none;
@@ -189,6 +208,11 @@
height: 200px; height: 200px;
} }
.audio-ring {
width: 240px;
height: 240px;
}
.ptt-icon { .ptt-icon {
width: 56px; width: 56px;
height: 56px; height: 56px;
@@ -210,6 +234,11 @@
height: 160px; height: 160px;
} }
.audio-ring {
width: 200px;
height: 200px;
}
.ptt-icon { .ptt-icon {
width: 48px; width: 48px;
height: 48px; height: 48px;
+60 -1
View File
@@ -7,8 +7,9 @@ import './PTTButton.css';
* Modes : * Modes :
* - PTT classique : maintenir pour parler * - PTT classique : maintenir pour parler
* - Mode continu (lock) : glisser vers le haut pendant qu'on parle * - Mode continu (lock) : glisser vers le haut pendant qu'on parle
* Inclut VU-mètre intégré (anneau autour du bouton)
*/ */
export default function PTTButton({ isTalking, onPressStart, onPressEnd }) { export default function PTTButton({ isTalking, onPressStart, onPressEnd, audioLevel = 0 }) {
const buttonRef = useRef(null); const buttonRef = useRef(null);
const isPressingRef = useRef(false); const isPressingRef = useRef(false);
const [isLockMode, setIsLockMode] = useState(false); const [isLockMode, setIsLockMode] = useState(false);
@@ -241,6 +242,25 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
} }
}; };
// Calculer le niveau audio normalisé (0-100)
const normalizedLevel = Math.min(100, Math.max(0, audioLevel));
// Convertir le niveau en angle pour le cercle SVG (0-360°)
const levelAngle = (normalizedLevel / 100) * 360;
// Calculer le dasharray pour l'arc SVG
const radius = 130; // Rayon du cercle VU-mètre
const circumference = 2 * Math.PI * radius;
const dashOffset = circumference - (levelAngle / 360) * circumference;
// Déterminer la couleur selon le niveau
const getAudioColor = () => {
if (normalizedLevel > 90) return '#ef4444'; // Danger (rouge)
if (normalizedLevel > 75) return '#f59e0b'; // Warning (orange)
if (isTalking) return '#3b82f6'; // Talking (bleu)
return '#10b981'; // Normal (vert)
};
return ( return (
<div className="ptt-container"> <div className="ptt-container">
{/* Zone de drag vers le haut (indicateur visuel) */} {/* Zone de drag vers le haut (indicateur visuel) */}
@@ -253,6 +273,44 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
</div> </div>
)} )}
{/* Conteneur bouton + VU-mètre */}
<div className="ptt-button-wrapper">
{/* VU-mètre circulaire (SVG) */}
<svg
className="audio-ring"
width="280"
height="280"
viewBox="0 0 280 280"
>
{/* Cercle de fond (gris) */}
<circle
cx="140"
cy="140"
r={radius}
fill="none"
stroke="rgba(255, 255, 255, 0.1)"
strokeWidth="8"
/>
{/* Cercle de niveau audio (coloré) */}
<circle
cx="140"
cy="140"
r={radius}
fill="none"
stroke={getAudioColor()}
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
transform="rotate(-90 140 140)"
style={{
transition: 'stroke-dashoffset 0.1s ease, stroke 0.2s ease',
filter: normalizedLevel > 0 ? `drop-shadow(0 0 8px ${getAudioColor()})` : 'none'
}}
/>
</svg>
{/* Bouton PTT principal */} {/* Bouton PTT principal */}
<button <button
ref={buttonRef} ref={buttonRef}
@@ -295,6 +353,7 @@ export default function PTTButton({ isTalking, onPressStart, onPressEnd }) {
: 'Maintenir pour parler'} : 'Maintenir pour parler'}
</span> </span>
</button> </button>
</div>
<p className="ptt-hint"> <p className="ptt-hint">
{isLockMode {isLockMode
+45 -3
View File
@@ -3,8 +3,32 @@
.user-list { .user-list {
background: var(--color-surface); background: var(--color-surface);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
max-height: 180px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
/* Améliorer le scroll sur mobile */
-webkit-overflow-scrolling: touch;
/* Style de scrollbar personnalisé */
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
/* Scrollbar personnalisée (webkit) */
.user-list::-webkit-scrollbar {
width: 6px;
}
.user-list::-webkit-scrollbar-track {
background: transparent;
}
.user-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.user-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
} }
.user-list.empty { .user-list.empty {
@@ -36,7 +60,10 @@
} }
.user-list-items { .user-list-items {
padding: var(--spacing-sm); padding: var(--spacing-xs) var(--spacing-sm);
display: flex;
flex-direction: column;
gap: 4px;
} }
/* Item utilisateur */ /* Item utilisateur */
@@ -131,7 +158,7 @@
/* Responsive */ /* Responsive */
@media (max-width: 640px) { @media (max-width: 640px) {
.user-list { .user-list {
max-height: 150px; max-height: 250px;
} }
.user-avatar { .user-avatar {
@@ -148,3 +175,18 @@
font-size: 0.75rem; font-size: 0.75rem;
} }
} }
/* Portrait mobile - encore plus d'espace */
@media (max-width: 640px) and (orientation: portrait) {
.user-list {
max-height: 200px;
min-height: 100px;
}
}
/* Paysage - réduire un peu pour laisser place au PTT */
@media (max-height: 500px) and (orientation: landscape) {
.user-list {
max-height: 120px;
}
}