squadrant 0.16.4 → 0.16.6
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/dist/index.js +698 -309
- package/dist/index.js.map +1 -1
- package/dist/squadrantd.js +160 -38
- package/dist/squadrantd.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/captain-ops/SKILL.md +23 -0
- package/plugin/skills/explainer-reel/SKILL.md +160 -0
- package/plugin/skills/explainer-reel/assets/scene-kit.js +196 -0
- package/plugin/skills/explainer-reel/assets/swimlane-preset.html +391 -0
- package/plugin/skills/explainer-reel/assets/theme.css +99 -0
- package/plugin/skills/explainer-reel/examples/jwt-reel.gif +0 -0
- package/plugin/skills/explainer-reel/examples/jwt-reel.html +233 -0
- package/plugin/skills/explainer-reel/references/component-library.md +75 -0
- package/plugin/skills/explainer-reel/references/design-tokens.md +72 -0
- package/plugin/skills/explainer-reel/references/interactive-mode.md +62 -0
- package/plugin/skills/explainer-reel/scripts/README.md +30 -0
- package/plugin/skills/explainer-reel/scripts/contact-sheet.sh +20 -0
- package/plugin/skills/explainer-reel/scripts/render-gif.sh +44 -0
- package/plugin/skills/explainer-reel/scripts/seek-shot.sh +24 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
explainer-reel "swimlane band system-flow" preset — mode=interactive.
|
|
4
|
+
|
|
5
|
+
This is a GENERIC starting template, not a rendered deliverable: the mechanism (bands, grid,
|
|
6
|
+
SVG edges with offset-path flow-particles, click->detail panel, tabs) is reusable; the example
|
|
7
|
+
data below (a generic checkout flow) exists only to demonstrate the schema. Adapt it via
|
|
8
|
+
visual-explainer's generate-web-diagram command — this preset supplies the layout/motion
|
|
9
|
+
grammar so that command doesn't have to invent it from scratch. See ../references/
|
|
10
|
+
interactive-mode.md for the schema reference and composition instructions.
|
|
11
|
+
|
|
12
|
+
Structural pattern credit: this preset generalizes the swimlane mechanism first produced by the
|
|
13
|
+
visual-explainer skill (dev-console IBM Plex theme, CSS-grid bands + SVG offset-path particles);
|
|
14
|
+
explainer-reel packages it as a named, reusable preset on top.
|
|
15
|
+
-->
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20
|
+
<title>Swimlane preset — system-flow (edit me)</title>
|
|
21
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
22
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
23
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
24
|
+
<style>
|
|
25
|
+
:root{
|
|
26
|
+
--bg:#0e1116; --surface:#161c24; --surface2:#1b2330; --border:#263041; --border2:#31405a;
|
|
27
|
+
--text:#e6edf3; --dim:#93a1b0; --faint:#5b6b7d;
|
|
28
|
+
--lane-a:#4aa3ff; --lane-a2:#0f2740;
|
|
29
|
+
--lane-b:#35c46b; --lane-b2:#0e2a1c;
|
|
30
|
+
--shared:#c6d0da; --shared2:#232c39;
|
|
31
|
+
--db:#c99bff; --db2:#241733;
|
|
32
|
+
--store:#e3b341; --device:#8aa0b6;
|
|
33
|
+
--gold:#e3b341; --crit:#ff5d5d; --crit2:#2c1416;
|
|
34
|
+
--sans:"IBM Plex Sans",ui-sans-serif,system-ui,sans-serif;
|
|
35
|
+
--mono:"IBM Plex Mono",ui-monospace,Menlo,monospace;
|
|
36
|
+
}
|
|
37
|
+
*{box-sizing:border-box}
|
|
38
|
+
html,body{margin:0}
|
|
39
|
+
body{background:radial-gradient(1200px 600px at 70% -10%,#141b26,transparent),var(--bg);
|
|
40
|
+
color:var(--text);font-family:var(--sans);-webkit-font-smoothing:antialiased}
|
|
41
|
+
.wrap{max-width:1340px;margin:0 auto;padding:26px 22px 80px}
|
|
42
|
+
header .k{font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--gold);text-transform:uppercase}
|
|
43
|
+
h1{font-size:27px;margin:6px 0 4px;letter-spacing:-.02em;font-weight:700}
|
|
44
|
+
.lede{color:var(--dim);font-size:14px;max-width:820px;margin:0}
|
|
45
|
+
.lede b{color:var(--text)}
|
|
46
|
+
|
|
47
|
+
.tabs{display:flex;flex-wrap:wrap;gap:8px;margin:22px 0 6px}
|
|
48
|
+
.tab{font-family:var(--sans);font-size:13px;font-weight:600;color:var(--dim);background:var(--surface);
|
|
49
|
+
border:1px solid var(--border);border-radius:999px;padding:8px 15px;cursor:pointer;transition:.18s;display:flex;align-items:center;gap:8px}
|
|
50
|
+
.tab:hover{border-color:var(--border2);color:var(--text)}
|
|
51
|
+
.tab.on{color:#0e1116;background:linear-gradient(180deg,#f0d68a,var(--gold));border-color:var(--gold)}
|
|
52
|
+
.tab .n{font-family:var(--mono);font-size:11px;opacity:.7}
|
|
53
|
+
|
|
54
|
+
.bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:10px 0 4px;color:var(--dim);font-size:12.5px}
|
|
55
|
+
.btn{font-family:var(--sans);font-size:12.5px;font-weight:600;color:var(--text);background:var(--surface2);
|
|
56
|
+
border:1px solid var(--border2);border-radius:8px;padding:7px 13px;cursor:pointer;display:inline-flex;align-items:center;gap:7px;transition:.15s}
|
|
57
|
+
.btn:hover{background:#232d3d;border-color:#3d4f6d}
|
|
58
|
+
.btn.play{color:#0e1116;background:linear-gradient(180deg,#5ab0ff,var(--lane-a));border-color:var(--lane-a)}
|
|
59
|
+
.seg{display:inline-flex;border:1px solid var(--border2);border-radius:8px;overflow:hidden}
|
|
60
|
+
.seg button{font-family:var(--sans);font-size:12px;font-weight:600;color:var(--dim);background:var(--surface);border:0;padding:7px 13px;cursor:pointer;transition:.15s}
|
|
61
|
+
.seg button.on{color:#0e1116}
|
|
62
|
+
.seg button.on.a{background:var(--lane-a)} .seg button.on.b{background:var(--lane-b)}
|
|
63
|
+
.seg.hide{display:none}
|
|
64
|
+
.leg{display:flex;gap:13px;flex-wrap:wrap;margin-left:auto}
|
|
65
|
+
.leg span{display:inline-flex;align-items:center;gap:6px}
|
|
66
|
+
.sw{width:11px;height:11px;border-radius:3px;display:inline-block}
|
|
67
|
+
|
|
68
|
+
.flowtitle{font-size:15px;font-weight:600;margin:16px 2px 2px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
69
|
+
.flowtitle .why{font-weight:400;color:var(--dim);font-size:13px}
|
|
70
|
+
.stagewrap{position:relative;margin-top:12px;border:1px solid var(--border);border-radius:16px;
|
|
71
|
+
background:linear-gradient(180deg,#12181f,#0f141b);overflow:hidden}
|
|
72
|
+
.stage{position:relative;padding:16px 18px}
|
|
73
|
+
.bands{position:absolute;inset:16px 18px;z-index:0;pointer-events:none}
|
|
74
|
+
.band{position:absolute;left:0;right:0;border-radius:10px;border:1px solid transparent}
|
|
75
|
+
.bandlabel{position:absolute;left:8px;top:8px;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;
|
|
76
|
+
text-transform:uppercase;display:flex;align-items:center;gap:7px;color:var(--dim)}
|
|
77
|
+
.bandlabel .ic{font-size:13px}
|
|
78
|
+
svg.edges{position:absolute;inset:16px 18px;z-index:1;pointer-events:none;overflow:visible}
|
|
79
|
+
.grid{position:relative;display:grid;gap:30px 46px;z-index:2}
|
|
80
|
+
|
|
81
|
+
.node{position:relative;border-radius:12px;padding:11px 13px;border:1px solid var(--border2);
|
|
82
|
+
background:var(--surface);cursor:pointer;transition:transform .16s,box-shadow .16s,border-color .16s,opacity .16s;
|
|
83
|
+
min-width:0;align-self:center;max-width:230px}
|
|
84
|
+
.node:hover{transform:translateY(-2px)}
|
|
85
|
+
.node .lane{position:absolute;top:-1px;left:-1px;bottom:-1px;width:4px;border-radius:12px 0 0 12px}
|
|
86
|
+
.node .step{position:absolute;top:-9px;left:-9px;width:22px;height:22px;border-radius:50%;
|
|
87
|
+
font-family:var(--mono);font-size:11px;font-weight:600;display:flex;align-items:center;justify-content:center;
|
|
88
|
+
background:#0e1116;border:1px solid var(--border2);color:var(--dim);z-index:3}
|
|
89
|
+
.node .t{font-weight:600;font-size:12.5px;line-height:1.28;display:block}
|
|
90
|
+
.node .s{font-family:var(--mono);font-size:10px;color:var(--dim);display:block;margin-top:3px;word-break:break-word;line-height:1.4}
|
|
91
|
+
.node .badge{position:absolute;top:8px;right:9px;font-family:var(--mono);font-size:9px;padding:1px 5px;border-radius:4px}
|
|
92
|
+
.node .svc{display:inline-block;font-family:var(--mono);font-size:9px;margin-top:6px;padding:1px 6px;border-radius:5px;
|
|
93
|
+
color:var(--store);background:color-mix(in srgb,var(--store) 14%,transparent);border:1px solid color-mix(in srgb,var(--store) 32%,transparent)}
|
|
94
|
+
.node.crit .svc{color:var(--crit);background:var(--crit2);border-color:#5c2a2a}
|
|
95
|
+
.node.a{--c:var(--lane-a)} .node.b{--c:var(--lane-b)} .node.shared{--c:var(--shared)} .node.db{--c:var(--db)} .node.crit{--c:var(--crit)} .node.store{--c:var(--store)}
|
|
96
|
+
.node .lane{background:var(--c)}
|
|
97
|
+
.node.on{border-color:var(--c);box-shadow:0 0 0 1px var(--c),0 10px 28px -12px var(--c)}
|
|
98
|
+
.node.dim{opacity:.3}
|
|
99
|
+
.node.crit{background:var(--crit2);border-color:#5c2a2a}
|
|
100
|
+
.node.crit .t{color:#ffd0d0}
|
|
101
|
+
.node.db{background:var(--db2);border-color:#3d2657}
|
|
102
|
+
.badge.a{background:var(--lane-a2);color:var(--lane-a)} .badge.b{background:var(--lane-b2);color:var(--lane-b)}
|
|
103
|
+
.badge.warn{background:var(--crit2);color:var(--crit)} .badge.ok{background:var(--lane-b2);color:var(--lane-b)}
|
|
104
|
+
.node.pulse .lane{animation:lane 1.6s ease-in-out infinite}
|
|
105
|
+
@keyframes lane{0%,100%{opacity:.5}50%{opacity:1}}
|
|
106
|
+
|
|
107
|
+
.pt{position:absolute;width:7px;height:7px;border-radius:50%;z-index:1;
|
|
108
|
+
filter:drop-shadow(0 0 5px currentColor);will-change:offset-distance}
|
|
109
|
+
@keyframes flow{from{offset-distance:0%}to{offset-distance:100%}}
|
|
110
|
+
.elabel{font-family:var(--mono);font-size:9.5px;fill:var(--dim)}
|
|
111
|
+
.elabbg{fill:#0f141b;opacity:.9}
|
|
112
|
+
|
|
113
|
+
.detail{margin-top:14px;border:1px solid var(--border);border-radius:14px;background:var(--surface);
|
|
114
|
+
padding:15px 17px;min-height:78px}
|
|
115
|
+
.detail.empty{color:var(--faint);font-size:13px;display:flex;align-items:center;gap:9px}
|
|
116
|
+
.detail .dh{display:flex;align-items:center;gap:10px;margin-bottom:7px;flex-wrap:wrap}
|
|
117
|
+
.detail .dh .dt{font-size:15px;font-weight:700}
|
|
118
|
+
.detail .sysbadge{font-family:var(--mono);font-size:10.5px;padding:2px 8px;border-radius:5px;border:1px solid currentColor}
|
|
119
|
+
.detail .path{font-family:var(--mono);font-size:11.5px;color:#a5d6ff;background:#0b1a2b;padding:2px 8px;border-radius:5px}
|
|
120
|
+
.detail p{margin:6px 0 0;font-size:13px;color:#cdd6e0;line-height:1.55}
|
|
121
|
+
.detail .writes{font-family:var(--mono);font-size:11px;color:var(--lane-b);margin-top:8px;display:block}
|
|
122
|
+
.detail .flag{margin-top:9px;font-size:12.5px;color:#ffc0c0;background:var(--crit2);border:1px solid #5c2a2a;border-radius:8px;padding:8px 11px}
|
|
123
|
+
.detail .flag b{color:var(--crit)}
|
|
124
|
+
.foot{font-family:var(--mono);font-size:11px;color:var(--faint);margin-top:20px;text-align:center}
|
|
125
|
+
@media (prefers-reduced-motion: reduce){.pt{display:none!important}.node.pulse .lane{animation:none}}
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div class="wrap">
|
|
130
|
+
<header>
|
|
131
|
+
<div class="k">example · swimlane preset</div>
|
|
132
|
+
<h1>Checkout — who calls whom</h1>
|
|
133
|
+
<p class="lede">Nodes sit in the <b>system</b> that runs them — the browser, the payment provider, the order API, the database.
|
|
134
|
+
An arrow that <b>jumps a band is a network hop</b> — its label says what's sent. Click a node for detail.</p>
|
|
135
|
+
</header>
|
|
136
|
+
|
|
137
|
+
<div class="tabs" id="tabs"></div>
|
|
138
|
+
|
|
139
|
+
<div class="bar">
|
|
140
|
+
<button class="btn play" id="play">▶ Animate flow</button>
|
|
141
|
+
<button class="btn" id="reset">↺ Reset</button>
|
|
142
|
+
<div class="seg" id="platseg">
|
|
143
|
+
<button data-p="card" class="a">Card</button>
|
|
144
|
+
<button data-p="wallet" class="b">Wallet</button>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="leg">
|
|
147
|
+
<span><i class="sw" style="background:var(--lane-a)"></i>Card path</span>
|
|
148
|
+
<span><i class="sw" style="background:var(--lane-b)"></i>Wallet path</span>
|
|
149
|
+
<span><i class="sw" style="background:var(--store)"></i>Provider</span>
|
|
150
|
+
<span><i class="sw" style="background:var(--crit)"></i>Gotcha</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="flowtitle" id="ftitle"></div>
|
|
155
|
+
<div class="stagewrap">
|
|
156
|
+
<div class="stage">
|
|
157
|
+
<div class="bands" id="bands"></div>
|
|
158
|
+
<svg class="edges" id="edges"></svg>
|
|
159
|
+
<div class="grid" id="grid"></div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="detail empty" id="detail">◔ Click a node — the panel shows which system runs it and what crosses the wire.</div>
|
|
164
|
+
|
|
165
|
+
<div class="foot">explainer-reel · mode=interactive · swimlane preset — replace BANDS/FLOWS with your own system/flow data</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<script>
|
|
169
|
+
// ---- REPLACE EVERYTHING BELOW WITH YOUR OWN BANDS / FLOWS ----
|
|
170
|
+
// BANDS: the horizontal swimlanes, one per system. Order = render order top-to-bottom.
|
|
171
|
+
const BANDS=[
|
|
172
|
+
{id:'browser',label:'Browser · device',ic:'💻',col:'var(--device)'},
|
|
173
|
+
{id:'provider',label:'Payment provider',ic:'☁️',col:'var(--store)'},
|
|
174
|
+
{id:'api', label:'Order API',ic:'🖥️',col:'var(--lane-a)'},
|
|
175
|
+
{id:'db', label:'Database',ic:'🗄️',col:'var(--db)'}
|
|
176
|
+
];
|
|
177
|
+
const LANECOL={a:'var(--lane-a)',b:'var(--lane-b)',shared:'var(--shared)',db:'var(--db)',crit:'var(--crit)',store:'var(--store)'};
|
|
178
|
+
|
|
179
|
+
// n(sys, lane, col, title, subtitle, extras) — extras: {id, detail, path, writes, flag, badge, ok, svc}
|
|
180
|
+
function n(sys,lane,col,t,s,x={}){return Object.assign({sys,lane,col,t,s},x);}
|
|
181
|
+
|
|
182
|
+
const FLOWS={
|
|
183
|
+
checkout:{ title:"Checkout", why:"card vs. wallet — same API, different provider hop", plat:true,
|
|
184
|
+
card:{ nodes:[
|
|
185
|
+
n('browser','a',1,'Tap "Pay"','CheckoutView -> PaymentClient.charge()',{id:'d',svc:'Web · Checkout UI'}),
|
|
186
|
+
n('provider','store',2,'Provider charges card, returns token','Card Processor API',{id:'s1',svc:'Provider · Cards'}),
|
|
187
|
+
n('api','a',3,'POST /order/confirm','order.service.ts · confirmPayment',{id:'o1',path:'POST /order/confirm'}),
|
|
188
|
+
n('api','shared',4,'markPaid() + emit receipt','order.service.ts',{id:'o2',writes:'writes Order + Payment',ok:true}),
|
|
189
|
+
n('db','db',5,'Order · Payment','Postgres',{id:'db',detail:'Order marked paid; receipt emitted.'})
|
|
190
|
+
], edges:[['d','s1','a','charge card'],['s1','o1','a','payment token (via app)'],['o1','o2','a','confirm'],['o2','db','a','write']]},
|
|
191
|
+
wallet:{ nodes:[
|
|
192
|
+
n('browser','b',1,'Tap "Pay with wallet"','CheckoutView -> WalletClient.charge()',{id:'d',svc:'Web · Checkout UI'}),
|
|
193
|
+
n('provider','store',2,'Wallet provider authorizes, returns receipt id','Wallet API',{id:'s1',svc:'Provider · Wallet'}),
|
|
194
|
+
n('api','b',3,'POST /order/confirm-wallet','order.service.ts · confirmWalletPayment',{id:'o1',path:'POST /order/confirm-wallet'}),
|
|
195
|
+
n('api','crit',4,'markPaid() — no idempotency key','order.service.ts',{id:'o2',flag:'The wallet path does not check the provider receipt id before writing. A retried webhook can double-charge the order record — the card path guards this with an idempotency key, the wallet path doesn\'t (yet).'}),
|
|
196
|
+
n('db','db',5,'Order (possible duplicate)','Postgres',{id:'db'})
|
|
197
|
+
], edges:[['d','s1','b','authorize'],['s1','o1','b','receipt id (via app)'],['o1','o2','b','confirm'],['o2','db','crit','write (unguarded)']]}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const ORDER=[["checkout","Checkout"]];
|
|
202
|
+
let cur="checkout", plat="card";
|
|
203
|
+
|
|
204
|
+
const tabs=document.getElementById('tabs');
|
|
205
|
+
ORDER.forEach(([id,label],i)=>{
|
|
206
|
+
const b=document.createElement('button');
|
|
207
|
+
b.className='tab'+(id===cur?' on':''); b.dataset.id=id;
|
|
208
|
+
b.innerHTML=`<span class="n">${String.fromCharCode(65+i)}</span>${label}`;
|
|
209
|
+
b.onclick=()=>{cur=id;render();};
|
|
210
|
+
tabs.appendChild(b);
|
|
211
|
+
});
|
|
212
|
+
const platseg=document.getElementById('platseg');
|
|
213
|
+
platseg.querySelectorAll('button').forEach(b=>b.onclick=()=>{plat=b.dataset.p;render();});
|
|
214
|
+
|
|
215
|
+
const grid=document.getElementById('grid'), svg=document.getElementById('edges'),
|
|
216
|
+
bandsEl=document.getElementById('bands'), detail=document.getElementById('detail'),
|
|
217
|
+
ftitle=document.getElementById('ftitle');
|
|
218
|
+
|
|
219
|
+
function variant(){const f=FLOWS[cur];return f.plat?f[plat]:f.single;}
|
|
220
|
+
|
|
221
|
+
function render(){
|
|
222
|
+
[...tabs.children].forEach(t=>t.classList.toggle('on',t.dataset.id===cur));
|
|
223
|
+
const f=FLOWS[cur];
|
|
224
|
+
platseg.classList.toggle('hide',!f.plat);
|
|
225
|
+
platseg.querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.p===plat));
|
|
226
|
+
ftitle.innerHTML=`${f.title} <span class="why">— ${f.why}</span>`;
|
|
227
|
+
const v=variant();
|
|
228
|
+
const cols=[...new Set(v.nodes.map(n=>n.col))].sort((a,b)=>a-b);
|
|
229
|
+
const colMap={}; cols.forEach((c,i)=>colMap[c]=i+1);
|
|
230
|
+
const usedBands=[...new Set(v.nodes.map(n=>n.sys))];
|
|
231
|
+
const bandOrder=BANDS.filter(b=>usedBands.includes(b.id));
|
|
232
|
+
const rowMap={}; bandOrder.forEach((b,i)=>rowMap[b.id]=i+1);
|
|
233
|
+
grid.style.gridTemplateColumns=`repeat(${cols.length},minmax(150px,1fr))`;
|
|
234
|
+
grid.style.gridTemplateRows=`repeat(${bandOrder.length},minmax(74px,auto))`;
|
|
235
|
+
grid.innerHTML='';
|
|
236
|
+
v.nodes.forEach(nd=>{
|
|
237
|
+
const el=document.createElement('div');
|
|
238
|
+
el.className='node '+nd.lane+(nd.flag?' pulse':'');
|
|
239
|
+
el.style.gridColumn=colMap[nd.col]; el.style.gridRow=rowMap[nd.sys];
|
|
240
|
+
el.dataset.id=nd.id;
|
|
241
|
+
const badge=nd.badge?`<span class="badge ${nd.badge[0]}">${nd.badge[1]}</span>`
|
|
242
|
+
:(nd.ok?`<span class="badge ok">ok</span>`:(nd.flag?`<span class="badge warn">!</span>`:''));
|
|
243
|
+
const st=stepIndex(v,nd.id);
|
|
244
|
+
el.innerHTML=`<span class="lane"></span>${st!==''?`<span class="step">${st}</span>`:''}${badge}
|
|
245
|
+
<span class="t">${nd.t}</span><span class="s">${nd.s}</span>${nd.svc?`<span class="svc">${nd.svc}</span>`:''}`;
|
|
246
|
+
el.onclick=()=>select(nd.id);
|
|
247
|
+
grid.appendChild(el);
|
|
248
|
+
});
|
|
249
|
+
requestAnimationFrame(()=>{drawBands(bandOrder);drawEdges(v);});
|
|
250
|
+
detail.className='detail empty';
|
|
251
|
+
detail.innerHTML='◔ Click a node — the panel shows which system runs it and what crosses the wire.';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function drawBands(bandOrder){
|
|
255
|
+
bandsEl.innerHTML='';
|
|
256
|
+
const host=bandsEl.getBoundingClientRect();
|
|
257
|
+
const rows={};
|
|
258
|
+
[...grid.children].forEach(el=>{
|
|
259
|
+
const r=el.style.gridRow, rr=el.getBoundingClientRect();
|
|
260
|
+
(rows[r]=rows[r]||[]).push(rr);
|
|
261
|
+
});
|
|
262
|
+
Object.keys(rows).forEach(r=>{
|
|
263
|
+
const rr=rows[r]; const top=Math.min(...rr.map(x=>x.top))-host.top-8;
|
|
264
|
+
const bot=Math.max(...rr.map(x=>x.bottom))-host.top+8;
|
|
265
|
+
const b=bandOrder[+r-1];
|
|
266
|
+
const div=document.createElement('div'); div.className='band';
|
|
267
|
+
div.style.top=top+'px'; div.style.height=(bot-top)+'px';
|
|
268
|
+
div.style.background=`color-mix(in srgb, ${b.col} 7%, transparent)`;
|
|
269
|
+
div.style.borderColor=`color-mix(in srgb, ${b.col} 22%, transparent)`;
|
|
270
|
+
const lbl=document.createElement('div'); lbl.className='bandlabel';
|
|
271
|
+
lbl.style.color=b.col; lbl.innerHTML=`<span class="ic">${b.ic}</span>${b.label}`;
|
|
272
|
+
div.appendChild(lbl); bandsEl.appendChild(div);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function stepIndex(v,id){
|
|
277
|
+
const seen=new Set(),order=[];
|
|
278
|
+
const starts=v.nodes.filter(n=>!v.edges.some(e=>e[1]===n.id)).map(n=>n.id);
|
|
279
|
+
const adj={}; v.edges.forEach(([a,b])=>{(adj[a]=adj[a]||[]).push(b);});
|
|
280
|
+
starts.forEach(s=>{let q=[s];while(q.length){const x=q.shift();if(seen.has(x))continue;seen.add(x);order.push(x);(adj[x]||[]).forEach(y=>q.push(y));}});
|
|
281
|
+
const i=order.indexOf(id);return i<0?'':i+1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function anchor(el,side){
|
|
285
|
+
const host=svg.getBoundingClientRect(), r=el.getBoundingClientRect();
|
|
286
|
+
return {x:(side==='r'?r.right:r.left)-host.left, y:r.top-host.top+r.height/2,
|
|
287
|
+
t:r.top-host.top, b:r.bottom-host.top, cx:r.left-host.left+r.width/2};
|
|
288
|
+
}
|
|
289
|
+
let particles=[];
|
|
290
|
+
function clearParticles(){particles.forEach(p=>p.remove());particles=[];}
|
|
291
|
+
function drawEdges(v){
|
|
292
|
+
svg.removeAttribute('viewBox');
|
|
293
|
+
svg.innerHTML=''; clearParticles();
|
|
294
|
+
v.edges.forEach(([a,b,style,label],idx)=>{
|
|
295
|
+
const ea=grid.querySelector(`[data-id="${a}"]`), eb=grid.querySelector(`[data-id="${b}"]`);
|
|
296
|
+
if(!ea||!eb)return;
|
|
297
|
+
const ra=ea.getBoundingClientRect(), rb=eb.getBoundingClientRect();
|
|
298
|
+
let p1,p2,d;
|
|
299
|
+
if(rb.left>=ra.right-4){
|
|
300
|
+
p1=anchor(ea,'r'); p2=anchor(eb,'l');
|
|
301
|
+
const dx=Math.max(34,(p2.x-p1.x)*0.5);
|
|
302
|
+
d=`M ${p1.x} ${p1.y} C ${p1.x+dx} ${p1.y}, ${p2.x-dx} ${p2.y}, ${p2.x} ${p2.y}`;
|
|
303
|
+
} else {
|
|
304
|
+
const A=anchor(ea,'r'),B=anchor(eb,'r');
|
|
305
|
+
const x=A.cx; const y1=(A.b<B.t)?A.b:A.t, y2=(A.b<B.t)?B.t:B.b;
|
|
306
|
+
d=`M ${x} ${y1} C ${x} ${(y1+y2)/2}, ${x} ${(y1+y2)/2}, ${x} ${y2}`;
|
|
307
|
+
p1={x,y:y1};p2={x,y:y2};
|
|
308
|
+
}
|
|
309
|
+
const col=LANECOL[style];
|
|
310
|
+
const path=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
311
|
+
path.setAttribute('d',d);path.setAttribute('fill','none');path.setAttribute('stroke',col);
|
|
312
|
+
path.setAttribute('stroke-width','2');path.setAttribute('stroke-opacity','.5');path.setAttribute('data-edge',a+'>'+b);
|
|
313
|
+
if(style==='crit')path.setAttribute('stroke-dasharray','6 5');
|
|
314
|
+
svg.appendChild(path);
|
|
315
|
+
const ah=document.createElementNS('http://www.w3.org/2000/svg','path');
|
|
316
|
+
ah.setAttribute('d',`M ${p2.x-8} ${p2.y-4} L ${p2.x} ${p2.y} L ${p2.x-8} ${p2.y+4}`);
|
|
317
|
+
ah.setAttribute('fill','none');ah.setAttribute('stroke',col);ah.setAttribute('stroke-width','2');ah.setAttribute('stroke-opacity','.75');
|
|
318
|
+
svg.appendChild(ah);
|
|
319
|
+
if(label){
|
|
320
|
+
const mx=(p1.x+p2.x)/2, my=(p1.y+p2.y)/2;
|
|
321
|
+
const tw=label.length*5.6+8;
|
|
322
|
+
const bg=document.createElementNS('http://www.w3.org/2000/svg','rect');
|
|
323
|
+
bg.setAttribute('x',mx-tw/2);bg.setAttribute('y',my-9);bg.setAttribute('width',tw);bg.setAttribute('height',15);
|
|
324
|
+
bg.setAttribute('rx',4);bg.setAttribute('class','elabbg');svg.appendChild(bg);
|
|
325
|
+
const tx=document.createElementNS('http://www.w3.org/2000/svg','text');
|
|
326
|
+
tx.setAttribute('x',mx);tx.setAttribute('y',my+2);tx.setAttribute('text-anchor','middle');
|
|
327
|
+
tx.setAttribute('class','elabel');tx.textContent=label;svg.appendChild(tx);
|
|
328
|
+
}
|
|
329
|
+
spawnParticles(d,col,idx,style);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function spawnParticles(d,col,idx,style){
|
|
333
|
+
const stage=svg.parentElement, N=style==='crit'?1:2;
|
|
334
|
+
for(let k=0;k<N;k++){
|
|
335
|
+
const dot=document.createElement('div');dot.className='pt';dot.style.color=col;dot.style.background=col;
|
|
336
|
+
dot.style.offsetPath=`path('${d}')`;
|
|
337
|
+
dot.style.animation=`flow ${2.6+(idx%3)*0.3}s linear ${(idx*0.3)+(k*1.2)}s infinite`;
|
|
338
|
+
if(style==='crit')dot.style.opacity='.65';
|
|
339
|
+
stage.appendChild(dot);particles.push(dot);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function select(id){
|
|
344
|
+
const v=variant(); const nd=v.nodes.find(x=>x.id===id); if(!nd)return;
|
|
345
|
+
[...grid.children].forEach(el=>{
|
|
346
|
+
const nid=el.dataset.id;
|
|
347
|
+
const connected=nid===id||v.edges.some(e=>(e[0]===id&&e[1]===nid)||(e[1]===id&&e[0]===nid));
|
|
348
|
+
el.classList.toggle('on',nid===id); el.classList.toggle('dim',!connected);
|
|
349
|
+
});
|
|
350
|
+
svg.querySelectorAll('path[data-edge]').forEach(p=>{
|
|
351
|
+
const [a,b]=p.getAttribute('data-edge').split('>');
|
|
352
|
+
p.setAttribute('stroke-opacity',(a===id||b===id)?'1':'.14');
|
|
353
|
+
});
|
|
354
|
+
const band=BANDS.find(b=>b.id===nd.sys);
|
|
355
|
+
detail.className='detail'; detail.style.borderColor=LANECOL[nd.lane];
|
|
356
|
+
detail.innerHTML=`<div class="dh"><span class="dt">${nd.t}</span>
|
|
357
|
+
<span class="sysbadge" style="color:${band.col}">${band.ic} ${band.label}</span>
|
|
358
|
+
${nd.path?`<span class="path">${nd.path}</span>`:''}</div>
|
|
359
|
+
<p>${nd.detail||nd.s}</p>
|
|
360
|
+
${nd.writes?`<span class="writes">▸ ${nd.writes}</span>`:''}
|
|
361
|
+
${nd.flag?`<div class="flag">⚠ ${nd.flag}</div>`:''}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let playTimer=null;
|
|
365
|
+
function play(){
|
|
366
|
+
reset(); const v=variant();
|
|
367
|
+
const ordered=[...v.nodes].map(n=>({n,i:stepIndex(v,n.id)})).filter(o=>o.i!=='').sort((a,b)=>a.i-b.i);
|
|
368
|
+
let k=0; document.getElementById('play').textContent='● Playing…';
|
|
369
|
+
playTimer=setInterval(()=>{
|
|
370
|
+
if(k>=ordered.length){clearInterval(playTimer);document.getElementById('play').textContent='▶ Animate flow';return;}
|
|
371
|
+
select(ordered[k].n.id);k++;
|
|
372
|
+
},900);
|
|
373
|
+
}
|
|
374
|
+
function reset(){
|
|
375
|
+
if(playTimer)clearInterval(playTimer);
|
|
376
|
+
document.getElementById('play').textContent='▶ Animate flow';
|
|
377
|
+
[...grid.children].forEach(el=>el.classList.remove('on','dim'));
|
|
378
|
+
svg.querySelectorAll('path[data-edge]').forEach(p=>p.setAttribute('stroke-opacity','.5'));
|
|
379
|
+
detail.className='detail empty';detail.style.borderColor='';
|
|
380
|
+
detail.innerHTML='◔ Click a node — the panel shows which system runs it and what crosses the wire.';
|
|
381
|
+
}
|
|
382
|
+
document.getElementById('play').onclick=play;
|
|
383
|
+
document.getElementById('reset').onclick=reset;
|
|
384
|
+
function redraw(){const v=variant();const bo=BANDS.filter(b=>[...new Set(v.nodes.map(n=>n.sys))].includes(b.id));drawBands(bo);drawEdges(v);}
|
|
385
|
+
let rt;window.addEventListener('resize',()=>{clearTimeout(rt);rt=setTimeout(redraw,120);});
|
|
386
|
+
new ResizeObserver(()=>{clearTimeout(rt);rt=setTimeout(redraw,60);}).observe(grid);
|
|
387
|
+
if(document.fonts&&document.fonts.ready){document.fonts.ready.then(redraw);}
|
|
388
|
+
render();
|
|
389
|
+
</script>
|
|
390
|
+
</body>
|
|
391
|
+
</html>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
explainer-reel dark-neon theme — design tokens for the style-pack.
|
|
3
|
+
Decoded from @duchminh_nguyen's reel series (see ../references/design-tokens.md).
|
|
4
|
+
Copy this whole file inline into a scene's <style> block — the deliverable is one
|
|
5
|
+
self-contained .html file, so don't leave this as an external stylesheet reference.
|
|
6
|
+
*/
|
|
7
|
+
:root {
|
|
8
|
+
/* -- surface -- */
|
|
9
|
+
--er-bg: #060608;
|
|
10
|
+
--er-vignette: radial-gradient(ellipse at 50% 45%, #1a0f18 0%, #060608 65%);
|
|
11
|
+
|
|
12
|
+
/* -- neon accent palette (meaning is fixed, never remap mid-piece) -- */
|
|
13
|
+
--er-client: #d9b843; /* yellow = client / requester */
|
|
14
|
+
--er-server: #d454e0; /* magenta = server / authority */
|
|
15
|
+
--er-ok: #3ddc84; /* green = verified / hit / success */
|
|
16
|
+
--er-cache: #8b6bff; /* purple = cache / redis / storage tier */
|
|
17
|
+
--er-danger: #ff5a3c; /* red-orange = danger / counter / tamper / title */
|
|
18
|
+
|
|
19
|
+
/* -- monoline -- */
|
|
20
|
+
--er-line: #aeb4bd; /* default stroke, unaccented (funnel, connectors) */
|
|
21
|
+
--er-line-dim: #4a4e56; /* dimmed/inactive context stroke */
|
|
22
|
+
--er-stroke-w: 1.5;
|
|
23
|
+
|
|
24
|
+
/* -- glow -- */
|
|
25
|
+
--er-glow-blur: 4px;
|
|
26
|
+
|
|
27
|
+
/* -- type -- */
|
|
28
|
+
--er-font-title: 'Barlow Condensed', 'Oswald', 'Arial Narrow', sans-serif;
|
|
29
|
+
--er-font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html, body { margin: 0; background: var(--er-bg); }
|
|
33
|
+
|
|
34
|
+
.er-stage {
|
|
35
|
+
background: var(--er-bg);
|
|
36
|
+
background-image: var(--er-vignette);
|
|
37
|
+
color: var(--er-line);
|
|
38
|
+
font-family: var(--er-font-mono);
|
|
39
|
+
position: relative;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* -- chrome: handle / title / footer -- */
|
|
44
|
+
.er-handle {
|
|
45
|
+
text-align: center;
|
|
46
|
+
font-size: 11px;
|
|
47
|
+
letter-spacing: 0.15em;
|
|
48
|
+
color: var(--er-line-dim);
|
|
49
|
+
}
|
|
50
|
+
.er-title {
|
|
51
|
+
text-align: center;
|
|
52
|
+
font-family: var(--er-font-title);
|
|
53
|
+
font-weight: 800;
|
|
54
|
+
font-size: 34px;
|
|
55
|
+
letter-spacing: 0.02em;
|
|
56
|
+
color: var(--er-danger);
|
|
57
|
+
text-shadow: 0 0 10px color-mix(in srgb, var(--er-danger) 55%, transparent);
|
|
58
|
+
margin: 2px 0 10px;
|
|
59
|
+
}
|
|
60
|
+
.er-title-rule {
|
|
61
|
+
border: none;
|
|
62
|
+
border-top: 1px solid var(--er-line-dim);
|
|
63
|
+
opacity: 0.4;
|
|
64
|
+
margin: 0 0 18px;
|
|
65
|
+
}
|
|
66
|
+
.er-footer {
|
|
67
|
+
position: absolute;
|
|
68
|
+
left: 0; right: 0; bottom: 22px;
|
|
69
|
+
text-align: center;
|
|
70
|
+
font-size: 11px;
|
|
71
|
+
letter-spacing: 0.12em;
|
|
72
|
+
color: var(--er-line-dim);
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* -- svg monoline defaults -- */
|
|
77
|
+
.er-svg-root { display: block; }
|
|
78
|
+
.er-mono {
|
|
79
|
+
fill: none;
|
|
80
|
+
stroke: var(--er-line);
|
|
81
|
+
stroke-width: var(--er-stroke-w);
|
|
82
|
+
}
|
|
83
|
+
.er-dim { stroke: var(--er-line-dim) !important; opacity: 0.5; }
|
|
84
|
+
|
|
85
|
+
/* glow filter is defined per-scene via <filter id="er-glow"> (feGaussianBlur),
|
|
86
|
+
applied with filter: url(#er-glow) on accent strokes/badges when they're "live" */
|
|
87
|
+
.er-glow { filter: url(#er-glow); }
|
|
88
|
+
|
|
89
|
+
/* -- reusable node/component classes (see component-library.md) -- */
|
|
90
|
+
.er-panel-rect { fill: #0b0c10; stroke-width: var(--er-stroke-w); rx: 6; }
|
|
91
|
+
.er-panel-title { font-size: 10px; letter-spacing: 0.08em; }
|
|
92
|
+
.er-panel-line { stroke: var(--er-line-dim); stroke-width: 3; stroke-linecap: round; opacity: 0.6; }
|
|
93
|
+
.er-panel-dot { r: 2.5; }
|
|
94
|
+
|
|
95
|
+
.er-pill-rect { fill: #0b0c10; stroke: var(--er-line); stroke-width: var(--er-stroke-w); rx: 4; }
|
|
96
|
+
.er-pill-text { font-size: 9px; fill: var(--er-line); font-family: var(--er-font-mono); }
|
|
97
|
+
|
|
98
|
+
.er-badge-hex { fill: none; stroke-width: var(--er-stroke-w); }
|
|
99
|
+
.er-badge-label { font-size: 10px; letter-spacing: 0.1em; font-family: var(--er-font-mono); }
|
|
Binary file
|