Bonjour à tous et aux pros du css ![]()
J’essaie de créer un bouton animé, que j’applique des commandes actions sur un virtuel en disposition de tableau.
Mon problème est que les boutons sont centrés et décallés à droite. Ni chatgpt, ni Gemini m’ont trouvé la solution.
Quelqu’un a t’il une idée ?
Le visuel


la mise en forme du virtuel
Les options
Le code du widget
<div class="cmd cmd-widget cursor neon-button_#uid#" id="neonBtn_#uid#" data-cmdid="#id#" style="min-width: unset;">
<div class="btn-root" id="btnRoot_#uid#">
<span class="label" id="btnLabel_#uid#"></span>
<svg class="ring" viewBox="0 0 100 100">
<circle class="track" cx="50" cy="50" r="46"/>
<circle class="path" cx="50" cy="50" r="46"/>
</svg>
</div>
</div>
<style>
/* -------------------- STYLE CSS V51 (Base V41) -------------------- */
.neon-button_#uid# {
display: flex !important;
align-items: center;
justify-content: center;
width: 100% !important;
height: 100% !important;
box-sizing: border-box;
padding: 0 !important;
}
.neon-button_#uid# .btn-root {
position: relative;
width: var(--btn-size-clamped, 100px);
height: var(--btn-size-clamped, 100px);
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #2c3138, #111);
box-shadow: inset 0 2px 4px rgba(255,255,255,0.1),
inset -2px -4px 6px rgba(0,0,0,0.5),
0 0 40px rgba(0,0,0,0.2),
0 0 25px var(--glow-color, rgba(100,200,255,0.6));
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.12s ease, box-shadow 0.12s ease;
margin: 0;
}
.neon-button_#uid# .btn-root:active { transform: scale(0.96); }
.neon-button_#uid# .label {
font-family: Arial, sans-serif;
font-weight: bold;
font-size: 20px;
color: #fff;
text-shadow: 0 0 6px var(--ring-color, #69d6ff),
0 0 12px var(--ring-color, #2fd0ff);
pointer-events: none;
z-index: 2;
display: inline-flex;
align-items: center;
line-height: 1;
}
.neon-button_#uid# .label i { line-height: 1; display: inline-block; }
.neon-button_#uid# .ring {
position: absolute;
inset: 0;
transform: rotate(-90deg);
z-index: 1;
}
.neon-button_#uid# .ring .track {
fill: none;
stroke: #444;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.5;
}
.neon-button_#uid# .ring .path {
fill: none;
stroke: var(--ring-color, #69d6ff);
stroke-width: 6;
stroke-linecap: round;
stroke-dasharray: 290;
stroke-dashoffset: 290;
filter: drop-shadow(0 0 3px #FFFFFF)
drop-shadow(0 0 15px var(--ring-color, #69d6ff));
transition: stroke-dashoffset 0s linear;
animation: none !important;
}
/* -------------------- FIN CSS -------------------- */
</style>
<script>
/* -------------------- DEBUT JAVASCRIPT V51 -------------------- */
(function(){
var uid = '#uid#';
var rootContainer = document.getElementById('neonBtn_' + uid);
var root = document.getElementById('btnRoot_' + uid);
var label = document.getElementById('btnLabel_' + uid);
var svgRing = root.querySelector('.ring');
var path = svgRing.querySelector('.path');
var track = svgRing.querySelector('.track');
var cmdId = rootContainer.getAttribute('data-cmdid');
var defaultTransition = 'transform 0.12s ease, box-shadow 0.12s ease';
// Récupération des options
function fromParamOrDefault(token, fallback){
var v = (token || '').toString().trim();
if (!v || v === 'undefined' || /^#.*#$/.test(v)) return fallback;
return v;
}
var sizeParam = fromParamOrDefault('#size#', '120');
var colorParam = fromParamOrDefault('#color#', '#69d6ff');
var durationParam = parseFloat(fromParamOrDefault('#duration#', '1')) || 1;
var pulsesParam = parseInt(fromParamOrDefault('#pulses#', '1')) || 1;
// Calculs de temps
var fullDurationMs = (durationParam / pulsesParam) * 1000;
var timeFullMs = fullDurationMs * 0.75;
var timeEmptyMs = fullDurationMs * 0.25;
// Initialisation des styles
var sizePx = parseInt(sizeParam, 10);
var defaultLabel = '#name_display#'.toString().trim();
if (defaultLabel === '#name_display#') { defaultLabel = 'START'; }
var labelParam = fromParamOrDefault('#label#', defaultLabel);
var ringColor = colorParam;
var glowColor = colorParam;
root.style.setProperty('--ring-color', ringColor);
root.style.setProperty('--glow-color', glowColor + 'AA');
// Gestion du label
var labelText = labelParam.toString().trim();
label.innerHTML = '';
if (/\bfa-\w+|\bfa[srlb]\b|\bfa\b/.test(labelText)) {
var i = document.createElement('i');
var classes = labelText.split(/\s+/).filter(c => c.length > 0);
classes.forEach(function(cls) { i.classList.add(cls); });
var hasFamily = classes.some(function(cls) { return /^fa[srlb]$/.test(cls); });
if (!hasFamily && classes.some(c => c.startsWith('fa-'))) { i.classList.add('fas'); }
label.appendChild(i);
} else { label.textContent = labelText; }
// Gestion de la taille
if (!isNaN(sizePx) && sizePx >= 20) {
var clampedSize = Math.max(20, sizePx - 20);
root.style.setProperty('--btn-size-clamped', clampedSize + 'px');
var strokeWidth = Math.max(2, Math.round(clampedSize * 0.05));
path.setAttribute('stroke-width', strokeWidth);
track.setAttribute('stroke-width', strokeWidth);
var fontSize = Math.max(12, Math.min(30, clampedSize * 0.16));
label.style.fontSize = fontSize + 'px';
var sWhite = clampedSize * 0.03;
var sDiffuse = clampedSize * 0.15;
var sGlow = clampedSize * 0.25;
path.style.filter = `drop-shadow(0 0 ${sWhite}px #FFFFFF)
drop-shadow(0 0 ${sDiffuse}px ${ringColor})`;
var initialBoxShadow = `inset 0 2px 4px rgba(255,255,255,0.1),
inset -2px -4px 6px rgba(0,0,0,0.5),
0 0 40px rgba(0,0,0,0.2),
0 0 ${sGlow}px ${glowColor}AA`;
root.setAttribute('data-initial-shadow', initialBoxShadow);
root.style.boxShadow = initialBoxShadow;
}
// Gestion des pulses
var currentPulse = 0;
var isPulsing = false;
// V51: Fonction dédiée au battement de coeur (scale 1.01)
function triggerHeartbeat() {
var originalShadow = root.getAttribute('data-initial-shadow');
var heartShadow = `0 0 50px ${glowColor}, ${originalShadow}`;
var scaleFactor = 1.01; // Battement très subtil
// 1. Battement (Zoom avant)
root.style.transition = 'transform 0.15s ease-out, box-shadow 0.15s ease-out';
root.style.transform = `scale(${scaleFactor})`;
root.style.boxShadow = heartShadow;
// 2. Retour (Zoom arrière)
setTimeout(() => {
root.style.transition = 'transform 0.25s ease-in, box-shadow 0.25s ease-in';
root.style.transform = 'scale(1)';
root.style.boxShadow = originalShadow;
// 3. Réinitialiser la transition par défaut
setTimeout(() => {
root.style.transition = defaultTransition;
}, 300);
}, 150);
}
// Nettoyage après l'animation
function finalizePulse() {
path.style.transitionDuration = '0s';
path.style.strokeDashoffset = 290;
isPulsing = false;
root.style.pointerEvents = 'auto';
path.style.transitionTimingFunction = 'linear';
}
function startPulseCycle() {
if (currentPulse >= pulsesParam) {
finalizePulse();
return;
}
currentPulse++;
// Définir la transition pour le remplissage
path.style.transitionDuration = (timeFullMs / 1000) + 's';
path.style.transitionTimingFunction = 'ease-in-out';
path.style.strokeDashoffset = 290;
setTimeout(function() {
path.style.strokeDashoffset = 0; // Fin du remplissage
setTimeout(function() {
// Déclenchement du battement de cœur après le remplissage de CHAQUE tour
triggerHeartbeat();
// Réinitialiser la transition pour le vidage instantané
path.style.transitionDuration = '0s';
path.style.transitionTimingFunction = 'linear';
if (currentPulse < pulsesParam) {
path.style.strokeDashoffset = 290; // Réinitialise l'anneau
setTimeout(startPulseCycle, timeEmptyMs);
} else {
finalizePulse();
}
}, timeFullMs);
}, 10);
}
root.addEventListener('click', function(ev){
if (isPulsing) return;
// 1. Exécution de la commande Jeedom
if (cmdId && typeof jeedom !== 'undefined' && jeedom.cmd && typeof jeedom.cmd.execute === 'function') {
jeedom.cmd.execute({ id: cmdId });
}
// 2. Préparation
currentPulse = 0;
isPulsing = true;
path.style.transitionDuration = '0s';
path.style.transitionTimingFunction = 'linear';
path.style.strokeDashoffset = 290;
root.style.pointerEvents = 'none';
// 3. Lancement du cycle
startPulseCycle();
});
})();
/* -------------------- FIN JAVASCRIPT -------------------- */
</script>
Merci !
Frédéric



