etm-powersync-app/docs/mockups/etm-powersync-flux-drilldown.html
Patrick Schurig 0ebbde49da chore(docs): range specs/contrats/refs dans docs/, track l'autorité
- specs -> docs/ (DASHBOARD_SPEC compagnon du contrat)
- ref introspection nymea-jsonRPC -> docs/reference/
- mockups HTML -> docs/mockups/ ; briefs consommés -> docs/archive/
- track AGENTS.md + INTERFACE_etmvariableload.md (étaient untracked)
- INTERFACE_*: marqué miroir, canonique = repo plugin
2026-06-28 12:49:57 +02:00

183 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="fr" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ETM PowerSync — flux drill-down</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--sans:'IBM Plex Sans',system-ui,sans-serif;--mono:'IBM Plex Mono',ui-monospace,monospace;
--brand:#31a3dd;--solar:#fec113;--eco:#28a06a;--heat:#ef8b2a;--water:#31a3dd;--grid-c:#8a98a3;--radius:20px}
html[data-theme="light"]{--bg:#eef2f5;--bg2:#e4eaee;--surface:#fff;--ink:#0d2b3b;--muted:#5d6f7b;--faint:#8b9aa4;--line:rgba(13,43,59,.10);--line-strong:rgba(13,43,59,.16);--shadow:0 1px 2px rgba(13,43,59,.04)}
html[data-theme="dark"]{--bg:#06141d;--bg2:#0a1f2b;--surface:#0d2b3b;--ink:#eaf3f7;--muted:#9bb0bc;--faint:#6c8493;--line:rgba(255,255,255,.09);--line-strong:rgba(255,255,255,.16);--shadow:none;--eco:#33b87a;--solar:#ffcb33}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--sans);background:var(--bg2);color:var(--ink);min-height:100vh;display:flex;justify-content:center;align-items:flex-start;padding:24px 12px;-webkit-font-smoothing:antialiased}
body,.surface,.theme-toggle,.flowcard,.node{transition:background .35s,color .35s,border-color .35s}
.phone{width:100%;max-width:392px;background:var(--bg);border-radius:34px;border:1px solid var(--line);overflow:hidden;position:relative}
.topbar{display:flex;align-items:center;justify-content:space-between;padding:18px 20px 10px}
.wordmark b{font-size:17px;font-weight:700;letter-spacing:-.01em;display:block;line-height:1.1}.wordmark span{font-family:var(--mono);font-size:10px;color:var(--faint)}
.theme-toggle{width:58px;height:30px;border-radius:30px;border:1px solid var(--line-strong);background:var(--surface);position:relative;cursor:pointer;display:flex;align-items:center;padding:0 6px;justify-content:space-between}
.theme-toggle svg{width:14px;height:14px;color:var(--faint);z-index:1}
.theme-toggle .knob{position:absolute;top:3px;left:3px;width:22px;height:22px;border-radius:50%;background:var(--brand);transition:transform .32s cubic-bezier(.34,1.56,.64,1)}
html[data-theme="dark"] .theme-toggle .knob{transform:translateX(28px);background:var(--solar)}
.scroll{padding:6px 16px 22px}
.flowcard{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:12px 16px 14px}
.flowhead{display:flex;align-items:center;gap:10px;min-height:34px;margin-bottom:2px}
.flowhead .back{width:32px;height:32px;border-radius:10px;border:1px solid var(--line);background:transparent;display:none;align-items:center;justify-content:center;cursor:pointer;color:var(--ink);flex:0 0 auto}
.flowhead .back svg{width:17px;height:17px}
.flowhead.detail .back{display:flex}
.flowhead h2{font-size:13px;font-weight:600;flex:1}
.flowhead .live{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;color:var(--muted)}
.flowhead .dot{width:7px;height:7px;border-radius:50%;background:var(--eco)}
.flow{position:relative;width:100%;aspect-ratio:1/1;max-width:320px;margin:2px auto 0}
.flow svg.lines{position:absolute;inset:0;width:100%;height:100%;overflow:visible;transition:opacity .25s}
.node{position:absolute;transform:translate(-50%,-50%);width:74px;height:74px;border-radius:50%;background:var(--surface);border:1.5px solid var(--line);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;text-align:center}
.node .ico{width:21px;height:21px}.node .val{font-family:var(--mono);font-size:11px;font-weight:600;line-height:1}.node .lab{font-size:8.5px;color:var(--muted)}
.node.hub{width:92px;height:92px;border-width:2px}.node.hub .val{font-size:14px}.node.hub .lab{font-size:9px}
.node.tappable{cursor:pointer}
.node.tappable::after{content:"";position:absolute;right:5px;top:5px;width:15px;height:15px;border-radius:50%;background:var(--brand);background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3'><path d='M12 5v14M5 12h14'/></svg>");background-size:11px;background-repeat:no-repeat;background-position:center}
.node.estim{border-style:dashed;opacity:.85}
.node .state{position:absolute;bottom:6px;width:7px;height:7px;border-radius:50%}
.state.ok{background:var(--eco)} .state.warn{background:var(--heat)}
.ico.solar{color:var(--solar)}.ico.batt{color:var(--eco)}.ico.grid{color:var(--grid-c)}.ico.home{color:var(--ink)}.ico.heat{color:var(--heat)}.ico.water{color:var(--water)}.ico.car{color:var(--eco)}.ico.other{color:var(--faint)}.ico.inv{color:var(--solar)}.ico.hems{color:var(--eco)}
.heos-ring{position:absolute;inset:-5px;border-radius:50%;border:1.5px solid var(--eco);animation:ring 3s ease-in-out infinite}
@keyframes ring{0%,100%{opacity:.22}50%{opacity:.55}}
.flowline{fill:none;stroke:var(--line-strong);stroke-width:.7}
.enter{animation:pop .42s cubic-bezier(.34,1.5,.5,1) both}
@keyframes pop{from{transform:translate(-50%,-50%) scale(.25);opacity:0}to{transform:translate(-50%,-50%) scale(1);opacity:1}}
.leaving{animation:fade .16s ease forwards}
@keyframes fade{to{opacity:0;transform:translate(-50%,-50%) scale(.8)}}
.hint{text-align:center;font-size:11.5px;color:var(--faint);margin-top:12px}
.hint b{color:var(--brand);font-weight:600}
@media (prefers-reduced-motion:reduce){.enter,.leaving{animation:none!important}.heos-ring{animation:none}}
</style>
</head>
<body>
<div class="phone">
<div class="topbar">
<div class="wordmark"><b>ETM PowerSync</b><span>Flux énergétique</span></div>
<button class="theme-toggle" id="themeBtn" aria-label="Basculer le thème">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.4 1.4M17.6 17.6L19 19M19 5l-1.4 1.4M6.4 17.6L5 19"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.8A9 9 0 1111.2 3 7 7 0 0021 12.8z"/></svg>
<span class="knob"></span>
</button>
</div>
<div class="scroll">
<div class="flowcard">
<div class="flowhead" id="head">
<button class="back" id="back" aria-label="Retour"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg></button>
<h2 id="title">Flux énergétique</h2>
<div class="live"><span class="dot"></span> en direct</div>
</div>
<div class="flow" id="flow"><svg class="lines" id="lines" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"></svg></div>
<div class="hint" id="hint">Touchez <b>Maison</b> ou <b>Solaire</b> pour détailler</div>
</div>
</div>
</div>
<script>
const reduce=window.matchMedia('(prefers-reduced-motion:reduce)').matches;
document.getElementById('themeBtn').addEventListener('click',()=>{const h=document.documentElement;h.dataset.theme=h.dataset.theme==='dark'?'light':'dark';});
const NS="http://www.w3.org/2000/svg";
const SE=(t,a,p)=>{const e=document.createElementNS(NS,t);for(const k in a)e.setAttribute(k,a[k]);p&&p.appendChild(e);return e;};
const flow=document.getElementById('flow'), lines=document.getElementById('lines');
const title=document.getElementById('title'), head=document.getElementById('head'), hint=document.getElementById('hint');
const ICONS={
sun:'<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.4 1.4M17.6 17.6L19 19M19 5l-1.4 1.4M6.4 17.6L5 19"/>',
batt:'<rect x="4" y="8" width="16" height="11" rx="2"/><path d="M9 8V6h6v2M8 13l3-2v4l3-2"/>',
grid:'<path d="M5 21V9l7-6 7 6v12M9 21v-6h6v6"/>',
home:'<path d="M3 11l9-7 9 7M5 10v10h14V10M10 20v-6h4v6"/>',
plug:'<path d="M9 2v6M15 2v6M7 8h10v3a5 5 0 01-10 0zM12 16v6"/>',
heat:'<path d="M12 3c2 3 1 5 0 6 3-1 5 2 5 5a5 5 0 01-10 0c0-4 3-6 5-11z"/>',
water:'<path d="M12 3s6 6.5 6 11a6 6 0 01-12 0c0-4.5 6-11 6-11z"/>',
car:'<path d="M5 13l1.5-5h11L19 13M5 13h14v5H5zM7 18v2M17 18v2"/>',
other:'<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6v6H9z"/>',
inv:'<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M8 9l3 3-3 3M13 15h3"/>',
hems:'<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"/><circle cx="12" cy="12" r="2.4"/>'
};
const POS={top:[50,12],right:[88,50],bottom:[50,88],left:[12,50],hub:[50,50]};
const EDGE={ // [nodeEdge, hubEdge]
top:[[50,22],[50,39]], right:[[78,50],[61,50]], left:[[22,50],[39,50]], bottom:[[50,78],[50,61]]
};
const SCENES={
overview:{title:'Flux énergétique', hub:{lab:'Héos',val:'',ico:'hems',heos:true},
ring:[
{pos:'top',lab:'Solaire',val:'3.8 kW',ico:'sun',color:'#fec113',dir:'in',tap:'prod'},
{pos:'right',lab:'Batterie',val:'87 %',ico:'batt',color:'#28a06a',dir:'out'},
{pos:'left',lab:'Réseau',val:'0.2 kW',ico:'grid',color:'#8a98a3',dir:'in'},
{pos:'bottom',lab:'Maison',val:'2.4 kW',ico:'home',color:'#31a3dd',dir:'out',tap:'conso'}
]},
conso:{title:'Consommation — détail', hub:{lab:'Maison',val:'3.1 kW',ico:'home'},
ring:[
{pos:'top',lab:'PAC',val:'0.4 kW',ico:'heat',color:'#ef8b2a',dir:'out'},
{pos:'right',lab:'Eau chaude',val:'1.2 kW',ico:'water',color:'#31a3dd',dir:'out'},
{pos:'bottom',lab:'Voiture',val:'0.9 kW',ico:'car',color:'#28a06a',dir:'out'},
{pos:'left',lab:'Autres',val:'0.6 kW',ico:'other',color:'#8a98a3',dir:'out',estim:true}
]},
prod:{title:'Production — détail', hub:{lab:'Solaire',val:'3.8 kW',ico:'sun'},
ring:[
{pos:'top',lab:'Onduleur 1',val:'2.3 kW',ico:'inv',color:'#fec113',dir:'in',state:'ok'},
{pos:'bottom',lab:'Onduleur 2',val:'1.5 kW',ico:'inv',color:'#fec113',dir:'in',state:'ok'}
]}
};
function mkNode(cfg,cls,delay){
const [l,t]=POS[cfg.pos||'hub'];
const n=document.createElement('div');
n.className='node '+(cls||'')+(cfg.estim?' estim':'')+(cfg.tap?' tappable':'');
n.style.left=l+'%'; n.style.top=t+'%';
if(delay!=null)n.style.animationDelay=delay+'ms';
n.innerHTML=(cfg.heos?'<div class="heos-ring"></div>':'')
+`<svg class="ico ${cfg.ico}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${ICONS[cfg.ico]}</svg>`
+(cfg.val?`<div class="val">${cfg.val}</div>`:'')+`<div class="lab">${cfg.lab}</div>`
+(cfg.state?`<span class="state ${cfg.state}"></span>`:'');
if(cfg.tap)n.addEventListener('click',()=>go(cfg.tap));
return n;
}
function render(sceneKey){
const sc=SCENES[sceneKey];
title.textContent=sc.title;
head.classList.toggle('detail',sceneKey!=='overview');
hint.style.visibility = sceneKey==='overview'?'visible':'hidden';
// clear lines + old nodes
lines.innerHTML='';
[...flow.querySelectorAll('.node')].forEach(n=>n.remove());
// lines + particles
sc.ring.forEach(r=>{
const [ne,he]=EDGE[r.pos];
const from=r.dir==='in'?ne:he, to=r.dir==='in'?he:ne;
const id='ln_'+r.pos;
SE('path',{class:'flowline',d:`M${ne[0]} ${ne[1]} L${he[0]} ${he[1]}`},lines);
const mp=SE('path',{id,d:`M${from[0]} ${from[1]} L${to[0]} ${to[1]}`,fill:'none',stroke:'none'},lines);
const g=SE('g',{fill:r.color},lines);
for(let i=0;i<2;i++){const c=SE('circle',{r:1.4},g);
const am=SE('animateMotion',{dur:'2s',begin:(-i)+'s',repeatCount:'indefinite'},c);
SE('mpath',{href:'#'+id},am);}
});
// hub + ring nodes
flow.appendChild(mkNode(sc.hub,'hub enter',0));
sc.ring.forEach((r,i)=>flow.appendChild(mkNode(r,'enter',120+i*60)));
}
function go(key){
// anim out
[...flow.querySelectorAll('.node')].forEach(n=>n.classList.add('leaving'));
lines.style.opacity=0;
setTimeout(()=>{lines.style.opacity=1;render(key);},reduce?0:170);
}
document.getElementById('back').addEventListener('click',()=>go('overview'));
render('overview');
</script>
</body>
</html>