@bramas/simulant-ui 0.1.2 → 0.2.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 +5 -0
- package/dist/SimulatorApp.jsx +782 -174
- package/dist/components/AlgorithmViewer.css +268 -74
- package/dist/components/AlgorithmViewer.jsx +171 -66
- package/dist/components/BranchPicker.css +74 -32
- package/dist/components/BranchPicker.jsx +9 -4
- package/dist/components/ConfigGraphGenerator.css +180 -38
- package/dist/components/ConfigGraphGenerator.jsx +155 -96
- package/dist/components/GenericEditor.jsx +18 -44
- package/dist/components/Icon.jsx +54 -0
- package/dist/components/ModelView.css +15 -15
- package/dist/components/SettingsLibrary.jsx +7 -5
- package/dist/components/Splitter.jsx +28 -0
- package/dist/components/StateInspector.css +59 -33
- package/dist/components/StateInspector.jsx +37 -4
- package/dist/components/theme.js +28 -0
- package/dist/pico.min.css +4 -4
- package/dist/renderers/ring.js +19 -10
- package/dist/simulator.css +1553 -363
- package/package.json +6 -6
- package/src/SimulatorApp.tsx +888 -219
- package/src/components/AlgorithmViewer.css +268 -74
- package/src/components/AlgorithmViewer.tsx +188 -75
- package/src/components/BranchPicker.css +74 -32
- package/src/components/BranchPicker.tsx +9 -4
- package/src/components/ConfigGraphGenerator.css +180 -38
- package/src/components/ConfigGraphGenerator.tsx +172 -114
- package/src/components/GenericEditor.tsx +25 -59
- package/src/components/Icon.tsx +76 -0
- package/src/components/ModelView.css +15 -15
- package/src/components/SettingsLibrary.tsx +7 -5
- package/src/components/Splitter.tsx +55 -0
- package/src/components/StateInspector.css +59 -33
- package/src/components/StateInspector.tsx +35 -4
- package/src/components/theme.ts +33 -0
- package/src/pico.min.css +4 -4
- package/src/renderers/ring.ts +22 -12
- package/src/simulator.css +1553 -363
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* more lightly, on the lines of the calls that led there. Below, the definitions the algorithm
|
|
5
5
|
* uses, folded to their signatures (a click on a function name opens one); an agent is marked
|
|
6
6
|
* (↩) on the lines its last step returned from. Clicking a cursor explains that agent's last
|
|
7
|
-
* step
|
|
7
|
+
* step (its view, and each definition it called with its arguments and result) in a card
|
|
8
|
+
* anchored under that cursor: it covers the code without moving it, and follows the agent's line
|
|
9
|
+
* while stepping.
|
|
8
10
|
*/
|
|
9
|
-
import { For, Show, createMemo, createSignal, onCleanup } from "solid-js";
|
|
11
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
10
12
|
import { tokenizeAll } from "./highlight.js";
|
|
13
|
+
import { Icon } from "./Icon.jsx";
|
|
11
14
|
import "./AlgorithmViewer.css";
|
|
12
15
|
const SHORT = 48;
|
|
13
16
|
const shorten = (s) => s.length > SHORT ? s.slice(0, SHORT - 1) + '…' : s;
|
|
@@ -62,7 +65,10 @@ export function AlgorithmViewer(props) {
|
|
|
62
65
|
const [open, setOpen] = createSignal(new Set());
|
|
63
66
|
// The whole group of definitions is folded at first.
|
|
64
67
|
const [defsOpen, setDefsOpen] = createSignal(false);
|
|
68
|
+
// By name: an agent's index is its place in the configuration, which can change between steps.
|
|
65
69
|
const [selected, setSelected] = createSignal();
|
|
70
|
+
/** The cursor the explanation is anchored to (`data-marker`). */
|
|
71
|
+
const [anchor, setAnchor] = createSignal();
|
|
66
72
|
const [flash, setFlash] = createSignal();
|
|
67
73
|
const defElements = new Map();
|
|
68
74
|
let flashTimer;
|
|
@@ -86,7 +92,81 @@ export function AlgorithmViewer(props) {
|
|
|
86
92
|
(target ?? el)?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
87
93
|
});
|
|
88
94
|
};
|
|
89
|
-
const selectedAgent = createMemo(() => (props.agents ?? []).find(a => a.
|
|
95
|
+
const selectedAgent = createMemo(() => (props.agents ?? []).find(a => a.agentId === selected()));
|
|
96
|
+
const markerKey = (m, def, line) => `${m.kind}-${m.agent.agentId}-${def ?? 'main'}-${line}`;
|
|
97
|
+
const markerAt = (key) => scroller?.querySelector(`[data-marker="${CSS.escape(key)}"]`);
|
|
98
|
+
/** Opens (or closes, from the same cursor) an agent's explanation, under the cursor `key`. */
|
|
99
|
+
const select = (a, key) => {
|
|
100
|
+
const same = selected() === a.agentId && (key === undefined || key === anchor());
|
|
101
|
+
setSelected(same ? undefined : a.agentId);
|
|
102
|
+
setAnchor(same ? undefined : key);
|
|
103
|
+
reveal = !same;
|
|
104
|
+
if (!same)
|
|
105
|
+
props.onSelectAgent?.(a.agentId);
|
|
106
|
+
};
|
|
107
|
+
const close = () => { setSelected(undefined); setAnchor(undefined); };
|
|
108
|
+
const onKey = (e) => { if (e.key === 'Escape')
|
|
109
|
+
close(); };
|
|
110
|
+
window.addEventListener('keydown', onKey);
|
|
111
|
+
onCleanup(() => window.removeEventListener('keydown', onKey));
|
|
112
|
+
// The agents' lines stay in view while stepping: scroll to the first one when it is hidden.
|
|
113
|
+
let scroller;
|
|
114
|
+
createEffect(() => {
|
|
115
|
+
mainMarks();
|
|
116
|
+
queueMicrotask(() => {
|
|
117
|
+
const line = scroller?.querySelector('.algorithm-body > .algorithm-line.highlighted');
|
|
118
|
+
if (!scroller || !line)
|
|
119
|
+
return;
|
|
120
|
+
const view = scroller.getBoundingClientRect(), box = line.getBoundingClientRect();
|
|
121
|
+
if (box.top < view.top + 8 || box.bottom > view.bottom - 8) {
|
|
122
|
+
scroller.scrollTop += box.top - view.top - view.height / 3;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
// The explanation's card: under its cursor (the agent's current line when that cursor is gone),
|
|
127
|
+
// in the scrolling content, so that it scrolls with the code.
|
|
128
|
+
let card;
|
|
129
|
+
let reveal = false;
|
|
130
|
+
const [place, setPlace] = createSignal();
|
|
131
|
+
const reposition = () => {
|
|
132
|
+
const a = selectedAgent();
|
|
133
|
+
if (!scroller || !a)
|
|
134
|
+
return setPlace(undefined);
|
|
135
|
+
const marker = (anchor() && markerAt(anchor())) || markerAt(`at-${a.agentId}-main-${lineOf(a)}`);
|
|
136
|
+
if (!marker)
|
|
137
|
+
return setPlace(undefined);
|
|
138
|
+
const view = scroller.getBoundingClientRect(), box = marker.getBoundingClientRect();
|
|
139
|
+
const x = box.left + box.width / 2 - view.left + scroller.scrollLeft;
|
|
140
|
+
const width = Math.max(240, Math.min(400, scroller.clientWidth - 16));
|
|
141
|
+
const left = Math.max(scroller.scrollLeft + 8, Math.min(x - 48, scroller.scrollLeft + scroller.clientWidth - width - 8));
|
|
142
|
+
setPlace({ top: box.bottom - view.top + scroller.scrollTop + 6, left, width, arrow: x - left });
|
|
143
|
+
// When opened by a click, the card is scrolled into view.
|
|
144
|
+
if (reveal)
|
|
145
|
+
queueMicrotask(() => {
|
|
146
|
+
reveal = false;
|
|
147
|
+
if (!card || !scroller)
|
|
148
|
+
return;
|
|
149
|
+
const c = card.getBoundingClientRect(), v = scroller.getBoundingClientRect();
|
|
150
|
+
if (c.bottom > v.bottom - 8)
|
|
151
|
+
scroller.scrollTop += Math.min(c.bottom - v.bottom + 8, c.top - v.top - 40);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
createEffect(() => {
|
|
155
|
+
selected();
|
|
156
|
+
anchor();
|
|
157
|
+
mainMarks();
|
|
158
|
+
defMarks();
|
|
159
|
+
defsOpen();
|
|
160
|
+
open();
|
|
161
|
+
queueMicrotask(reposition);
|
|
162
|
+
});
|
|
163
|
+
onMount(() => {
|
|
164
|
+
if (!scroller)
|
|
165
|
+
return;
|
|
166
|
+
const observer = new ResizeObserver(() => reposition());
|
|
167
|
+
observer.observe(scroller);
|
|
168
|
+
onCleanup(() => observer.disconnect());
|
|
169
|
+
});
|
|
90
170
|
const Code = (p) => (<For each={p.lines}>
|
|
91
171
|
{(line) => {
|
|
92
172
|
const marks = () => p.marks.get(line.number) ?? [];
|
|
@@ -107,9 +187,9 @@ export function AlgorithmViewer(props) {
|
|
|
107
187
|
<Show when={marks().length > 0}>
|
|
108
188
|
<span class="agent-markers">
|
|
109
189
|
<For each={marks()}>
|
|
110
|
-
{(m) => (<span class={`agent-marker${m.kind === 'call' ? ' call-marker' : m.kind === 'return' ? ' return-marker' : ''}${selected() === m.agent.
|
|
190
|
+
{(m) => (<span class={`agent-marker${m.kind === 'call' ? ' call-marker' : m.kind === 'return' ? ' return-marker' : ''}${selected() === m.agent.agentId || props.highlighted === m.agent.agentId ? ' selected' : ''}`} style={{ '--agent': m.agent.color }} data-marker={markerKey(m, p.def, line.number)} title={m.kind === 'at' ? `${m.agent.agentId} is at this line: click to explain its last step`
|
|
111
191
|
: m.kind === 'call' ? `${m.agent.agentId} is inside the procedure called here`
|
|
112
|
-
: `${m.agent.agentId}'s last step returned from this line: click to explain it`} onClick={() =>
|
|
192
|
+
: `${m.agent.agentId}'s last step returned from this line: click to explain it`} onClick={() => select(m.agent, markerKey(m, p.def, line.number))}>
|
|
113
193
|
{m.kind === 'at' ? '◀' : m.kind === 'call' ? '◁' : '↩'} {m.agent.agentId}
|
|
114
194
|
</span>)}
|
|
115
195
|
</For>
|
|
@@ -141,78 +221,103 @@ export function AlgorithmViewer(props) {
|
|
|
141
221
|
</For>
|
|
142
222
|
</ul>);
|
|
143
223
|
return (<div class="algorithm-viewer">
|
|
144
|
-
<div class=
|
|
145
|
-
<
|
|
224
|
+
<div class={`algorithm-header${props.onHeadPointerDown ? ' algorithm-header-draggable' : ''}`} onPointerDown={e => props.onHeadPointerDown?.(e)}>
|
|
225
|
+
<Icon name="file"/>
|
|
146
226
|
<Show when={props.source.file}>
|
|
147
227
|
<span class="algorithm-file">{props.source.file.split('/').pop()}</span>
|
|
228
|
+
<Icon name="chevronRight" size={12} class="algorithm-crumb"/>
|
|
148
229
|
</Show>
|
|
230
|
+
<h3 class="algorithm-caption">{props.source.name}</h3>
|
|
231
|
+
<span class="sim-spacer"/>
|
|
232
|
+
<div class="algorithm-agents" aria-label="Agents">
|
|
233
|
+
<For each={props.agents ?? []}>
|
|
234
|
+
{(a) => (<button onPointerDown={e => e.stopPropagation()} class={`algorithm-agent${selected() === a.agentId || props.highlighted === a.agentId ? ' selected' : ''}`} style={{ '--agent': a.color }} onClick={() => {
|
|
235
|
+
// The card opens under the agent's cursor, scrolled into view.
|
|
236
|
+
const key = `at-${a.agentId}-main-${lineOf(a)}`;
|
|
237
|
+
select(a, key);
|
|
238
|
+
queueMicrotask(() => markerAt(key)?.closest('.algorithm-line')?.scrollIntoView({ block: 'nearest' }));
|
|
239
|
+
}} title={`${a.agentId}: line ${lineOf(a)}. Click to explain its last step`} aria-pressed={selected() === a.agentId}>
|
|
240
|
+
<span class="algorithm-agent-dot"/>{a.agentId}<span class="algorithm-agent-line">{lineOf(a)}</span>
|
|
241
|
+
</button>)}
|
|
242
|
+
</For>
|
|
243
|
+
</div>
|
|
244
|
+
{props.actions}
|
|
149
245
|
</div>
|
|
150
246
|
|
|
151
|
-
<
|
|
152
|
-
|
|
247
|
+
<div class="algorithm-scroll" ref={scroller}>
|
|
248
|
+
<div class="algorithm-body">
|
|
249
|
+
<Code lines={lines()} marks={mainMarks()}/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<Show when={defLines().length > 0}>
|
|
253
|
+
<div class="algorithm-defs">
|
|
254
|
+
<div class="defs-title" onClick={() => setDefsOpen(v => !v)} title={defsOpen() ? 'Fold the definitions' : 'Show the definitions'}>
|
|
255
|
+
<Icon name="chevronRight" size={13} class={defsOpen() ? 'defs-chevron open' : 'defs-chevron'}/> Definitions <span class="sim-muted">{defLines().length} · a click on a function name opens its definition</span>
|
|
256
|
+
</div>
|
|
257
|
+
<Show when={defsOpen()}>
|
|
258
|
+
<For each={defLines()}>
|
|
259
|
+
{(d) => {
|
|
260
|
+
const marks = () => defMarks().get(d.name) ?? new Map();
|
|
261
|
+
const agents = () => [...new Map([...marks().values()].flat().map(m => [m.agent.index, m.agent])).values()];
|
|
262
|
+
return (<div class={`def${flash() === d.name ? ' flash' : ''}`} ref={el => defElements.set(d.name, el)}>
|
|
263
|
+
<Show when={open().has(d.name)} fallback={<div class="algorithm-line def-folded" onClick={() => toggle(d.name)} title="Show the definition">
|
|
264
|
+
<span class="line-number">▸</span>
|
|
265
|
+
<span class="line-content">
|
|
266
|
+
<For each={d.tokens[0]?.tokens ?? []}>{(t) => t.kind ? <span class={`tok-${t.kind}`}>{t.text}</span> : t.text}</For>
|
|
267
|
+
</span>
|
|
268
|
+
<Show when={agents().length}>
|
|
269
|
+
<span class="agent-markers">
|
|
270
|
+
<For each={agents()}>{(a) => <span class="agent-marker return-marker" style={{ '--agent': a.color }}>↩ {a.agentId}</span>}</For>
|
|
271
|
+
</span>
|
|
272
|
+
</Show>
|
|
273
|
+
</div>}>
|
|
274
|
+
<div class="def-open">
|
|
275
|
+
<div class="def-fold" onClick={() => toggle(d.name)} title="Fold">▾ {d.name}</div>
|
|
276
|
+
<Code lines={d.tokens} marks={marks()} def={d.name}/>
|
|
277
|
+
</div>
|
|
278
|
+
</Show>
|
|
279
|
+
</div>);
|
|
280
|
+
}}
|
|
281
|
+
</For>
|
|
282
|
+
</Show>
|
|
283
|
+
</div>
|
|
284
|
+
</Show>
|
|
285
|
+
|
|
286
|
+
<Show when={selectedAgent() && place()}>
|
|
287
|
+
<div ref={card} class="ex-card" role="dialog" aria-label={`Last step of ${selectedAgent().agentId}`} style={{ top: `${place().top}px`, left: `${place().left}px`, width: `${place().width}px`, '--arrow': `${place().arrow}px`, '--agent': selectedAgent().color }}>
|
|
153
288
|
<div class="ex-head">
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<Show when={e().line !== undefined}> · decided at line {e().line}</Show>
|
|
159
|
-
<span class="sim-muted"> · frame: {e().frame}</span>
|
|
160
|
-
</span>)}
|
|
161
|
-
</Show>
|
|
162
|
-
<span style={{ flex: 1 }}/>
|
|
163
|
-
<button class="sim-ghost sim-icon" onClick={() => setSelected(undefined)} title="Close">✕</button>
|
|
289
|
+
<span class="ex-agent"><span class="algorithm-agent-dot"/>{selectedAgent().agentId}</span>
|
|
290
|
+
<span class="ex-title">Last step</span>
|
|
291
|
+
<span class="sim-spacer"/>
|
|
292
|
+
<button class="sim-ghost sim-icon sim-sm" onClick={close} aria-label="Close the explanation" title="Close (Esc)"><Icon name="close" size={14}/></button>
|
|
164
293
|
</div>
|
|
165
|
-
<Show when={
|
|
294
|
+
<Show when={selectedAgent().explanation} fallback={<p class="ex-none">No decision in the last step: the agent was not activated, or this is the initial configuration.</p>}>
|
|
166
295
|
{e => (<>
|
|
167
|
-
<div class="ex-
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
</
|
|
296
|
+
<div class="ex-meta">
|
|
297
|
+
<span><span class="tok-action">move</span> {e().move}</span>
|
|
298
|
+
<Show when={e().line !== undefined}><span>decided at line {e().line}</span></Show>
|
|
299
|
+
<span>frame: {e().frame}</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="ex-section">
|
|
302
|
+
<h4>Sensors</h4>
|
|
303
|
+
<Show when={Object.keys(e().view).length} fallback={<p class="ex-none">No sensor.</p>}>
|
|
304
|
+
<dl class="ex-view">
|
|
305
|
+
<For each={Object.entries(e().view)}>
|
|
306
|
+
{([k, v]) => <><dt class="si-key">{k}</dt><dd class="ex-result">{v}</dd></>}
|
|
307
|
+
</For>
|
|
308
|
+
</dl>
|
|
309
|
+
</Show>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="ex-section">
|
|
312
|
+
<h4>Calls</h4>
|
|
313
|
+
<Show when={e().calls.length} fallback={<p class="ex-none">No definition was called.</p>}>
|
|
314
|
+
<CallTree calls={e().calls} depth={0}/>
|
|
315
|
+
</Show>
|
|
171
316
|
</div>
|
|
172
|
-
<Show when={e().calls.length} fallback={<p class="sim-muted ex-none">No definition was called.</p>}>
|
|
173
|
-
<CallTree calls={e().calls} depth={0}/>
|
|
174
|
-
</Show>
|
|
175
317
|
</>)}
|
|
176
318
|
</Show>
|
|
177
|
-
</div>)}
|
|
178
|
-
</Show>
|
|
179
|
-
|
|
180
|
-
<div class="algorithm-body">
|
|
181
|
-
<Code lines={lines()} marks={mainMarks()}/>
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
<Show when={defLines().length > 0}>
|
|
185
|
-
<div class="algorithm-defs">
|
|
186
|
-
<div class="defs-title" onClick={() => setDefsOpen(v => !v)} title={defsOpen() ? 'Fold the definitions' : 'Show the definitions'}>
|
|
187
|
-
{defsOpen() ? '▾' : '▸'} Definitions <span class="sim-muted">({defLines().length}; a click on a function name opens its definition)</span>
|
|
188
319
|
</div>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
{(d) => {
|
|
192
|
-
const marks = () => defMarks().get(d.name) ?? new Map();
|
|
193
|
-
const agents = () => [...new Map([...marks().values()].flat().map(m => [m.agent.index, m.agent])).values()];
|
|
194
|
-
return (<div class={`def${flash() === d.name ? ' flash' : ''}`} ref={el => defElements.set(d.name, el)}>
|
|
195
|
-
<Show when={open().has(d.name)} fallback={<div class="algorithm-line def-folded" onClick={() => toggle(d.name)} title="Show the definition">
|
|
196
|
-
<span class="line-number">▸</span>
|
|
197
|
-
<span class="line-content">
|
|
198
|
-
<For each={d.tokens[0]?.tokens ?? []}>{(t) => t.kind ? <span class={`tok-${t.kind}`}>{t.text}</span> : t.text}</For>
|
|
199
|
-
</span>
|
|
200
|
-
<Show when={agents().length}>
|
|
201
|
-
<span class="agent-markers">
|
|
202
|
-
<For each={agents()}>{(a) => <span class="agent-marker return-marker" style={{ color: a.color }}>↩ {a.agentId}</span>}</For>
|
|
203
|
-
</span>
|
|
204
|
-
</Show>
|
|
205
|
-
</div>}>
|
|
206
|
-
<div class="def-open">
|
|
207
|
-
<div class="def-fold" onClick={() => toggle(d.name)} title="Fold">▾ {d.name}</div>
|
|
208
|
-
<Code lines={d.tokens} marks={marks()} def={d.name}/>
|
|
209
|
-
</div>
|
|
210
|
-
</Show>
|
|
211
|
-
</div>);
|
|
212
|
-
}}
|
|
213
|
-
</For>
|
|
214
|
-
</Show>
|
|
215
|
-
</div>
|
|
216
|
-
</Show>
|
|
320
|
+
</Show>
|
|
321
|
+
</div>
|
|
217
322
|
</div>);
|
|
218
323
|
}
|
|
@@ -4,93 +4,135 @@
|
|
|
4
4
|
position: fixed;
|
|
5
5
|
inset: 0;
|
|
6
6
|
background: rgba(0, 0, 0, 0.45);
|
|
7
|
+
backdrop-filter: blur(2px);
|
|
7
8
|
z-index: 50;
|
|
8
9
|
display: flex;
|
|
9
10
|
align-items: center;
|
|
10
11
|
justify-content: center;
|
|
11
12
|
padding: 2rem;
|
|
13
|
+
animation: bp-fade 0.15s ease-out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@keyframes bp-fade {
|
|
17
|
+
from { opacity: 0; }
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
.bp-window {
|
|
15
21
|
background: var(--sim-surface);
|
|
16
22
|
color: var(--sim-text);
|
|
17
23
|
border: 1px solid var(--sim-border);
|
|
18
|
-
border-radius:
|
|
19
|
-
box-shadow:
|
|
24
|
+
border-radius: 14px;
|
|
25
|
+
box-shadow: var(--sim-shadow-lg);
|
|
20
26
|
width: min(1100px, 100%);
|
|
21
27
|
max-height: 100%;
|
|
22
28
|
display: flex;
|
|
23
29
|
flex-direction: column;
|
|
24
|
-
|
|
30
|
+
overflow: hidden;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
.bp-header {
|
|
28
34
|
display: flex;
|
|
29
|
-
align-items:
|
|
30
|
-
gap:
|
|
35
|
+
align-items: flex-start;
|
|
36
|
+
gap: 12px;
|
|
37
|
+
padding: 14px 14px 14px 18px;
|
|
38
|
+
border-bottom: 1px solid var(--sim-border);
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
.bp-header
|
|
34
|
-
margin:
|
|
35
|
-
|
|
41
|
+
.bp-header > .sim-svg {
|
|
42
|
+
margin-top: 2px;
|
|
43
|
+
color: var(--sim-muted);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
.bp-
|
|
39
|
-
|
|
46
|
+
.bp-title {
|
|
47
|
+
min-width: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bp-header h3 {
|
|
40
51
|
margin: 0;
|
|
52
|
+
font-size: 15px;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
.bp-help {
|
|
44
|
-
margin:
|
|
56
|
+
margin: 3px 0 0;
|
|
57
|
+
font-size: 12.5px;
|
|
58
|
+
color: var(--sim-muted);
|
|
59
|
+
max-width: 52rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.bp-header input {
|
|
63
|
+
max-width: 16rem;
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
.bp-grid {
|
|
48
67
|
display: grid;
|
|
49
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
50
|
-
gap:
|
|
68
|
+
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
|
|
69
|
+
gap: 12px;
|
|
51
70
|
overflow: auto;
|
|
52
|
-
padding:
|
|
71
|
+
padding: 16px 18px 18px;
|
|
72
|
+
background: var(--sim-bg);
|
|
53
73
|
}
|
|
54
74
|
|
|
55
|
-
.sim-app .bp-branch
|
|
56
|
-
.bp-branch {
|
|
75
|
+
.sim-app .bp-branch {
|
|
57
76
|
display: flex;
|
|
58
77
|
flex-direction: column;
|
|
59
|
-
align-items:
|
|
60
|
-
gap:
|
|
78
|
+
align-items: stretch;
|
|
79
|
+
gap: 8px;
|
|
61
80
|
width: 100%;
|
|
62
|
-
padding:
|
|
63
|
-
background: var(--sim-surface
|
|
81
|
+
padding: 8px;
|
|
82
|
+
background: var(--sim-surface);
|
|
64
83
|
color: var(--sim-text);
|
|
65
|
-
border:
|
|
66
|
-
border-radius:
|
|
67
|
-
text-align:
|
|
68
|
-
font-size:
|
|
84
|
+
border: 1px solid var(--sim-border);
|
|
85
|
+
border-radius: 10px;
|
|
86
|
+
text-align: left;
|
|
87
|
+
font-size: 12.5px;
|
|
88
|
+
box-shadow: var(--sim-shadow-sm);
|
|
89
|
+
transition: border-color 0.12s, box-shadow 0.12s, transform 0.12s;
|
|
69
90
|
}
|
|
70
91
|
|
|
71
|
-
.bp-branch:hover {
|
|
92
|
+
.sim-app .bp-branch:hover {
|
|
72
93
|
border-color: var(--sim-accent);
|
|
94
|
+
box-shadow: var(--sim-shadow-md);
|
|
95
|
+
transform: translateY(-1px);
|
|
73
96
|
}
|
|
74
97
|
|
|
75
|
-
.bp-branch.bp-current {
|
|
76
|
-
border-color: var(--sim-
|
|
77
|
-
|
|
98
|
+
.sim-app .bp-branch.bp-current {
|
|
99
|
+
border-color: var(--sim-accent);
|
|
100
|
+
box-shadow: 0 0 0 3px var(--sim-accent-soft);
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
.bp-thumb {
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
align-self: center;
|
|
105
|
+
background-color: var(--sim-stage);
|
|
106
|
+
background-image: radial-gradient(var(--sim-stage-dot) 1px, transparent 1.2px);
|
|
107
|
+
background-size: 14px 14px;
|
|
108
|
+
border-radius: 7px;
|
|
109
|
+
border: 1px solid var(--sim-border);
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
.bp-caption {
|
|
86
113
|
display: flex;
|
|
87
114
|
align-items: center;
|
|
88
|
-
gap:
|
|
115
|
+
gap: 6px;
|
|
116
|
+
flex-wrap: wrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.bp-current-tag {
|
|
120
|
+
font-size: 10.5px;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
letter-spacing: 0.04em;
|
|
123
|
+
text-transform: uppercase;
|
|
124
|
+
color: var(--sim-accent);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.bp-caption .sim-chip {
|
|
128
|
+
margin-left: auto;
|
|
129
|
+
height: 20px;
|
|
130
|
+
font-size: 11px;
|
|
89
131
|
}
|
|
90
132
|
|
|
91
133
|
.bp-label {
|
|
92
134
|
font-family: var(--sim-mono);
|
|
93
|
-
font-size:
|
|
135
|
+
font-size: 11.5px;
|
|
94
136
|
color: var(--sim-muted);
|
|
95
137
|
word-break: break-word;
|
|
96
138
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* pick one.
|
|
5
5
|
*/
|
|
6
6
|
import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
7
|
+
import { Icon } from "./Icon.jsx";
|
|
7
8
|
import "./BranchPicker.css";
|
|
8
9
|
export { distinguishing } from "../sim/branches.js";
|
|
9
10
|
const VERDICT_TEXT = { success: 'success', failure: 'failure', inProgress: 'not yet' };
|
|
@@ -40,20 +41,24 @@ export function BranchPicker(props) {
|
|
|
40
41
|
props.onClose(); }}>
|
|
41
42
|
<div class="bp-window" role="dialog" aria-modal="true" aria-label="Branches">
|
|
42
43
|
<div class="bp-header">
|
|
43
|
-
<
|
|
44
|
+
<Icon name="branch"/>
|
|
45
|
+
<div class="bp-title">
|
|
46
|
+
<h3>{props.branches.length} branches at this step</h3>
|
|
47
|
+
<p class="bp-help">Each branch is one way the adversary can play this round; only the choices that differ between branches and are not the first option are listed.</p>
|
|
48
|
+
</div>
|
|
49
|
+
<span class="sim-spacer"/>
|
|
44
50
|
<Show when={props.branches.length > 8}>
|
|
45
51
|
<input type="search" placeholder="Filter (index or choice)" value={filter()} onInput={e => setFilter(e.currentTarget.value)} aria-label="Filter the branches"/>
|
|
46
52
|
</Show>
|
|
47
|
-
<
|
|
48
|
-
<button class="sim-ghost sim-icon" onClick={props.onClose} title="Close (Esc)">✕</button>
|
|
53
|
+
<button class="sim-ghost sim-icon" onClick={props.onClose} aria-label="Close" title="Close (Esc)"><Icon name="close"/></button>
|
|
49
54
|
</div>
|
|
50
|
-
<p class="sim-muted bp-help">Each branch is one way the adversary can play this round; only the choices that differ between branches and are not the first option are listed.</p>
|
|
51
55
|
<div class="bp-grid">
|
|
52
56
|
<For each={shown()} fallback={<p class="sim-muted">No branch matches.</p>}>
|
|
53
57
|
{b => (<button class={`bp-branch${b.index === props.current ? ' bp-current' : ''}`} onClick={() => props.onPick(b.index)} title={b.label}>
|
|
54
58
|
<Show when={props.draw}><Thumb index={b.index}/></Show>
|
|
55
59
|
<span class="bp-caption">
|
|
56
60
|
<strong>Branch {b.index}</strong>
|
|
61
|
+
<Show when={b.index === props.current}><span class="bp-current-tag">current</span></Show>
|
|
57
62
|
<Show when={b.verdict}>{v => <span class={`sim-chip sim-${v()}`}>{VERDICT_TEXT[v()]}</span>}</Show>
|
|
58
63
|
</span>
|
|
59
64
|
<span class="bp-label">{b.short || b.label || '(no choice)'}</span>
|