forma-arch 0.1.0 → 0.3.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.
- package/README.md +16 -2
- package/lib/check.mjs +26 -1
- package/lib/cluster.mjs +44 -0
- package/lib/describe.mjs +90 -0
- package/lib/doc.mjs +24 -34
- package/lib/enrich.mjs +0 -0
- package/lib/gen.mjs +37 -7
- package/lib/init.mjs +10 -1
- package/lib/render.mjs +74 -0
- package/lib/schema/c4-model.schema.json +16 -0
- package/lib/viewer/c4-hologram.html +582 -267
- package/package.json +1 -1
|
@@ -1,285 +1,600 @@
|
|
|
1
|
-
<!
|
|
2
|
-
<html lang="
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>C4 Hologram</title>
|
|
6
|
+
<title>Forma · C4 Hologram</title>
|
|
7
7
|
<style>
|
|
8
|
-
:root{color-scheme:
|
|
9
|
-
--bg:#
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--done:#
|
|
13
|
-
--
|
|
14
|
-
*{box-sizing:border-box}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
@keyframes
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
background:
|
|
92
|
-
.
|
|
93
|
-
background:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
@
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
html[data-skin="blueprint"]{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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}
|
|
8
|
+
:root{color-scheme:light;
|
|
9
|
+
--bg:#050d14; --grid:rgba(62,231,255,.04); --halo:rgba(62,231,255,.10);
|
|
10
|
+
--cyan:#3ee7ff; --sec:#6fa8bc; --ter:#4c7f94; --ter2:#41738a; --nm:#bfe9f7;
|
|
11
|
+
--stagebg:rgba(4,12,19,.72); --stageborder:#143547; --nodebg:rgba(8,26,38,.92); --detailbg:rgba(6,20,30,.97);
|
|
12
|
+
--border:#1e4a5e; --done:#2fe08a; --prog:#ffc74d; --next:#3ee7ff; --plan:#2d5a72; --planTxt:#7fb3c6; --prob:#ff5d5d;
|
|
13
|
+
--edge:#2b6e8a; --edgeplan:#264d61; --elbl:#3d7f9c; --ddtxt:#7fb3c6; --now:#cfeefb; --tgt:#8fd8ec; --src:#4fbf8d;}
|
|
14
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
15
|
+
body{font-family:"SF Mono",Menlo,Consolas,ui-monospace,monospace;background:var(--bg);color:var(--nm);overflow-x:hidden}
|
|
16
|
+
#wrap{padding:14px 18px 26px;min-height:100vh;background:
|
|
17
|
+
radial-gradient(ellipse at 50% -10%,var(--halo),transparent 55%),
|
|
18
|
+
repeating-linear-gradient(0deg,transparent,transparent 39px,var(--grid) 40px),
|
|
19
|
+
repeating-linear-gradient(90deg,transparent,transparent 39px,var(--grid) 40px)}
|
|
20
|
+
h1{font-size:16px;letter-spacing:.16em;color:var(--cyan);text-shadow:0 0 14px rgba(62,231,255,.7);text-transform:uppercase}
|
|
21
|
+
.sub{font-size:10.5px;color:var(--sec);margin-top:3px}
|
|
22
|
+
.top{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:8px}
|
|
23
|
+
.ctrls{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
24
|
+
button,select{font:inherit;font-size:10.5px;letter-spacing:.09em;cursor:pointer;border-radius:6px;padding:6px 12px;
|
|
25
|
+
background:transparent;border:1px solid var(--border);color:var(--sec)}
|
|
26
|
+
button:hover,select:hover{background:rgba(62,231,255,.10);color:var(--cyan)}
|
|
27
|
+
button.on{background:rgba(62,231,255,.18);color:var(--cyan);border-color:var(--cyan);text-shadow:0 0 8px rgba(62,231,255,.8)}
|
|
28
|
+
#back{border-color:var(--cyan);color:var(--cyan)}
|
|
29
|
+
#back[disabled]{opacity:.28;cursor:default;border-color:var(--border);color:var(--ter)}
|
|
30
|
+
select#skin option{background:#08202e;color:var(--nm)}
|
|
31
|
+
#crumb{font-size:11px;color:var(--sec);margin:8px 0}
|
|
32
|
+
#crumb b{color:var(--cyan)}
|
|
33
|
+
#crumb a{color:#8fd8ec;cursor:pointer}
|
|
34
|
+
#crumb a:hover{text-decoration:underline}
|
|
35
|
+
#stamp{font-size:9.5px;color:var(--ter2);margin-bottom:6px}
|
|
36
|
+
#stage{position:relative;width:100%;height:74vh;min-height:520px;border:1px solid var(--stageborder);border-radius:12px;background:var(--stagebg);overflow:hidden}
|
|
37
|
+
svg{width:100%;height:100%;display:block}
|
|
38
|
+
.nd{cursor:grab}
|
|
39
|
+
.nd.grabbing{cursor:grabbing}
|
|
40
|
+
.nd rect{fill:var(--nodebg);stroke-width:1.5;transition:filter .2s}
|
|
41
|
+
.nd text{font-family:inherit;pointer-events:none}
|
|
42
|
+
.nd .tt{font-size:11.5px;font-weight:700;letter-spacing:.03em}
|
|
43
|
+
.nd .kk{font-size:7.6px;letter-spacing:.14em;fill:var(--ter)}
|
|
44
|
+
.nd .dd{font-size:9px;fill:var(--ddtxt)}
|
|
45
|
+
.nd .pp{font-size:9px;font-weight:700}
|
|
46
|
+
.nd:hover rect{filter:brightness(1.32)}
|
|
47
|
+
.nd.drill .plus{font-size:9px;fill:var(--cyan);letter-spacing:.1em}
|
|
48
|
+
.nd .drillhit{fill:transparent;stroke:none;pointer-events:auto;cursor:zoom-in}
|
|
49
|
+
.nd .drillhit:hover{fill:rgba(62,231,255,.12)}
|
|
50
|
+
.s-done rect{stroke:var(--done);filter:drop-shadow(0 0 7px rgba(47,224,138,.55))}
|
|
51
|
+
.s-done .tt,.s-done .pp{fill:var(--done)}
|
|
52
|
+
.s-prog rect{stroke:var(--prog);filter:drop-shadow(0 0 7px rgba(255,199,77,.5))}
|
|
53
|
+
.s-prog .tt,.s-prog .pp{fill:var(--prog)}
|
|
54
|
+
.s-next rect{stroke:var(--next);stroke-width:2.2;filter:drop-shadow(0 0 11px rgba(62,231,255,.75))}
|
|
55
|
+
.s-next .tt,.s-next .pp{fill:var(--next)}
|
|
56
|
+
.s-plan rect{stroke:var(--plan);stroke-dasharray:5 4}
|
|
57
|
+
.s-plan .tt,.s-plan .pp{fill:var(--planTxt)}
|
|
58
|
+
.s-prob rect{stroke:var(--prob);filter:drop-shadow(0 0 8px rgba(255,93,93,.6))}
|
|
59
|
+
.s-prob .tt,.s-prob .pp{fill:var(--prob)}
|
|
60
|
+
.mode-t .nd rect{stroke:var(--next)!important;stroke-dasharray:none!important;filter:drop-shadow(0 0 8px rgba(62,231,255,.5))!important}
|
|
61
|
+
.mode-t .nd .tt,.mode-t .nd .pp{fill:var(--next)!important}
|
|
62
|
+
.edge{stroke:var(--edge);stroke-width:1.3;fill:none;marker-end:url(#ar);cursor:help}
|
|
63
|
+
.edge.flow{stroke-dasharray:5 7;animation:dash 1.1s linear infinite}
|
|
64
|
+
.edge.plan{stroke:var(--edgeplan);stroke-dasharray:3 6;opacity:.6;marker-end:url(#arp)}
|
|
65
|
+
.edge.hot{stroke:var(--cyan);stroke-width:2.6;opacity:1}
|
|
66
|
+
.edgehit{stroke:transparent;stroke-width:14;fill:none;cursor:help}
|
|
67
|
+
@keyframes dash{to{stroke-dashoffset:-24}}
|
|
68
|
+
.nd.pulse rect{animation:pul 2.4s ease-in-out infinite}
|
|
69
|
+
@keyframes pul{0%,100%{filter:drop-shadow(0 0 8px rgba(62,231,255,.5))}50%{filter:drop-shadow(0 0 18px rgba(62,231,255,.95))}}
|
|
70
|
+
@keyframes svgin{from{opacity:0;transform:scale(.975)}to{opacity:1;transform:none}}
|
|
71
|
+
@keyframes ndin{from{opacity:0}to{opacity:1}}
|
|
72
|
+
#stage.anim svg{animation:svgin .34s cubic-bezier(.2,.7,.2,1) both;transform-origin:50% 44%}
|
|
73
|
+
#stage.anim .nd{animation:ndin .55s ease both}
|
|
74
|
+
@media(prefers-reduced-motion:reduce){#stage.anim svg,#stage.anim .nd{animation:none}}
|
|
75
|
+
#etip{position:absolute;pointer-events:none;display:none;z-index:6;background:var(--detailbg);border:1px solid var(--border);
|
|
76
|
+
color:var(--nm);font-size:10px;letter-spacing:.03em;padding:3px 8px;border-radius:5px;white-space:nowrap;box-shadow:0 3px 12px rgba(0,0,0,.45)}
|
|
77
|
+
#legend{display:flex;gap:13px;font-size:9.5px;color:var(--sec);margin:10px 0 0;flex-wrap:wrap}
|
|
78
|
+
#legend i{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px}
|
|
79
|
+
.hint{color:var(--ter2)}
|
|
80
|
+
#detail{margin-top:12px;border:1px solid var(--border);border-radius:10px;padding:13px;background:var(--detailbg);display:none}
|
|
81
|
+
#detail h3{color:var(--cyan);font-size:12.5px;margin-bottom:8px;letter-spacing:.05em}
|
|
82
|
+
#detail h3 .cat{color:var(--ter);font-size:9px;letter-spacing:.12em}
|
|
83
|
+
.row{margin-bottom:7px;font-size:10.5px;line-height:1.5}
|
|
84
|
+
.lbl{color:var(--ter2);font-size:8.5px;letter-spacing:.15em;text-transform:uppercase;display:block}
|
|
85
|
+
.func{color:#dff4fc}.now{color:var(--now)}.tgt{color:var(--tgt)}.src{color:var(--src);font-size:9.5px}
|
|
86
|
+
.cl{float:right;color:var(--sec);cursor:pointer;font-size:10px}
|
|
87
|
+
#err{color:var(--prob);font-size:11px;padding:14px}
|
|
88
|
+
/* PRINT / EXPORT menu */
|
|
89
|
+
.menuwrap{position:relative}
|
|
90
|
+
#pmenu{position:absolute;right:0;top:calc(100% + 4px);z-index:9;display:none;flex-direction:column;min-width:150px;
|
|
91
|
+
background:var(--detailbg);border:1px solid var(--border);border-radius:8px;padding:4px;box-shadow:0 6px 18px rgba(0,0,0,.5)}
|
|
92
|
+
#pmenu.open{display:flex}
|
|
93
|
+
#pmenu button{border:none;background:transparent;text-align:left;width:100%;border-radius:5px;padding:7px 10px;color:var(--sec)}
|
|
94
|
+
#pmenu button:hover{background:rgba(62,231,255,.12);color:var(--cyan)}
|
|
95
|
+
/* catalogue (collapsed homogeneous group) node */
|
|
96
|
+
.nd.cat rect{stroke-dasharray:2 3}
|
|
97
|
+
.nd.cat .plus{fill:var(--sec)}
|
|
98
|
+
/* detail LIST variant (catalogue roster) */
|
|
99
|
+
#detail .rostersearch{width:100%;font:inherit;font-size:10.5px;margin:2px 0 10px;padding:6px 9px;border-radius:6px;
|
|
100
|
+
background:transparent;border:1px solid var(--border);color:var(--nm)}
|
|
101
|
+
#detail .roster{max-height:44vh;overflow-y:auto;border-top:1px solid var(--border)}
|
|
102
|
+
#detail .ritem{padding:7px 2px 7px 4px;border-bottom:1px solid rgba(30,74,94,.4);display:block}
|
|
103
|
+
#detail .ritem .rn{color:#dff4fc;font-size:11px;font-weight:700}
|
|
104
|
+
#detail .ritem .rp{float:right;color:var(--sec);font-size:9px;letter-spacing:.08em}
|
|
105
|
+
#detail .ritem .rd{color:var(--ddtxt);font-size:9.5px;line-height:1.45;display:block;margin-top:2px;clear:both}
|
|
106
|
+
#detail .rempty{color:var(--ter2);font-size:10px;padding:10px 4px}
|
|
107
|
+
@media print{
|
|
108
|
+
body{background:#fff}
|
|
109
|
+
#wrap{background:none;padding:0}
|
|
110
|
+
.ctrls,#legend,.sub,#etip,#pmenu{display:none!important}
|
|
111
|
+
#stage{border:1px solid #999;height:auto;min-height:0}
|
|
112
|
+
#stage svg{height:70vh}
|
|
113
|
+
}
|
|
114
|
+
html[data-skin="blueprint"]{--bg:#eef3f9;--grid:rgba(31,119,180,.10);--halo:rgba(31,119,180,.08);
|
|
115
|
+
--cyan:#1f77b4;--sec:#5a6b7d;--ter:#6b7b8c;--ter2:#5a6b7d;--nm:#16273a;
|
|
116
|
+
--stagebg:#ffffff;--stageborder:rgba(31,119,180,.25);--nodebg:#ffffff;--detailbg:#ffffff;--border:rgba(31,119,180,.35);
|
|
117
|
+
--done:#1f9d57;--prog:#c07a12;--next:#1f77b4;--plan:#8592a3;--planTxt:#5a6b7d;--prob:#d1453b;
|
|
118
|
+
--edge:#8aa0b4;--edgeplan:#b9c6d3;--elbl:#5a6b7d;--ddtxt:#40536a;--now:#16273a;--tgt:#1f5f8a;--src:#1f9d57}
|
|
119
|
+
html[data-skin="blueprint"] h1{text-shadow:none}
|
|
120
|
+
html[data-skin="blueprint"] .s-done rect,html[data-skin="blueprint"] .s-prog rect,html[data-skin="blueprint"] .s-next rect,html[data-skin="blueprint"] .s-prob rect{filter:none}
|
|
121
|
+
html[data-skin="blueprint"] .nd.pulse rect{animation:none}
|
|
122
|
+
html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
144
123
|
</style>
|
|
145
124
|
</head>
|
|
146
125
|
<body>
|
|
147
|
-
<div
|
|
126
|
+
<div id="wrap">
|
|
148
127
|
<div class="top">
|
|
149
|
-
<div>
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<button
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
128
|
+
<div><h1 id="title">Forma · C4 Hologram</h1>
|
|
129
|
+
<div class="sub" id="subhelp"></div></div>
|
|
130
|
+
<div class="ctrls">
|
|
131
|
+
<button id="back" type="button" disabled></button>
|
|
132
|
+
<button id="bnow" class="on" type="button"></button>
|
|
133
|
+
<button id="btgt" type="button"></button>
|
|
134
|
+
<button id="brf" type="button"></button>
|
|
135
|
+
<button id="brl" type="button"></button>
|
|
136
|
+
<span class="menuwrap">
|
|
137
|
+
<button id="pexp" type="button" aria-haspopup="true" aria-expanded="false"></button>
|
|
138
|
+
<span id="pmenu" role="menu">
|
|
139
|
+
<button id="pexpSvg" type="button" role="menuitem"></button>
|
|
140
|
+
<button id="pexpPng" type="button" role="menuitem"></button>
|
|
141
|
+
<button id="pexpPrint" type="button" role="menuitem"></button>
|
|
142
|
+
</span>
|
|
143
|
+
</span>
|
|
144
|
+
<select id="skin" title="skin"></select>
|
|
161
145
|
</div>
|
|
162
146
|
</div>
|
|
163
|
-
<div
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
<
|
|
170
|
-
<span style="
|
|
147
|
+
<div id="crumb"></div>
|
|
148
|
+
<div id="stamp"></div>
|
|
149
|
+
<div id="stage"><div id="etip"></div><div id="err" style="display:none"></div></div>
|
|
150
|
+
<div id="legend">
|
|
151
|
+
<span><i style="background:#2fe08a"></i><span id="lg-done"></span></span>
|
|
152
|
+
<span><i style="background:#ffc74d"></i><span id="lg-prog"></span></span>
|
|
153
|
+
<span><i style="background:#3ee7ff"></i><span id="lg-next"></span></span>
|
|
154
|
+
<span><i style="background:#2d5a72"></i><span id="lg-plan"></span></span>
|
|
155
|
+
<span><i style="background:#ff5d5d"></i><span id="lg-prob"></span></span>
|
|
156
|
+
<span class="hint" id="lg-hint"></span>
|
|
171
157
|
</div>
|
|
158
|
+
<div id="detail"></div>
|
|
172
159
|
</div>
|
|
173
|
-
<div class="toast" id="toast"></div>
|
|
174
160
|
<script>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
161
|
+
(function(){
|
|
162
|
+
"use strict";
|
|
163
|
+
// ---- i18n: every user-facing string. en is DEFAULT. ----
|
|
164
|
+
var STRINGS={
|
|
165
|
+
en:{
|
|
166
|
+
subhelp:"click = explain (any level) · double-click or [+] = drill in · drag to move · hover an arrow = label · ESC/BACK",
|
|
167
|
+
back:"← BACK", now:"CURRENT STATE", tgt:"TARGET STATE", reverify:"RE-VERIFY",
|
|
168
|
+
resetLayout:"RESET LAYOUT", resetLayoutTitle:"re-arrange",
|
|
169
|
+
printExport:"PRINT / EXPORT", exportSvg:"Export SVG", exportPng:"Export PNG", print:"Print",
|
|
170
|
+
legDone:"DONE", legProg:"IN PROGRESS", legNext:"NEXT", legPlan:"PLAN ONLY", legProb:"PROBLEM",
|
|
171
|
+
legHint:"— animated arrow = active flow · dashed = to build",
|
|
172
|
+
crumbRoot:"Context", crumbLevel:"C4", lvlContext:"CONTEXT",
|
|
173
|
+
lvlNames:{context:"Context",container:"Container",component:"Components",leaf:"Leaves"},
|
|
174
|
+
detClose:"[x] close", detFunc:"What it does", detNow:"Current state (facts)",
|
|
175
|
+
detTgt:"Target state (project)", detSrc:"Verification source", detIssue:"Issue",
|
|
176
|
+
detTarget:"TARGET",
|
|
177
|
+
rosterSearch:"filter…", rosterCount:"members", rosterEmpty:"no match",
|
|
178
|
+
stampBase:"Fact base:", stampMethod:"method:", stampVerify:"code+topology",
|
|
179
|
+
errEmpty:"Model unreachable or empty.",
|
|
180
|
+
errNotFound:"Model not found (./c4-model.json). Open via `forma serve` or inject window.__C4_MODEL__.",
|
|
181
|
+
errUnavail:"Model unavailable."
|
|
182
|
+
},
|
|
183
|
+
it:{
|
|
184
|
+
subhelp:"click = spiega (ogni livello) · doppio-click o [+] = entra · trascina per spostare · hover su una freccia = etichetta · ESC/BACK",
|
|
185
|
+
back:"← BACK", now:"STATO ATTUALE", tgt:"STATO FINITO", reverify:"RE-VERIFICA",
|
|
186
|
+
resetLayout:"RESET LAYOUT", resetLayoutTitle:"ridisponi",
|
|
187
|
+
printExport:"STAMPA / ESPORTA", exportSvg:"Esporta SVG", exportPng:"Esporta PNG", print:"Stampa",
|
|
188
|
+
legDone:"FATTO", legProg:"IN CORSO", legNext:"PROSSIMO", legPlan:"SOLO PROGETTO", legProb:"PROBLEMA",
|
|
189
|
+
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire",
|
|
190
|
+
crumbRoot:"Contesto", crumbLevel:"C4", lvlContext:"CONTESTO",
|
|
191
|
+
lvlNames:{context:"Contesto",container:"Container",component:"Componenti",leaf:"Foglie"},
|
|
192
|
+
detClose:"[x] chiudi", detFunc:"Cosa fa", detNow:"Stato attuale (fatti)",
|
|
193
|
+
detTgt:"Stato finito (progetto)", detSrc:"Fonte verifica", detIssue:"Issue",
|
|
194
|
+
detTarget:"TARGET",
|
|
195
|
+
rosterSearch:"filtra…", rosterCount:"elementi", rosterEmpty:"nessun risultato",
|
|
196
|
+
stampBase:"Base fatti:", stampMethod:"metodo:", stampVerify:"code+topology",
|
|
197
|
+
errEmpty:"Modello non raggiungibile o vuoto.",
|
|
198
|
+
errNotFound:"Modello non trovato (./c4-model.json). Apri via `forma serve` o inietta window.__C4_MODEL__.",
|
|
199
|
+
errUnavail:"Modello non disponibile."
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var LANG="en", STR=STRINGS.en;
|
|
203
|
+
function pickLang(model){
|
|
204
|
+
var q=null;try{q=new URLSearchParams(location.search).get("lang");}catch(e){}
|
|
205
|
+
var cand=q||window.__C4_LANG__||(model&&model.meta&&model.meta.lang)||"en";
|
|
206
|
+
cand=String(cand).toLowerCase().slice(0,2);
|
|
207
|
+
return STRINGS[cand]?cand:"en";
|
|
208
|
+
}
|
|
209
|
+
var CATALOG_THRESHOLD=24; // group of same-category siblings above this collapses to one catalogue node
|
|
210
|
+
var SKINS=[["holo","◆ holo"],["blueprint","▭ blueprint"]], SKIN_IDS=["holo","blueprint"];
|
|
211
|
+
var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"prob"};
|
|
212
|
+
var M=null, stack=[null], mode="now", POS={}, drag=null, justDragged=false, CATNODES={};
|
|
213
|
+
function $(i){return document.getElementById(i)}
|
|
214
|
+
function esc(s){return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/"/g,""")}
|
|
215
|
+
function setSkin(s){var k=SKIN_IDS.indexOf(s)>-1?s:"holo";document.documentElement.dataset.skin=k;var el=$("skin");if(el)el.value=k;}
|
|
216
|
+
function keyOf(){return stack.join(">")||"root";}
|
|
217
|
+
function childrenOf(pid){var o=[];for(var i=0;i<M.nodes.length;i++){var n=M.nodes[i];if((pid==null&&!n.parent)||n.parent===pid)o.push(n);}return o;}
|
|
218
|
+
// FEATURE #4: collapse large homogeneous catalogs.
|
|
219
|
+
// Given real children, returns render items where any group of >CATALOG_THRESHOLD
|
|
220
|
+
// leaf siblings sharing the same category is replaced by ONE synthetic catalogue node
|
|
221
|
+
// carrying its members (for the LIST detail variant). Real drill-down nodes are never
|
|
222
|
+
// collapsed. Small groups pass through unchanged and keep their id order.
|
|
223
|
+
function collapseCatalogs(pid,kids){
|
|
224
|
+
var groups={},order=[],i;
|
|
225
|
+
for(i=0;i<kids.length;i++){
|
|
226
|
+
var n=kids[i];
|
|
227
|
+
// only leaf-like, non-drillable nodes with a category are collapse candidates
|
|
228
|
+
var collapsible=!hasKids(n.id)&&(n.category!=null&&n.category!=="");
|
|
229
|
+
var key=collapsible?("cat:"+n.category):("solo:"+i);
|
|
230
|
+
if(!groups[key]){groups[key]={key:key,cat:n.category,members:[],collapsible:collapsible};order.push(key);}
|
|
231
|
+
groups[key].members.push(n);
|
|
232
|
+
}
|
|
233
|
+
var out=[];
|
|
234
|
+
for(i=0;i<order.length;i++){
|
|
235
|
+
var g=groups[order[i]];
|
|
236
|
+
if(g.collapsible&&g.members.length>CATALOG_THRESHOLD){
|
|
237
|
+
out.push(makeCatalogNode(pid,g));
|
|
238
|
+
}else{
|
|
239
|
+
for(var j=0;j<g.members.length;j++)out.push(g.members[j]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function makeCatalogNode(pid,g){
|
|
245
|
+
// Aggregate status: worst-of-a-few heuristic — problem > next > in-progress > planned > done.
|
|
246
|
+
var rank={problem:5,next:4,"in-progress":3,planned:2,done:1},best=0,bestKey="done",done=0,tot=g.members.length;
|
|
247
|
+
for(var i=0;i<g.members.length;i++){
|
|
248
|
+
var s2=g.members[i].status2||(g.members[i].status==="planned"?"planned":"done");
|
|
249
|
+
if((rank[s2]||0)>best){best=rank[s2]||0;bestKey=s2;}
|
|
250
|
+
if(s2==="done")done++;
|
|
251
|
+
}
|
|
252
|
+
var lvl=(g.members[0]&&g.members[0].level)||"leaf";
|
|
253
|
+
return {__catalog:true, id:"__cat__"+pid+"__"+g.cat, name:g.cat, category:g.cat, kind:"catalog",
|
|
254
|
+
level:lvl, status2:bestKey, completion:Math.round(done/tot*100),
|
|
255
|
+
statusWord:g.cat+" · "+tot, members:g.members, count:tot};
|
|
256
|
+
}
|
|
257
|
+
function hasKids(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].parent===id)return true;return false;}
|
|
258
|
+
function node(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].id===id)return M.nodes[i];return null;}
|
|
259
|
+
function nameOf(id){var n=node(id);return n?n.name:id;}
|
|
260
|
+
function srcOf(n){if(n.verify){if(typeof n.verify==="string")return n.verify;if(n.verify.source)return n.verify.source;if(n.verify.ref)return n.verify.ref;if(n.verify.issue)return "#"+n.verify.issue;}
|
|
261
|
+
var ev=(n.evidence||[]).filter(function(e){return e.type==="doc"||e.type==="path";})[0];return ev?ev.ref:((M.meta&&M.meta.verifyMethod)||"");}
|
|
262
|
+
|
|
263
|
+
function seedLayout(kids){
|
|
264
|
+
var NW=228,NH=106,pos={},i,hub=null;
|
|
265
|
+
for(i=0;i<kids.length;i++)if(kids[i].kind==="system"){hub=kids[i];break;}
|
|
266
|
+
if(hub&&kids.length<=10){
|
|
267
|
+
var W=1080,H=660,cx=W/2,cy=H/2;
|
|
268
|
+
pos[hub.id]={x:cx-NW/2,y:cy-NH/2,w:NW,h:NH};
|
|
269
|
+
var others=[];for(i=0;i<kids.length;i++)if(kids[i]!==hub)others.push(kids[i]);
|
|
270
|
+
var N=others.length,rx=Math.min(410,150+N*40),ry=Math.min(250,110+N*22);
|
|
271
|
+
for(i=0;i<N;i++){var a=-Math.PI/2+(i/N)*2*Math.PI;
|
|
272
|
+
pos[others[i].id]={x:cx+rx*Math.cos(a)-NW/2,y:cy+ry*Math.sin(a)-NH/2,w:NW,h:NH};}
|
|
273
|
+
return {W:W,H:H,pos:pos};
|
|
274
|
+
}
|
|
275
|
+
var n=kids.length,cols=n<=3?Math.max(n,1):Math.ceil(Math.sqrt(n));if(cols>4)cols=4;
|
|
276
|
+
var rows=Math.ceil(n/cols),gx=96,gy=82,px=64,py=58;
|
|
277
|
+
var blockW=cols*NW+(cols-1)*gx,W=Math.max(1020,blockW+px*2),H=Math.max(560,py*2+rows*NH+(rows-1)*gy);
|
|
278
|
+
var offx=(W-blockW)/2;
|
|
279
|
+
for(i=0;i<n;i++){var c=i%cols,r=Math.floor(i/cols);
|
|
280
|
+
pos[kids[i].id]={x:offx+c*(NW+gx),y:py+r*(NH+gy),w:NW,h:NH};}
|
|
281
|
+
return {W:W,H:H,pos:pos};
|
|
187
282
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
283
|
+
function layoutFor(kids){var k=keyOf();if(!POS[k])POS[k]=seedLayout(kids);
|
|
284
|
+
// make sure every current kid has a slot (model may have changed)
|
|
285
|
+
var lay=POS[k];for(var i=0;i<kids.length;i++)if(!lay.pos[kids[i].id]){var s=seedLayout(kids);POS[k]=s;return s;}return lay;}
|
|
286
|
+
|
|
287
|
+
function draw(animate){
|
|
288
|
+
if(!M)return;
|
|
289
|
+
var pid=stack[stack.length-1],realKids=childrenOf(pid);
|
|
290
|
+
if(!realKids.length){if(stack.length>1){stack.pop();return draw(animate);}}
|
|
291
|
+
var kids=collapseCatalogs(pid,realKids);
|
|
292
|
+
CATNODES={};for(var ci=0;ci<kids.length;ci++)if(kids[ci].__catalog)CATNODES[kids[ci].id]=kids[ci];
|
|
293
|
+
var lay=layoutFor(kids),pos=lay.pos;
|
|
294
|
+
var idset={},memberMap={};
|
|
295
|
+
for(var z=0;z<kids.length;z++){idset[kids[z].id]=1;
|
|
296
|
+
if(kids[z].__catalog)for(var mm=0;mm<kids[z].members.length;mm++)memberMap[kids[z].members[mm].id]=kids[z].id;}
|
|
297
|
+
function mapId(id){return memberMap[id]||id;}
|
|
298
|
+
var edges=(M.edges||[]).map(function(e){return {from:mapId(e.from),to:mapId(e.to),label:e.label,estatus:e.estatus};})
|
|
299
|
+
.filter(function(e){return idset[e.from]&&idset[e.to]&&e.from!==e.to;});
|
|
300
|
+
var s='<svg viewBox="0 0 '+lay.W+' '+lay.H+'" preserveAspectRatio="xMidYMid meet" class="'+(mode==="target"?"mode-t":"")+'">'
|
|
301
|
+
+'<defs><marker id="ar" markerWidth="9" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,9 3.5,0 7" fill="'+cssv("--edge")+'"/></marker>'
|
|
302
|
+
+'<marker id="arp" markerWidth="9" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,9 3.5,0 7" fill="'+cssv("--edgeplan")+'"/></marker></defs>';
|
|
303
|
+
for(var i=0;i<edges.length;i++){
|
|
304
|
+
var a=pos[edges[i].from],b=pos[edges[i].to];if(!a||!b)continue;
|
|
305
|
+
var d=edgePath(a,b),cl=(edges[i].estatus==="to-build"?"plan":"flow");
|
|
306
|
+
s+='<path class="edge '+cl+'" data-e="'+i+'" d="'+d+'"/>';
|
|
307
|
+
s+='<path class="edgehit" data-e="'+i+'" data-label="'+esc(edges[i].label||"")+'" d="'+d+'"/>';
|
|
308
|
+
}
|
|
309
|
+
for(var j=0;j<kids.length;j++){
|
|
310
|
+
var n=kids[j],p=pos[n.id];
|
|
311
|
+
var st=STMAP[n.status2]||(n.status==="planned"?"plan":"done");
|
|
312
|
+
var pulse=(st==="next"||st==="prog")&&mode!=="target"&&!n.__catalog;
|
|
313
|
+
var isCat=!!n.__catalog,dr=!isCat&&hasKids(n.id),cls="nd s-"+st+(dr?" drill":"")+(isCat?" cat":"")+(pulse?" pulse":"");
|
|
314
|
+
var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||n.tech||""));
|
|
315
|
+
var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:(n.statusWord||(n.completion!=null?n.completion+"%":(n.tech||""))));
|
|
316
|
+
var tmax=Math.floor((p.w-70)/6.7),tt=n.name;if(String(tt).length>tmax)tt=String(tt).slice(0,tmax-1)+"…";
|
|
317
|
+
s+='<g class="'+cls+'" data-id="'+esc(n.id)+'">'
|
|
318
|
+
+'<rect x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="10"/>'
|
|
319
|
+
+'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc(n.category||n.kind||"")+'</text>'
|
|
320
|
+
+'<text class="tt" x="'+(p.x+13)+'" y="'+(p.y+33)+'">'+esc(tt)+'</text>'
|
|
321
|
+
+'<text class="pp" x="'+(p.x+p.w-13)+'" y="'+(p.y+16)+'" text-anchor="end">'+esc(badge)+'</text>';
|
|
322
|
+
var words=String(desc).split(" "),line="",ln=0,cpl=Math.floor((p.w-26)/5.4),MAXL=2;
|
|
323
|
+
for(var k=0;k<words.length&&ln<MAXL;k++){
|
|
324
|
+
if((line+words[k]).length>cpl){s+='<text class="dd" x="'+(p.x+13)+'" y="'+(p.y+51+ln*12.5)+'">'+esc(line.trim())+'</text>';line=words[k]+" ";ln++;}
|
|
325
|
+
else line+=words[k]+" ";}
|
|
326
|
+
if(ln<MAXL&&line)s+='<text class="dd" x="'+(p.x+13)+'" y="'+(p.y+51+ln*12.5)+'">'+esc(line.length>cpl?line.slice(0,cpl-1)+"…":line.trim())+'</text>';
|
|
327
|
+
else if(ln===MAXL&&line.trim())s+='<text class="dd" x="'+(p.x+13)+'" y="'+(p.y+51+ln*12.5)+'">…</text>';
|
|
328
|
+
if(dr)s+='<rect class="drillhit" data-drill="1" x="'+(p.x+p.w-96)+'" y="'+(p.y+p.h-25)+'" width="88" height="20" rx="5"/>'
|
|
329
|
+
+'<text class="plus" x="'+(p.x+p.w-13)+'" y="'+(p.y+p.h-9)+'" text-anchor="end">[+] DRILL</text>';
|
|
330
|
+
else if(isCat)s+='<text class="plus" x="'+(p.x+p.w-13)+'" y="'+(p.y+p.h-9)+'" text-anchor="end">[≡] '+esc(String(n.count))+'</text>';
|
|
331
|
+
s+='</g>';
|
|
332
|
+
}
|
|
333
|
+
s+='</svg>';
|
|
334
|
+
var st2=$("stage");
|
|
335
|
+
var etip=$("etip"); // preserve tooltip node
|
|
336
|
+
st2.innerHTML=s;st2.appendChild(etip);
|
|
337
|
+
st2.classList.toggle("anim",animate!==false);
|
|
338
|
+
// breadcrumb
|
|
339
|
+
var chain=[],c=pid;while(c){var nn=node(c);chain.unshift(nn);c=nn?nn.parent:null;}
|
|
340
|
+
var lvl=pid?levelName(kids[0]&&kids[0].level):STR.lvlContext;
|
|
341
|
+
var cb=STR.crumbLevel+" · "+lvl.toUpperCase()+" <a data-d='-1'>"+esc(STR.crumbRoot)+"</a>";
|
|
342
|
+
for(var w=0;w<chain.length;w++)cb+=" › "+(w===chain.length-1?"<b>"+esc(chain[w].name)+"</b>":"<a data-d='"+w+"'>"+esc(chain[w].name)+"</a>");
|
|
343
|
+
$("crumb").innerHTML=cb;
|
|
344
|
+
$("back").disabled=stack.length<2;
|
|
345
|
+
$("detail").style.display="none";
|
|
346
|
+
}
|
|
347
|
+
function edgePath(a,b){
|
|
348
|
+
var ax=a.x+a.w/2,ay=a.y+a.h/2,bx=b.x+b.w/2,by=b.y+b.h/2,dx=bx-ax,dy=by-ay,L=Math.sqrt(dx*dx+dy*dy)||1;
|
|
349
|
+
var x1=ax+(a.w/2+10)*dx/L,y1=ay+(a.h/2+10)*dy/L,x2=bx-(b.w/2+12)*dx/L,y2=by-(b.h/2+12)*dy/L;
|
|
350
|
+
var mx=(x1+x2)/2+(y2-y1)*.12,my=(y1+y2)/2-(x2-x1)*.12;
|
|
351
|
+
return "M"+x1+","+y1+" Q"+mx+","+my+" "+x2+","+y2;
|
|
352
|
+
}
|
|
353
|
+
function levelName(l){return (STR.lvlNames&&STR.lvlNames[l])||l||"—";}
|
|
354
|
+
function cssv(v){try{return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||"#2b6e8a";}catch(e){return "#2b6e8a";}}
|
|
355
|
+
|
|
356
|
+
function showDetail(n){
|
|
357
|
+
if(n&&n.__catalog)return showRoster(n);
|
|
358
|
+
var src=srcOf(n),cur=n.current||n.func||n.tech||"",showSrc=src&&String(cur).indexOf(src)<0;
|
|
359
|
+
$("detail").innerHTML='<span class="cl" id="dc">'+esc(STR.detClose)+'</span><h3>'+esc(n.name)+' <span class="cat">· '+esc(n.category||n.kind||"")+(n.tech?" · "+esc(n.tech):"")+'</span></h3>'
|
|
360
|
+
+(n.func?'<div class="row"><span class="lbl">'+esc(STR.detFunc)+'</span><span class="func">'+esc(n.func)+'</span></div>':'')
|
|
361
|
+
+(cur&&cur!==n.func?'<div class="row"><span class="lbl">'+esc(STR.detNow)+'</span><span class="now">'+esc(cur)+'</span></div>':'')
|
|
362
|
+
+((n.target&&n.target!=="—")?'<div class="row"><span class="lbl">'+esc(STR.detTgt)+'</span><span class="tgt">'+esc(n.target)+'</span></div>':'')
|
|
363
|
+
+(showSrc?'<div class="row"><span class="lbl">'+esc(STR.detSrc)+'</span><span class="src">'+esc(src)+'</span></div>':'')
|
|
364
|
+
+((n.issues&&n.issues.length)?'<div class="row"><span class="lbl">'+esc(STR.detIssue)+'</span><span class="now">'+esc(n.issues.join(", "))+'</span></div>':'');
|
|
365
|
+
$("detail").style.display="block";
|
|
366
|
+
var dc=$("dc");if(dc)dc.addEventListener("click",function(){$("detail").style.display="none";});
|
|
367
|
+
}
|
|
368
|
+
// FEATURE #4: LIST detail variant — searchable roster of a collapsed catalogue's members.
|
|
369
|
+
// Each row keeps the per-leaf name + func/current description (the payoff must survive).
|
|
370
|
+
function showRoster(cat){
|
|
371
|
+
var members=cat.members||[];
|
|
372
|
+
var head='<span class="cl" id="dc">'+esc(STR.detClose)+'</span>'
|
|
373
|
+
+'<h3>'+esc(cat.name)+' <span class="cat">· '+esc(cat.count+" "+STR.rosterCount)+'</span></h3>'
|
|
374
|
+
+'<input class="rostersearch" id="rsq" type="text" placeholder="'+esc(STR.rosterSearch)+'" autocomplete="off">'
|
|
375
|
+
+'<div class="roster" id="rlist"></div>';
|
|
376
|
+
$("detail").innerHTML=head;$("detail").style.display="block";
|
|
377
|
+
var dc=$("dc");if(dc)dc.addEventListener("click",function(){$("detail").style.display="none";});
|
|
378
|
+
function rowHtml(m){
|
|
379
|
+
var d=m.current||m.func||m.tech||"";
|
|
380
|
+
var pct=m.completion!=null?(m.completion+"%"):(m.statusWord||"");
|
|
381
|
+
return '<span class="ritem">'+(pct?'<span class="rp">'+esc(pct)+'</span>':'')
|
|
382
|
+
+'<span class="rn">'+esc(m.name)+'</span>'
|
|
383
|
+
+(d?'<span class="rd">'+esc(d)+'</span>':'')+'</span>';
|
|
384
|
+
}
|
|
385
|
+
function render(q){
|
|
386
|
+
q=String(q||"").toLowerCase();var out="",shown=0,i;
|
|
387
|
+
for(i=0;i<members.length;i++){var m=members[i];
|
|
388
|
+
var hay=((m.name||"")+" "+(m.func||"")+" "+(m.current||"")+" "+(m.tech||"")).toLowerCase();
|
|
389
|
+
if(!q||hay.indexOf(q)>-1){out+=rowHtml(m);shown++;}}
|
|
390
|
+
$("rlist").innerHTML=shown?out:'<div class="rempty">'+esc(STR.rosterEmpty)+'</div>';
|
|
391
|
+
}
|
|
392
|
+
render("");
|
|
393
|
+
var q=$("rsq");if(q)q.addEventListener("input",function(){render(q.value);});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---- interaction: drag nodes + hover edge labels ----
|
|
397
|
+
function svgPt(evt){var svg=$("stage").querySelector("svg");if(!svg||!svg.getScreenCTM)return null;
|
|
398
|
+
var p=svg.createSVGPoint();p.x=evt.clientX;p.y=evt.clientY;try{return p.matrixTransform(svg.getScreenCTM().inverse());}catch(e){return null;}}
|
|
399
|
+
function curPos(){return POS[keyOf()]?POS[keyOf()].pos:null;}
|
|
400
|
+
function clamp(v,lo,hi){return v<lo?lo:v>hi?hi:v;}
|
|
203
401
|
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
402
|
+
function wire(){
|
|
403
|
+
var stage=$("stage");
|
|
404
|
+
stage.addEventListener("mousedown",function(ev){
|
|
405
|
+
if(ev.target.getAttribute&&ev.target.getAttribute("data-drill"))return; // [+] is a click→drill, not a drag grip
|
|
406
|
+
var g=ev.target;while(g&&g!==stage&&!(g.getAttribute&&g.getAttribute("data-id")))g=g.parentNode;
|
|
407
|
+
if(!g||g===stage)return;
|
|
408
|
+
var id=g.getAttribute("data-id"),pos=curPos();if(!pos||!pos[id])return;
|
|
409
|
+
var p=svgPt(ev);if(!p)return;
|
|
410
|
+
drag={id:id,ox:p.x-pos[id].x,oy:p.y-pos[id].y,moved:false};
|
|
411
|
+
g.classList.add("grabbing");ev.preventDefault();
|
|
412
|
+
});
|
|
413
|
+
document.addEventListener("mousemove",function(ev){
|
|
414
|
+
if(!drag)return;var pos=curPos();if(!pos)return;var p=svgPt(ev);if(!p)return;var lay=POS[keyOf()];
|
|
415
|
+
pos[drag.id].x=clamp(p.x-drag.ox,4,lay.W-pos[drag.id].w-4);
|
|
416
|
+
pos[drag.id].y=clamp(p.y-drag.oy,4,lay.H-pos[drag.id].h-4);
|
|
417
|
+
drag.moved=true;draw(false);
|
|
418
|
+
});
|
|
419
|
+
document.addEventListener("mouseup",function(){if(drag){if(drag.moved)justDragged=true;drag=null;}});
|
|
420
|
+
// edge label on hover (invisible fat hit-path)
|
|
421
|
+
stage.addEventListener("mouseover",function(ev){
|
|
422
|
+
if(ev.target.classList&&ev.target.classList.contains("edgehit")){
|
|
423
|
+
var lb=ev.target.getAttribute("data-label");if(!lb)return;
|
|
424
|
+
var real=stage.querySelector('.edge[data-e="'+ev.target.getAttribute("data-e")+'"]');if(real)real.classList.add("hot");
|
|
425
|
+
var t=$("etip");t.textContent=lb;t.style.display="block";moveTip(ev);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
stage.addEventListener("mousemove",function(ev){if($("etip").style.display==="block"&&ev.target.classList&&ev.target.classList.contains("edgehit"))moveTip(ev);});
|
|
429
|
+
stage.addEventListener("mouseout",function(ev){
|
|
430
|
+
if(ev.target.classList&&ev.target.classList.contains("edgehit")){$("etip").style.display="none";
|
|
431
|
+
var real=stage.querySelector('.edge.hot');if(real)real.classList.remove("hot");}
|
|
432
|
+
});
|
|
433
|
+
stage.addEventListener("click",function(ev){
|
|
434
|
+
if(justDragged){justDragged=false;return;}
|
|
435
|
+
var wantDrill=ev.target.getAttribute&&ev.target.getAttribute("data-drill");
|
|
436
|
+
var g=ev.target;while(g&&g!==stage&&!(g.getAttribute&&g.getAttribute("data-id")))g=g.parentNode;
|
|
437
|
+
if(!g||g===stage){ $("detail").style.display="none"; return; }
|
|
438
|
+
var id=g.getAttribute("data-id");
|
|
439
|
+
if(CATNODES[id]){showDetail(CATNODES[id]);return;}
|
|
440
|
+
var n=node(id);if(!n)return;
|
|
441
|
+
// click inspects at EVERY level (parents carry func/current/description too);
|
|
442
|
+
// drilling is the explicit [+] control or a double-click — it no longer steals the click.
|
|
443
|
+
if(wantDrill&&hasKids(id)){stack.push(id);draw(true);return;}
|
|
444
|
+
showDetail(n);
|
|
445
|
+
});
|
|
446
|
+
stage.addEventListener("dblclick",function(ev){
|
|
447
|
+
var g=ev.target;while(g&&g!==stage&&!(g.getAttribute&&g.getAttribute("data-id")))g=g.parentNode;
|
|
448
|
+
if(!g||g===stage)return;
|
|
449
|
+
var id=g.getAttribute("data-id");
|
|
450
|
+
if(CATNODES[id])return; // catalogues open their roster on click; nothing to drill into
|
|
451
|
+
if(hasKids(id)){$("detail").style.display="none";stack.push(id);draw(true);}
|
|
452
|
+
});
|
|
453
|
+
$("crumb").addEventListener("click",function(ev){if(ev.target.tagName==="A"){var d=+ev.target.getAttribute("data-d");
|
|
454
|
+
stack=stack.slice(0,d+2);if(!stack.length)stack=[null];draw(true);}});
|
|
455
|
+
$("back").addEventListener("click",function(){if(stack.length>1){stack.pop();draw(true);}});
|
|
456
|
+
document.addEventListener("keydown",function(e){if(e.key==="Escape"&&stack.length>1){stack.pop();draw(true);}});
|
|
457
|
+
$("bnow").addEventListener("click",function(){mode="now";$("bnow").className="on";$("btgt").className="";draw(false);});
|
|
458
|
+
$("btgt").addEventListener("click",function(){mode="target";$("btgt").className="on";$("bnow").className="";draw(false);});
|
|
459
|
+
$("brf").addEventListener("click",function(){var m=(M&&M.meta)||{};$("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M&&M.source&&M.source.commit)||"—")+" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify);});
|
|
460
|
+
$("brl").addEventListener("click",function(){delete POS[keyOf()];draw(true);});
|
|
461
|
+
$("skin").innerHTML=SKINS.map(function(s){return '<option value="'+s[0]+'">'+s[1]+'</option>';}).join("");
|
|
462
|
+
$("skin").addEventListener("change",function(e){setSkin(e.target.value);});
|
|
463
|
+
wireExport();
|
|
464
|
+
}
|
|
465
|
+
// ---- i18n: apply every static UI string from STR ----
|
|
466
|
+
function applyStrings(){
|
|
467
|
+
function set(id,txt){var e=$(id);if(e)e.textContent=txt;}
|
|
468
|
+
set("subhelp",STR.subhelp);set("back",STR.back);set("bnow",STR.now);set("btgt",STR.tgt);
|
|
469
|
+
set("brf",STR.reverify);set("brl",STR.resetLayout);var brl=$("brl");if(brl)brl.title=STR.resetLayoutTitle;
|
|
470
|
+
set("pexp",STR.printExport);set("pexpSvg",STR.exportSvg);set("pexpPng",STR.exportPng);set("pexpPrint",STR.print);
|
|
471
|
+
set("lg-done",STR.legDone);set("lg-prog",STR.legProg);set("lg-next",STR.legNext);
|
|
472
|
+
set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-hint",STR.legHint);
|
|
473
|
+
}
|
|
474
|
+
// ---- FEATURE #2: PRINT / EXPORT (all client-side, offline) ----
|
|
475
|
+
function exportBaseName(){
|
|
476
|
+
var repo=(M&&M.source&&M.source.repo)||"forma";
|
|
477
|
+
var pid=stack[stack.length-1];
|
|
478
|
+
var lvl=pid?(node(pid)&&node(pid).name)||"level":"context";
|
|
479
|
+
return String(repo+"-"+lvl).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"forma";
|
|
480
|
+
}
|
|
481
|
+
// Collect the CSS rules the SVG needs so a standalone serialize renders styled.
|
|
482
|
+
function collectSvgCss(){
|
|
483
|
+
var out=[],ss=document.styleSheets,i,j;
|
|
484
|
+
for(i=0;i<ss.length;i++){
|
|
485
|
+
var rules;try{rules=ss[i].cssRules;}catch(e){continue;}
|
|
486
|
+
if(!rules)continue;
|
|
487
|
+
for(j=0;j<rules.length;j++){var r=rules[j],t=r.cssText||"";
|
|
488
|
+
// keep rules that target svg-internal classes/elements + the color vars on :root/html
|
|
489
|
+
if(/\.(nd|edge|edgehit|s-done|s-prog|s-next|s-plan|s-prob|mode-t|tt|kk|dd|pp|plus|cat)\b/.test(t)
|
|
490
|
+
||/(^|[\s,])svg\b/.test(t)||/:root|html\[data-skin/.test(t)){
|
|
491
|
+
// drop animations for a static export (keepdefs stable)
|
|
492
|
+
out.push(t);
|
|
493
|
+
}}
|
|
494
|
+
}
|
|
495
|
+
return out.join("\n");
|
|
496
|
+
}
|
|
497
|
+
// Resolve the CURRENT skin's variables from the live document, so a standalone SVG
|
|
498
|
+
// (where `html[data-skin=...]` selectors have no ancestor to match) still renders
|
|
499
|
+
// with the on-screen palette. Returns a "--k:v;--k:v" inline-style string.
|
|
500
|
+
var SVG_VARS=["--bg","--grid","--halo","--cyan","--sec","--ter","--ter2","--nm",
|
|
501
|
+
"--stagebg","--stageborder","--nodebg","--detailbg","--border","--done","--prog",
|
|
502
|
+
"--next","--plan","--planTxt","--prob","--edge","--edgeplan","--elbl","--ddtxt",
|
|
503
|
+
"--now","--tgt","--src"];
|
|
504
|
+
function resolvedVars(){
|
|
505
|
+
var cs;try{cs=getComputedStyle(document.documentElement);}catch(e){return "";}
|
|
506
|
+
var out="";for(var i=0;i<SVG_VARS.length;i++){var v=(cs.getPropertyValue(SVG_VARS[i])||"").trim();if(v)out+=SVG_VARS[i]+":"+v+";";}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
function serializeSvg(){
|
|
510
|
+
var svg=$("stage").querySelector("svg");if(!svg)return null;
|
|
511
|
+
var clone=svg.cloneNode(true);
|
|
512
|
+
clone.setAttribute("xmlns","http://www.w3.org/2000/svg");
|
|
513
|
+
clone.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
|
|
514
|
+
var vb=(svg.getAttribute("viewBox")||"0 0 1080 660").split(/\s+/);
|
|
515
|
+
var w=+vb[2]||1080,h=+vb[3]||660;
|
|
516
|
+
clone.setAttribute("width",w);clone.setAttribute("height",h);
|
|
517
|
+
// Inline the resolved variables on the root so var(--x) resolves standalone,
|
|
518
|
+
// independent of the skin-selector chain that won't exist in the exported file.
|
|
519
|
+
var vars=resolvedVars();if(vars)clone.setAttribute("style",vars);
|
|
520
|
+
// solid dark/light background so the standalone file isn't transparent
|
|
521
|
+
var bg=cssv("--stagebg")||"#050d14";
|
|
522
|
+
var rect='<rect x="0" y="0" width="'+w+'" height="'+h+'" fill="'+bg+'"/>';
|
|
523
|
+
var style="<style>"+collectSvgCss()+"</style>";
|
|
524
|
+
var inner=clone.innerHTML;
|
|
525
|
+
var attrs='';for(var a=0;a<clone.attributes.length;a++){var at=clone.attributes[a];attrs+=' '+at.name+'="'+String(at.value).replace(/"/g,""")+'"';}
|
|
526
|
+
var body='<svg'+attrs+'>'+style+rect+inner+'</svg>';
|
|
527
|
+
return {xml:'<?xml version="1.0" encoding="UTF-8"?>\n'+body,w:w,h:h,bg:bg};
|
|
528
|
+
}
|
|
529
|
+
function triggerDownload(blob,filename){
|
|
530
|
+
var url=URL.createObjectURL(blob),a=document.createElement("a");
|
|
531
|
+
a.href=url;a.download=filename;document.body.appendChild(a);a.click();
|
|
532
|
+
document.body.removeChild(a);setTimeout(function(){URL.revokeObjectURL(url);},4000);
|
|
533
|
+
}
|
|
534
|
+
function exportSvg(){
|
|
535
|
+
var s=serializeSvg();if(!s)return;
|
|
536
|
+
triggerDownload(new Blob([s.xml],{type:"image/svg+xml;charset=utf-8"}),exportBaseName()+".svg");
|
|
537
|
+
}
|
|
538
|
+
function exportPng(){
|
|
539
|
+
var s=serializeSvg();if(!s)return;
|
|
540
|
+
var scale=Math.min(3,Math.max(2,Math.round(2400/Math.max(1,s.w))))||2;
|
|
541
|
+
var cw=Math.round(s.w*scale),ch=Math.round(s.h*scale);
|
|
542
|
+
var img=new Image();
|
|
543
|
+
var url=URL.createObjectURL(new Blob([s.xml],{type:"image/svg+xml;charset=utf-8"}));
|
|
544
|
+
img.onload=function(){
|
|
545
|
+
try{
|
|
546
|
+
var cv=document.createElement("canvas");cv.width=cw;cv.height=ch;
|
|
547
|
+
var ctx=cv.getContext("2d");
|
|
548
|
+
ctx.fillStyle=s.bg||"#050d14";ctx.fillRect(0,0,cw,ch);
|
|
549
|
+
ctx.drawImage(img,0,0,cw,ch);
|
|
550
|
+
URL.revokeObjectURL(url);
|
|
551
|
+
if(cv.toBlob)cv.toBlob(function(b){if(b)triggerDownload(b,exportBaseName()+".png");},"image/png");
|
|
552
|
+
else{var dataUrl=cv.toDataURL("image/png");var a=document.createElement("a");a.href=dataUrl;a.download=exportBaseName()+".png";document.body.appendChild(a);a.click();document.body.removeChild(a);}
|
|
553
|
+
}catch(e){URL.revokeObjectURL(url);}
|
|
554
|
+
};
|
|
555
|
+
img.onerror=function(){URL.revokeObjectURL(url);};
|
|
556
|
+
img.src=url;
|
|
557
|
+
}
|
|
558
|
+
function wireExport(){
|
|
559
|
+
var btn=$("pexp"),menu=$("pmenu");if(!btn||!menu)return;
|
|
560
|
+
function close(){menu.classList.remove("open");btn.setAttribute("aria-expanded","false");}
|
|
561
|
+
function toggle(){var open=menu.classList.toggle("open");btn.setAttribute("aria-expanded",open?"true":"false");}
|
|
562
|
+
btn.addEventListener("click",function(ev){ev.stopPropagation();toggle();});
|
|
563
|
+
document.addEventListener("click",function(ev){if(!menu.contains(ev.target)&&ev.target!==btn)close();});
|
|
564
|
+
document.addEventListener("keydown",function(e){if(e.key==="Escape")close();});
|
|
565
|
+
var sv=$("pexpSvg");if(sv)sv.addEventListener("click",function(){close();exportSvg();});
|
|
566
|
+
var pn=$("pexpPng");if(pn)pn.addEventListener("click",function(){close();exportPng();});
|
|
567
|
+
var pr=$("pexpPrint");if(pr)pr.addEventListener("click",function(){close();window.print();});
|
|
568
|
+
}
|
|
569
|
+
function moveTip(ev){var st=$("stage").getBoundingClientRect(),t=$("etip");
|
|
570
|
+
var x=ev.clientX-st.left+12,y=ev.clientY-st.top+12;
|
|
571
|
+
t.style.left=Math.min(x,st.width-t.offsetWidth-6)+"px";t.style.top=Math.min(y,st.height-t.offsetHeight-6)+"px";}
|
|
572
|
+
|
|
573
|
+
function boot(model){
|
|
574
|
+
M=model;
|
|
575
|
+
LANG=pickLang(M);STR=STRINGS[LANG];
|
|
576
|
+
try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
|
|
577
|
+
applyStrings();
|
|
578
|
+
if(!M||!M.nodes||!M.nodes.length){$("err").style.display="block";$("err").textContent=STR.errEmpty;return;}
|
|
579
|
+
if(M.source&&M.source.repo)$("title").textContent="Forma · "+M.source.repo;
|
|
580
|
+
var m=M.meta||{};$("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M.source&&M.source.commit)||"—")+" · "+(m.verifyMethod||STR.stampVerify);
|
|
581
|
+
setSkin((new URLSearchParams(location.search).get("skin"))||window.__C4_SKIN__||(m.skin)||"holo");
|
|
582
|
+
stack=[null];mode="now";POS={};
|
|
583
|
+
draw(true);
|
|
584
|
+
}
|
|
585
|
+
function start(){
|
|
586
|
+
// Pick language from URL/global up-front so header + errors render even if the model never loads.
|
|
587
|
+
LANG=pickLang(null);STR=STRINGS[LANG];
|
|
588
|
+
try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
|
|
589
|
+
applyStrings();
|
|
590
|
+
wire();
|
|
591
|
+
if(window.__C4_MODEL__){boot(window.__C4_MODEL__);return;}
|
|
592
|
+
try{fetch("./c4-model.json").then(function(r){return r.json();}).then(boot).catch(function(){
|
|
593
|
+
$("err").style.display="block";$("err").textContent=STR.errNotFound;});}
|
|
594
|
+
catch(e){$("err").style.display="block";$("err").textContent=STR.errUnavail;}
|
|
595
|
+
}
|
|
596
|
+
if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",start);else start();
|
|
597
|
+
})();
|
|
283
598
|
</script>
|
|
284
599
|
</body>
|
|
285
600
|
</html>
|