forma-arch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ <!doctype html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>C4 Hologram</title>
7
+ <style>
8
+ :root{color-scheme:dark;
9
+ --bg:#060a10;--ink:#c8d6e6;--dim:#6f8098;--faint:#3a4a63;
10
+ --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,"Liberation Mono",monospace;
11
+ --sans:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;--font:var(--mono);--nodebg:rgba(10,18,28,.72);--nm:#eaf3fb;
12
+ --done:#37d07f;--prog:#e6a545;--next:#4fc8d6;--plan:#7f8ea8;--prob:#f2565b;
13
+ --line:rgba(79,200,214,.12)}
14
+ *{box-sizing:border-box}
15
+ html,body{margin:0;height:100%;background:radial-gradient(1200px 700px at 50% -10%,#0a1420,var(--bg) 60%),var(--bg);
16
+ color:var(--ink);font-family:var(--font);font-size:14px;overflow:hidden}
17
+ body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.5;
18
+ background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);
19
+ background-size:46px 46px;mask:radial-gradient(1100px 640px at 50% 42%,#000 34%,transparent 82%)}
20
+ .app{position:relative;z-index:2;display:flex;flex-direction:column;height:100%;padding:14px 22px 0}
21
+ .top{display:flex;align-items:flex-start;gap:14px}
22
+ h1{margin:0;font-size:22px;letter-spacing:.16em;color:#7fe9f0;text-shadow:0 0 18px rgba(79,200,214,.5);font-weight:700}
23
+ .sub{color:var(--dim);font-size:12px;margin-top:4px;letter-spacing:.03em}
24
+ .crumb{margin-top:5px;font-size:12px;color:var(--dim);letter-spacing:.05em}
25
+ .crumb b{color:#7fe9f0}.crumb a{color:var(--next);cursor:pointer}.crumb .sep{color:var(--faint)}
26
+ .prov{margin-top:4px;font-size:11px;color:var(--faint)}
27
+ .prov.warn{color:var(--prog)}
28
+ .btns{margin-left:auto;display:flex;gap:9px;flex-shrink:0}
29
+ .btn{background:transparent;color:var(--dim);border:1px solid rgba(79,200,214,.28);border-radius:4px;
30
+ padding:8px 13px;font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;transition:.15s}
31
+ .btn:hover{color:#eafaff;border-color:rgba(79,200,214,.6)}
32
+ .btn.on{color:#eafcff;border-color:var(--next);box-shadow:0 0 14px rgba(79,200,214,.35) inset,0 0 10px rgba(79,200,214,.2)}
33
+ .btn:disabled{opacity:.3;cursor:default}
34
+ select.btn.sel{-webkit-appearance:none;appearance:none;padding-right:26px;
35
+ background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);
36
+ background-position:calc(100% - 13px) 52%,calc(100% - 8px) 52%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
37
+ select.btn.sel option{background:#0a121c;color:var(--ink);text-transform:none;letter-spacing:normal}
38
+ html[data-skin="blueprint"] select.btn.sel option{background:#fff;color:var(--ink)}
39
+ .stage-wrap{flex:1;position:relative;margin:12px 0;border:1px solid rgba(79,200,214,.22);border-radius:16px;
40
+ box-shadow:0 0 40px rgba(79,200,214,.06) inset,0 0 30px rgba(0,0,0,.5);overflow:hidden}
41
+ .stage{position:absolute;inset:24px;will-change:transform,opacity}
42
+ svg.edges{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
43
+ .node{position:absolute;transform:translate(-50%,-50%) scale(.94);opacity:0;min-width:210px;max-width:270px;padding:12px 14px 26px;
44
+ border-radius:10px;cursor:pointer;background:var(--nodebg);border:1.5px solid var(--nc,var(--plan));
45
+ box-shadow:0 0 0 1px rgba(0,0,0,.4),0 8px 26px rgba(0,0,0,.5),0 0 22px var(--ng,transparent);
46
+ transition:opacity .4s,transform .4s cubic-bezier(.2,.8,.2,1),box-shadow .18s;backdrop-filter:blur(3px)}
47
+ .node.in{opacity:1;transform:translate(-50%,-50%) scale(1)}
48
+ .node.in:hover{transform:translate(-50%,-50%) scale(1.03)}
49
+ .node.dashed{border-style:dashed;background:rgba(20,26,38,.5)}
50
+ .node.sel{box-shadow:0 0 0 1px var(--nc) inset,0 8px 30px rgba(0,0,0,.55),0 0 34px var(--ng)}
51
+ .node .top2{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
52
+ .node .cat{font-size:10px;letter-spacing:.08em;color:var(--dim)}
53
+ .node .pct{font-size:11px;font-weight:700;color:var(--nc)}
54
+ .node .nm{font-size:15px;font-weight:700;color:var(--nm);margin-top:5px;letter-spacing:.01em}
55
+ .node .ds{font-size:12px;color:var(--dim);margin-top:6px;line-height:1.5}
56
+ .node .drill{position:absolute;right:12px;bottom:8px;font-size:11px;letter-spacing:.08em;color:var(--next)}
57
+ .grid{position:absolute;inset:34px 26px 14px;overflow:auto;display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));align-content:start}
58
+ .cell{opacity:0;transform:translateY(8px);transition:.35s;padding:11px 13px 22px;border-radius:9px;position:relative;cursor:pointer;
59
+ background:var(--nodebg);border:1.4px solid var(--nc,var(--plan));box-shadow:0 0 14px var(--ng,transparent)}
60
+ .cell.in{opacity:1;transform:none}.cell.dashed{border-style:dashed}
61
+ .cell.sel{box-shadow:0 0 0 1px var(--nc) inset,0 0 20px var(--ng)}
62
+ .cell .top2{display:flex;justify-content:space-between;gap:6px}.cell .cat{font-size:9.5px;color:var(--dim)}
63
+ .cell .pct{font-size:10.5px;font-weight:700;color:var(--nc)}.cell .nm{font-size:13px;font-weight:700;color:var(--nm);margin-top:3px}
64
+ .cell .ds{font-size:11px;color:var(--dim);margin-top:4px;line-height:1.45}.cell .drill{position:absolute;right:11px;bottom:7px;font-size:10px;color:var(--next)}
65
+ .elabel{fill:#9fc4cf;font-size:10.5px;font-family:var(--mono)}
66
+ .epill{fill:rgba(6,10,16,.85)}
67
+ @keyframes flow{to{stroke-dashoffset:-30}}
68
+ .eflow{fill:none;stroke:var(--next);stroke-width:1.3;opacity:.6;stroke-dasharray:2 8;animation:flow 1.1s linear infinite;filter:drop-shadow(0 0 3px var(--next))}
69
+ .legend{display:flex;flex-wrap:wrap;gap:16px;align-items:center;padding:9px 2px 12px;font-size:11.5px;color:var(--dim)}
70
+ .legend b{display:inline-flex;align-items:center;gap:6px;font-weight:400}
71
+ .legend i{width:9px;height:9px;border-radius:2px;display:inline-block}
72
+ .dot-done{background:var(--done);box-shadow:0 0 7px var(--done)}.dot-prog{background:var(--prog);box-shadow:0 0 7px var(--prog)}
73
+ .dot-next{background:var(--next);box-shadow:0 0 7px var(--next)}.dot-plan{border:1px dashed var(--plan)}.dot-prob{background:var(--prob);box-shadow:0 0 7px var(--prob)}
74
+ .panel{position:absolute;left:0;right:0;bottom:0;transform:translateY(101%);transition:transform .32s cubic-bezier(.2,.8,.2,1);
75
+ background:rgba(8,13,20,.97);border-top:1px solid var(--nc,rgba(79,200,214,.4));padding:18px 24px 22px;z-index:9;max-height:46%;overflow:auto;box-shadow:0 -14px 40px rgba(0,0,0,.6)}
76
+ .panel.open{transform:translateY(0)}
77
+ .panel .ph{display:flex;justify-content:space-between;align-items:baseline}
78
+ .panel h3{margin:0;font-size:18px;color:var(--nc);font-weight:700;letter-spacing:.02em}
79
+ .panel .close{color:var(--dim);cursor:pointer;font-size:12px}
80
+ .panel .lab{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-top:14px}
81
+ .panel p{margin:5px 0 0;font-size:13.5px;line-height:1.55;color:#dbe7f4}
82
+ .panel a{color:var(--next);text-decoration:none}.panel a:hover{text-shadow:0 0 8px rgba(79,200,214,.6)}
83
+ .panel code{color:#bfe4ea}
84
+ .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:rgba(8,13,20,.96);border:1px solid var(--next);
85
+ color:#eafcff;padding:9px 15px;border-radius:5px;font-size:12px;z-index:20;opacity:0;transition:.3s}.toast.show{opacity:1}
86
+ /* ── cyberpunk pass ────────────────────────────────────────── */
87
+ :root{--mag:#ff5ad6}
88
+ body::before{animation:griddrift 42s linear infinite}
89
+ @keyframes griddrift{to{background-position:46px 46px}}
90
+ body::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:1;opacity:.05;
91
+ background:repeating-linear-gradient(0deg,#7fe9f0 0,#7fe9f0 1px,transparent 1px,transparent 3px)}
92
+ .app::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:1;
93
+ background:radial-gradient(120% 90% at 50% 40%,transparent 62%,rgba(0,0,0,.55) 100%)}
94
+ h1{position:relative;text-shadow:0 0 12px rgba(79,200,214,.75),2px 0 0 rgba(255,90,214,.4),-2px 0 0 rgba(79,200,214,.4)}
95
+ h1::after{content:"_";margin-left:2px;color:#7fe9f0;animation:blink 1.1s step-end infinite}
96
+ @keyframes blink{50%{opacity:0}}
97
+ .stage-wrap{border-color:rgba(79,200,214,.42);box-shadow:0 0 60px rgba(79,200,214,.1) inset,0 0 30px rgba(0,0,0,.5),0 0 26px rgba(79,200,214,.14)}
98
+ .node::before,.node::after{content:"";position:absolute;width:13px;height:13px;pointer-events:none;border:2px solid var(--nc);opacity:.9}
99
+ .node::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
100
+ .node::after{bottom:-1px;right:-1px;border-left:0;border-top:0}
101
+ .cell::before,.cell::after{content:"";position:absolute;width:10px;height:10px;pointer-events:none;border:1.5px solid var(--nc);opacity:.85}
102
+ .cell::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
103
+ .cell::after{bottom:-1px;right:-1px;border-left:0;border-top:0}
104
+ .node .nm{text-shadow:0 0 9px var(--ng)}.node .pct,.cell .pct{text-shadow:0 0 8px var(--nc)}
105
+ .node .drill,.cell .drill{text-shadow:0 0 8px var(--next)}
106
+ .node.in:hover{animation:flick .22s steps(2,end) 1}
107
+ @keyframes flick{50%{opacity:.72;filter:brightness(1.4)}}
108
+ .node.sel,.cell.sel{animation:selpulse 1.5s ease-in-out infinite}
109
+ @keyframes selpulse{50%{box-shadow:0 0 0 1px var(--nc) inset,0 0 48px var(--ng)}}
110
+ .eflow{stroke-width:1.7;opacity:.85}
111
+ .elabel{fill:#bfeaf0}
112
+ .btn.on{text-shadow:0 0 9px rgba(79,200,214,.65)}
113
+ #reverify:hover{color:var(--mag);border-color:var(--mag);text-shadow:0 0 9px rgba(255,90,214,.6)}
114
+ .legend i{box-shadow:0 0 8px currentColor}
115
+ h3{text-shadow:0 0 7px var(--nc)}
116
+ .panel{box-shadow:0 -16px 40px rgba(0,0,0,.72);border-top-width:2px}
117
+ .panel .lab{color:var(--dim)}
118
+ /* ── SKIN: blueprint (clean, light, no decor) — vince sul blocco cyberpunk ── */
119
+ html[data-skin="blueprint"]{color-scheme:light;
120
+ --bg:#eef3f9;--ink:#26374a;--dim:#5a6b7d;--faint:#93a3b4;--font:var(--sans);
121
+ --done:#1f9d57;--prog:#c07a12;--next:#1f77b4;--plan:#8592a3;--prob:#d1453b;
122
+ --line:rgba(31,119,180,.14);--nodebg:#ffffff;--nm:#16273a;--edge:#8aa0b4;--mag:#b5259e}
123
+ html[data-skin="blueprint"] body{background:radial-gradient(1100px 640px at 50% -8%,#ffffff,var(--bg) 62%),var(--bg)}
124
+ html[data-skin="blueprint"] body::before{animation:none;opacity:.7}
125
+ html[data-skin="blueprint"] body::after,html[data-skin="blueprint"] .app::after{display:none}
126
+ html[data-skin="blueprint"] h1{color:#123651;text-shadow:none}
127
+ html[data-skin="blueprint"] h1::after{content:""}
128
+ html[data-skin="blueprint"] .crumb b{color:#1f77b4}
129
+ html[data-skin="blueprint"] .node,html[data-skin="blueprint"] .cell{backdrop-filter:none;box-shadow:0 1px 3px rgba(20,40,70,.12),0 0 0 1px rgba(20,40,70,.05)}
130
+ html[data-skin="blueprint"] .node::before,html[data-skin="blueprint"] .node::after,html[data-skin="blueprint"] .cell::before,html[data-skin="blueprint"] .cell::after{display:none}
131
+ html[data-skin="blueprint"] .node.dashed{background:#f4f7fb}
132
+ html[data-skin="blueprint"] .node .nm,html[data-skin="blueprint"] .cell .nm,html[data-skin="blueprint"] h3{text-shadow:none}
133
+ html[data-skin="blueprint"] .node.sel,html[data-skin="blueprint"] .cell.sel{animation:none;box-shadow:0 0 0 2px var(--nc) inset,0 2px 10px rgba(20,40,70,.18)}
134
+ html[data-skin="blueprint"] .stage-wrap{border-color:rgba(31,119,180,.25);box-shadow:0 1px 8px rgba(20,40,70,.08)}
135
+ html[data-skin="blueprint"] .panel{background:#ffffff;box-shadow:0 -8px 26px rgba(20,40,70,.14)}
136
+ html[data-skin="blueprint"] .panel p{color:#2b3c4e}html[data-skin="blueprint"] .panel code{color:#1f77b4}
137
+ html[data-skin="blueprint"] .epill{fill:#ffffff}html[data-skin="blueprint"] .elabel{fill:#48596b}
138
+ html[data-skin="blueprint"] .eflow{opacity:.45;filter:none}
139
+ html[data-skin="blueprint"] .btn{border-color:rgba(31,119,180,.35)}
140
+ html[data-skin="blueprint"] .btn:hover{color:#0c2c46}
141
+ html[data-skin="blueprint"] .btn.on{text-shadow:none;box-shadow:0 0 0 1px var(--next) inset}
142
+ html[data-skin="blueprint"] .legend i{box-shadow:none}
143
+ html[data-skin="blueprint"] .toast{background:#ffffff;color:#16273a}
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <div class="app">
148
+ <div class="top">
149
+ <div>
150
+ <h1 id="title">C4 · HOLOGRAM</h1>
151
+ <div class="sub">click nodo = dettaglio · nodo con [+] = drill-down · ESC o BACK per risalire</div>
152
+ <div class="crumb" id="crumb"></div>
153
+ <div class="prov" id="prov"></div>
154
+ </div>
155
+ <div class="btns">
156
+ <button class="btn" id="back">← back</button>
157
+ <button class="btn on" id="mAttuale">stato attuale</button>
158
+ <button class="btn" id="mFinito">stato finito</button>
159
+ <button class="btn" id="reverify">re-verifica</button>
160
+ <select class="btn sel" id="skin" title="skin"></select>
161
+ </div>
162
+ </div>
163
+ <div class="stage-wrap">
164
+ <div class="stage" id="stage"><svg class="edges" id="edges"></svg></div>
165
+ <div class="panel" id="panel"></div>
166
+ </div>
167
+ <div class="legend">
168
+ <b><i class="dot-done"></i>fatto (provato)</b><b><i class="dot-prog"></i>in corso</b><b><i class="dot-next"></i>prossimo</b>
169
+ <b><i class="dot-plan"></i>solo progetto</b><b><i class="dot-prob"></i>problema</b>
170
+ <span style="color:var(--faint)">— freccia animata = flusso attivo · tratteggio = da costruire</span>
171
+ </div>
172
+ </div>
173
+ <div class="toast" id="toast"></div>
174
+ <script>
175
+ // Curated positions are OPTIONAL and come from the model (a node's `pos:[x,y]`), never hardcoded
176
+ // here — the viewer stays stack-agnostic. When absent, layout is an auto ring so the edge-drawing
177
+ // view works for ANY repo (else unknown ids would fall back to a grid with no edges).
178
+ function layoutPositions(kids){
179
+ const pos={};
180
+ if(kids.every(n=>Array.isArray(n.pos))){kids.forEach(n=>pos[n.id]=n.pos);return pos;}
181
+ const hub=kids.find(n=>n.kind==='system');
182
+ const ring=kids.filter(n=>n!==hub),N=Math.max(ring.length,1);
183
+ if(hub)pos[hub.id]=[50,49];
184
+ ring.forEach((n,i)=>{const a=-Math.PI/2+(i/N)*2*Math.PI,rx=hub?37:42,ry=hub?33:36;
185
+ pos[n.id]=[Math.round(50+rx*Math.cos(a)),Math.round(50+ry*Math.sin(a))];});
186
+ return pos;
187
+ }
188
+ const COL={done:'var(--done)','in-progress':'var(--prog)',next:'var(--next)',planned:'var(--plan)',problem:'var(--prob)'};
189
+ const GLOW={done:'rgba(55,208,127,.28)','in-progress':'rgba(230,165,69,.28)',next:'rgba(79,200,214,.32)',planned:'transparent',problem:'rgba(242,86,91,.3)'};
190
+ const MODEL_URL="./c4-model.json";
191
+ const SKINS=[['holo','◆ holo'],['blueprint','▭ blueprint']];
192
+ const SKIN_IDS=SKINS.map(s=>s[0]);
193
+ let MODEL=null,cur=null,mode="attuale",stack=[],curKids=[];
194
+ function setSkin(s){const sk=SKIN_IDS.includes(s)?s:'holo';document.documentElement.dataset.skin=sk;const el=$('skin');if(el)el.value=sk;}
195
+ const $=id=>document.getElementById(id),stage=$('stage'),edgesSvg=$('edges'),panel=$('panel');
196
+ const childrenOf=id=>MODEL.nodes.filter(n=>(n.parent||null)===id);
197
+ const visible=n=>true;
198
+ const effStat=n=>mode==="finito"?"done":(n.status2||"done");
199
+ const badge=n=>mode==="finito"?"100%":(n.statusWord?n.statusWord:(n.completion!=null?n.completion+'%':''));
200
+ const rawBadge=n=>n.statusWord?n.statusWord:(n.completion!=null?n.completion+'%':'');
201
+ const ndesc=n=>n.kind==='leaf'?(n.func||n.current||''):(mode==="finito"?(n.target||n.func||''):(n.func||n.current||n.description||''));
202
+ const DEFS='<defs><marker id="ah" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#4fc8d6"/></marker><marker id="ahd" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#7f8ea8"/></marker></defs>';
203
+
204
+ function boot(m){MODEL=m;
205
+ $('skin').innerHTML=SKINS.map(([id,lb])=>`<option value="${id}">${lb}</option>`).join('');
206
+ setSkin((new URLSearchParams(location.search).get('skin'))||window.__C4_SKIN__||(m.meta&&m.meta.skin)||'holo');
207
+ $('title').textContent=(m.source&&m.source.repo?m.source.repo.toUpperCase():'C4')+' · C4 HOLOGRAM';
208
+ const ve=m.meta&&m.meta.verifyError;
209
+ $('prov').textContent='Base fatti '+((m.meta&&m.meta.verifiedAt)||m.generatedAt||'').slice(0,16).replace('T',' ')+'Z · commit '+((m.source&&m.source.commit)||'').slice(0,8)+(ve?' (re-verifica fallita: '+ve+')':'');
210
+ $('prov').className='prov'+(ve?' warn':'');
211
+ new ResizeObserver(()=>{if(curKids.length){edgesSvg.innerHTML=DEFS;drawEdges(curKids,false)}}).observe(stage);
212
+ render(null);}
213
+ function styleVars(el,n){const s=effStat(n);el.style.setProperty('--nc',COL[s]||'var(--plan)');el.style.setProperty('--ng',GLOW[s]||'transparent');}
214
+ function render(pid){cur=pid;closePanel();
215
+ [...stage.querySelectorAll('.node,.grid')].forEach(e=>e.remove());edgesSvg.innerHTML=DEFS;
216
+ const pn=pid?MODEL.nodes.find(n=>n.id===pid):null;
217
+ const lvl=pn?('C4-'+({container:'L2',component:'L3',leaf:'L4'}[childrenOf(pid)[0]?childrenOf(pid)[0].level:'']||'L?')+' · '+pn.name.toUpperCase()):'C4-L1 · CONTESTO';
218
+ renderCrumb(pid,lvl);$('back').disabled=stack.length===0;
219
+ const kids=childrenOf(pid).filter(visible);
220
+ if(kids.length===0||kids.length>12||stage.clientWidth<640){curKids=[];return renderGrid(kids);}
221
+ curKids=kids;const POS=layoutPositions(kids);
222
+ kids.forEach((n,i)=>{const el=document.createElement('div'),hasK=childrenOf(n.id).length>0;
223
+ el.className="node"+((n.status2==='planned'&&mode!=='finito')?' dashed':'')+(hasK?' hasK':'');el.dataset.id=n.id;
224
+ const[x,y]=POS[n.id];el.style.left=x+'%';el.style.top=y+'%';styleVars(el,n);
225
+ el.innerHTML=`<div class="top2"><span class="cat">${n.category||n.kind}</span><span class="pct">${badge(n)}</span></div>
226
+ <div class="nm">${n.name}</div><div class="ds">${ndesc(n)}</div>${hasK?'<span class="drill">[+] DRILL</span>':''}`;
227
+ el.onclick=e=>{e.stopPropagation();hasK?drill(n):select(n);};el.ondblclick=e=>{e.stopPropagation();select(n);};
228
+ stage.appendChild(el);requestAnimationFrame(()=>setTimeout(()=>el.classList.add('in'),70+i*55));});
229
+ requestAnimationFrame(()=>setTimeout(()=>drawEdges(kids,true),150));}
230
+ function renderGrid(kids){const g=document.createElement('div');g.className="grid";
231
+ kids.forEach((n,i)=>{const hasK=childrenOf(n.id).length>0,c=document.createElement('div');
232
+ c.className="cell"+((n.status2==='planned'&&mode!=='finito')?' dashed':'');c.dataset.id=n.id;styleVars(c,n);
233
+ c.innerHTML=`<div class="top2"><span class="cat">${n.category||n.kind}</span><span class="pct">${badge(n)}</span></div>
234
+ <div class="nm">${n.name}</div><div class="ds">${ndesc(n)||n.tech||''}</div>${hasK?'<span class="drill">[+]</span>':''}`;
235
+ c.onclick=()=>{hasK?drill(n):select(n);};c.ondblclick=()=>select(n);
236
+ g.appendChild(c);requestAnimationFrame(()=>setTimeout(()=>c.classList.add('in'),i*16));});
237
+ stage.appendChild(g);}
238
+ function center(id){const el=stage.querySelector(`.node[data-id="${id}"]`);if(!el)return null;const s=stage.getBoundingClientRect(),r=el.getBoundingClientRect();return{x:r.left-s.left+r.width/2,y:r.top-s.top+r.height/2,w:r.width,h:r.height};}
239
+ function drawEdges(kids,anim){const ids=new Set(kids.map(n=>n.id));
240
+ MODEL.edges.filter(e=>ids.has(e.from)&&ids.has(e.to)).forEach((e,i)=>{const A=center(e.from),B=center(e.to);if(!A||!B)return;const ns="http://www.w3.org/2000/svg";
241
+ let x1=A.x,y1=A.y,x2=B.x,y2=B.y;const dx=x2-x1,dy=y2-y1,d=Math.hypot(dx,dy)||1;
242
+ x1+=dx/d*(A.w/2*.72);y1+=dy/d*(A.h/2*.9);x2-=dx/d*(B.w/2*.86);y2-=dy/d*(B.h/2*.95);
243
+ const mx=(x1+x2)/2,my=(y1+y2)/2,cx=mx-dy*.08,cy=my+dx*.08,ds=`M${x1},${y1} Q${cx},${cy} ${x2},${y2}`;
244
+ const build=e.estatus==='to-build';const p=document.createElementNS(ns,'path');p.setAttribute('d',ds);p.setAttribute('fill','none');
245
+ p.setAttribute('stroke',build?'#5a6b82':'#2f6f86');p.setAttribute('stroke-width','1.4');p.setAttribute('marker-end',build?'url(#ahd)':'url(#ah)');
246
+ if(build)p.setAttribute('stroke-dasharray','5 5');edgesSvg.appendChild(p);
247
+ if(!build){const fl=document.createElementNS(ns,'path');fl.setAttribute('d',ds);fl.setAttribute('class','eflow');fl.style.animationDelay=(i*.12)+'s';edgesSvg.appendChild(fl);}
248
+ if(e.label){const w=e.label.length*6.2+10,pill=document.createElementNS(ns,'rect');pill.setAttribute('x',cx-w/2);pill.setAttribute('y',cy-14);pill.setAttribute('width',w);pill.setAttribute('height',15);pill.setAttribute('rx',3);pill.setAttribute('class','epill');edgesSvg.appendChild(pill);
249
+ const t=document.createElementNS(ns,'text');t.setAttribute('x',cx);t.setAttribute('y',cy-3);t.setAttribute('text-anchor','middle');t.setAttribute('class','elabel');t.textContent=e.label;edgesSvg.appendChild(t);}});}
250
+ function zoomTo(o,s,cb){stage.style.transformOrigin=o[0]+'% '+o[1]+'%';stage.style.transition='transform .3s ease,opacity .3s ease';
251
+ requestAnimationFrame(()=>{stage.style.transform='scale('+s+')';stage.style.opacity='0';});
252
+ setTimeout(()=>{stage.style.transition='none';stage.style.transform='none';stage.style.opacity='1';cb();},260);}
253
+ function drill(n){const c=center(n.id);const o=c&&stage.clientWidth?[100*c.x/stage.clientWidth,100*c.y/stage.clientHeight]:(n.pos||[50,45]);zoomTo(o,1.5,()=>{stack.push(cur);render(n.id);});}
254
+ function back(){if(!stack.length)return;zoomTo([50,45],.74,()=>{render(stack.pop());});}
255
+ function select(n){[...stage.querySelectorAll('.sel')].forEach(e=>e.classList.remove('sel'));const el=stage.querySelector(`[data-id="${n.id}"]`);if(el)el.classList.add('sel');
256
+ const base=(MODEL.meta&&MODEL.meta.repoUrl)||'',br=(MODEL.meta&&MODEL.meta.docBranch)||'main';
257
+ panel.style.setProperty('--nc',COL[n.status2]||'var(--next)');
258
+ const v=n.verify||{};const issues=(n.issues||[]).map(x=>x).join(', ');
259
+ const vref=v.ref||'';const vlink=base&&/^(docs|src|scripts|standards)\//.test(vref)?`${base}/blob/${br}/${vref.split(' ')[0]}`:'';
260
+ const cosa=n.func||n.current||n.description||'—';
261
+ const showCur=n.current&&n.current!==cosa&&!/^Esiste:/.test(n.current);
262
+ const showTgt=n.target&&n.target!==''&&n.target!=='Idem.';
263
+ panel.innerHTML=`<div class="ph"><h3>${n.name} <span style="font-size:12px;color:var(--dim)">· ${n.category||n.kind} · ${rawBadge(n)}</span></h3><span class="close" onclick="closePanel()">[x] chiudi</span></div>
264
+ <div class="lab">Cosa fa</div><p>${cosa}</p>
265
+ ${showCur?`<div class="lab">Stato attuale (fatti)</div><p>${n.current}</p>`:''}
266
+ ${showTgt?`<div class="lab">Stato finito (progetto)</div><p>${n.target}</p>`:''}
267
+ <div class="lab">Fonte verifica</div><p>${vlink?`<a href="${vlink}" target="_blank">${vref}</a>`:`<code>${vref||'code+topology'}</code>`} · <code>${((MODEL.meta&&MODEL.meta.verifiedAt)||'').slice(0,16).replace('T',' ')}Z</code></p>
268
+ ${issues?`<div class="lab">Issue</div><p>${issues}</p>`:''}`;
269
+ panel.classList.add('open');}
270
+ function closePanel(){panel.classList.remove('open');[...stage.querySelectorAll('.sel')].forEach(e=>e.classList.remove('sel'));}
271
+ function renderCrumb(pid,lvl){const chain=[];let c=pid;while(c){const n=MODEL.nodes.find(x=>x.id===c);chain.unshift(n);c=n?n.parent:null;}
272
+ $('crumb').innerHTML=`<b>${lvl}</b>&nbsp;&nbsp;<a onclick="jump(null)">Contesto</a>`+chain.map(n=>` <span class="sep">›</span> <a onclick="jump('${n.id}')">${n.name}</a>`).join('');}
273
+ function jump(id){const chain=[];let c=id;while(c){const n=MODEL.nodes.find(x=>x.id===c);chain.unshift(c);c=n?n.parent:null;}stack=id===null?[]:[null,...chain.slice(0,-1)];render(id);}
274
+ function setMode(m){mode=m;$('mAttuale').classList.toggle('on',m==='attuale');$('mFinito').classList.toggle('on',m==='finito');render(cur);}
275
+ let tt;function toast(x){const t=$('toast');t.textContent=x;t.classList.add('show');clearTimeout(tt);tt=setTimeout(()=>t.classList.remove('show'),2800);}
276
+ $('back').onclick=back;$('mAttuale').onclick=()=>setMode('attuale');$('mFinito').onclick=()=>setMode('finito');
277
+ $('reverify').onclick=()=>toast('Re-verifica: '+((MODEL.meta&&MODEL.meta.verifyMethod)||'code+topology')+'. La skill rilancia il check contro GitHub (gh) quando disponibile.');
278
+ $('skin').onchange=(e)=>{setSkin(e.target.value);toast('skin: '+e.target.value);};
279
+ document.addEventListener('keydown',e=>{if(e.key==='Escape'){panel.classList.contains('open')?closePanel():back();}});
280
+ stage.onclick=()=>closePanel();
281
+ (async function(){if(window.__C4_MODEL__)return boot(window.__C4_MODEL__);
282
+ try{const r=await fetch(MODEL_URL,{cache:'no-store'});if(!r.ok)throw 0;boot(await r.json());}catch(_){$('title').textContent='c4-model.json non raggiungibile — servi la cartella o usa la build standalone';}})();
283
+ </script>
284
+ </body>
285
+ </html>
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "forma-arch",
3
+ "version": "0.1.0",
4
+ "description": "Forma — present your architecture instead of slides. An interactive, stack-agnostic C4 explorer (context → container → component → leaf) generated from your code and kept true to it by a deterministic drift check.",
5
+ "keywords": [
6
+ "architecture",
7
+ "c4",
8
+ "arc42",
9
+ "diagram",
10
+ "presentation",
11
+ "explorer",
12
+ "documentation",
13
+ "stack-agnostic",
14
+ "cli"
15
+ ],
16
+ "homepage": "https://github.com/LucaDominici/forma#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/LucaDominici/forma/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/LucaDominici/forma.git"
23
+ },
24
+ "license": "Apache-2.0",
25
+ "author": "Luca Dominici",
26
+ "type": "module",
27
+ "bin": {
28
+ "forma": "bin/forma.mjs"
29
+ },
30
+ "exports": {
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "bin/forma.mjs",
35
+ "lib",
36
+ "LICENSE",
37
+ "NOTICE",
38
+ "README.md"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "scripts": {
44
+ "lint": "node scripts/lint.mjs",
45
+ "test": "node test/run.mjs",
46
+ "prepack": "node scripts/check-clean.mjs"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }