@bramas/simulant-ui 0.1.1 → 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 +60 -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,11 +4,14 @@
|
|
|
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, type JSX } from 'solid-js';
|
|
11
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX } from 'solid-js';
|
|
10
12
|
import type { AgentExecutionState, AlgorithmSource, TraceCall } from '@bramas/simulant-core/types/project';
|
|
11
13
|
import { tokenizeAll, type Line } from './highlight';
|
|
14
|
+
import { Icon } from './Icon';
|
|
12
15
|
import './AlgorithmViewer.css';
|
|
13
16
|
|
|
14
17
|
export type { AgentExecutionState, AlgorithmSource } from '@bramas/simulant-core/types/project';
|
|
@@ -16,6 +19,14 @@ export type { AgentExecutionState, AlgorithmSource } from '@bramas/simulant-core
|
|
|
16
19
|
export interface AlgorithmViewerProps {
|
|
17
20
|
source: AlgorithmSource;
|
|
18
21
|
agents?: AgentExecutionState[];
|
|
22
|
+
/** Buttons for the header's end (the page's panel controls). */
|
|
23
|
+
actions?: JSX.Element;
|
|
24
|
+
/** A press on the header (the page drags the panel from there). */
|
|
25
|
+
onHeadPointerDown?: (e: PointerEvent) => void;
|
|
26
|
+
/** The agent selected elsewhere (by name): its cursors are emphasized. */
|
|
27
|
+
highlighted?: string | null;
|
|
28
|
+
/** Called with the agent's name when one of its cursors is clicked. */
|
|
29
|
+
onSelectAgent?: (name: string) => void;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
32
|
type Mark = { agent: AgentExecutionState; kind: 'at' | 'call' | 'return' };
|
|
@@ -74,7 +85,10 @@ export function AlgorithmViewer(props: AlgorithmViewerProps) {
|
|
|
74
85
|
const [open, setOpen] = createSignal<Set<string>>(new Set());
|
|
75
86
|
// The whole group of definitions is folded at first.
|
|
76
87
|
const [defsOpen, setDefsOpen] = createSignal(false);
|
|
77
|
-
|
|
88
|
+
// By name: an agent's index is its place in the configuration, which can change between steps.
|
|
89
|
+
const [selected, setSelected] = createSignal<string | undefined>();
|
|
90
|
+
/** The cursor the explanation is anchored to (`data-marker`). */
|
|
91
|
+
const [anchor, setAnchor] = createSignal<string | undefined>();
|
|
78
92
|
const [flash, setFlash] = createSignal<string | undefined>();
|
|
79
93
|
const defElements = new Map<string, HTMLElement>();
|
|
80
94
|
let flashTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -94,7 +108,69 @@ export function AlgorithmViewer(props: AlgorithmViewerProps) {
|
|
|
94
108
|
(target ?? el)?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
95
109
|
});
|
|
96
110
|
};
|
|
97
|
-
const selectedAgent = createMemo(() => (props.agents ?? []).find(a => a.
|
|
111
|
+
const selectedAgent = createMemo(() => (props.agents ?? []).find(a => a.agentId === selected()));
|
|
112
|
+
const markerKey = (m: Mark, def: string | undefined, line: number) => `${m.kind}-${m.agent.agentId}-${def ?? 'main'}-${line}`;
|
|
113
|
+
const markerAt = (key: string) => scroller?.querySelector<HTMLElement>(`[data-marker="${CSS.escape(key)}"]`);
|
|
114
|
+
/** Opens (or closes, from the same cursor) an agent's explanation, under the cursor `key`. */
|
|
115
|
+
const select = (a: AgentExecutionState, key?: string) => {
|
|
116
|
+
const same = selected() === a.agentId && (key === undefined || key === anchor());
|
|
117
|
+
setSelected(same ? undefined : a.agentId);
|
|
118
|
+
setAnchor(same ? undefined : key);
|
|
119
|
+
reveal = !same;
|
|
120
|
+
if (!same) props.onSelectAgent?.(a.agentId);
|
|
121
|
+
};
|
|
122
|
+
const close = () => { setSelected(undefined); setAnchor(undefined); };
|
|
123
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') close(); };
|
|
124
|
+
window.addEventListener('keydown', onKey);
|
|
125
|
+
onCleanup(() => window.removeEventListener('keydown', onKey));
|
|
126
|
+
|
|
127
|
+
// The agents' lines stay in view while stepping: scroll to the first one when it is hidden.
|
|
128
|
+
let scroller: HTMLDivElement | undefined;
|
|
129
|
+
createEffect(() => {
|
|
130
|
+
mainMarks();
|
|
131
|
+
queueMicrotask(() => {
|
|
132
|
+
const line = scroller?.querySelector<HTMLElement>('.algorithm-body > .algorithm-line.highlighted');
|
|
133
|
+
if (!scroller || !line) return;
|
|
134
|
+
const view = scroller.getBoundingClientRect(), box = line.getBoundingClientRect();
|
|
135
|
+
if (box.top < view.top + 8 || box.bottom > view.bottom - 8) {
|
|
136
|
+
scroller.scrollTop += box.top - view.top - view.height / 3;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// The explanation's card: under its cursor (the agent's current line when that cursor is gone),
|
|
142
|
+
// in the scrolling content, so that it scrolls with the code.
|
|
143
|
+
let card: HTMLDivElement | undefined;
|
|
144
|
+
let reveal = false;
|
|
145
|
+
const [place, setPlace] = createSignal<{ top: number; left: number; width: number; arrow: number } | undefined>();
|
|
146
|
+
const reposition = () => {
|
|
147
|
+
const a = selectedAgent();
|
|
148
|
+
if (!scroller || !a) return setPlace(undefined);
|
|
149
|
+
const marker = (anchor() && markerAt(anchor()!)) || markerAt(`at-${a.agentId}-main-${lineOf(a)}`);
|
|
150
|
+
if (!marker) return setPlace(undefined);
|
|
151
|
+
const view = scroller.getBoundingClientRect(), box = marker.getBoundingClientRect();
|
|
152
|
+
const x = box.left + box.width / 2 - view.left + scroller.scrollLeft;
|
|
153
|
+
const width = Math.max(240, Math.min(400, scroller.clientWidth - 16));
|
|
154
|
+
const left = Math.max(scroller.scrollLeft + 8, Math.min(x - 48, scroller.scrollLeft + scroller.clientWidth - width - 8));
|
|
155
|
+
setPlace({ top: box.bottom - view.top + scroller.scrollTop + 6, left, width, arrow: x - left });
|
|
156
|
+
// When opened by a click, the card is scrolled into view.
|
|
157
|
+
if (reveal) queueMicrotask(() => {
|
|
158
|
+
reveal = false;
|
|
159
|
+
if (!card || !scroller) return;
|
|
160
|
+
const c = card.getBoundingClientRect(), v = scroller.getBoundingClientRect();
|
|
161
|
+
if (c.bottom > v.bottom - 8) scroller.scrollTop += Math.min(c.bottom - v.bottom + 8, c.top - v.top - 40);
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
createEffect(() => {
|
|
165
|
+
selected(); anchor(); mainMarks(); defMarks(); defsOpen(); open();
|
|
166
|
+
queueMicrotask(reposition);
|
|
167
|
+
});
|
|
168
|
+
onMount(() => {
|
|
169
|
+
if (!scroller) return;
|
|
170
|
+
const observer = new ResizeObserver(() => reposition());
|
|
171
|
+
observer.observe(scroller);
|
|
172
|
+
onCleanup(() => observer.disconnect());
|
|
173
|
+
});
|
|
98
174
|
|
|
99
175
|
const Code = (p: { lines: Line[]; marks: Map<number, Mark[]>; def?: string }): JSX.Element => (
|
|
100
176
|
<For each={p.lines}>
|
|
@@ -120,12 +196,13 @@ export function AlgorithmViewer(props: AlgorithmViewerProps) {
|
|
|
120
196
|
<For each={marks()}>
|
|
121
197
|
{(m) => (
|
|
122
198
|
<span
|
|
123
|
-
class={`agent-marker${m.kind === 'call' ? ' call-marker' : m.kind === 'return' ? ' return-marker' : ''}${selected() === m.agent.
|
|
124
|
-
style={{
|
|
199
|
+
class={`agent-marker${m.kind === 'call' ? ' call-marker' : m.kind === 'return' ? ' return-marker' : ''}${selected() === m.agent.agentId || props.highlighted === m.agent.agentId ? ' selected' : ''}`}
|
|
200
|
+
style={{ '--agent': m.agent.color }}
|
|
201
|
+
data-marker={markerKey(m, p.def, line.number)}
|
|
125
202
|
title={m.kind === 'at' ? `${m.agent.agentId} is at this line: click to explain its last step`
|
|
126
203
|
: m.kind === 'call' ? `${m.agent.agentId} is inside the procedure called here`
|
|
127
204
|
: `${m.agent.agentId}'s last step returned from this line: click to explain it`}
|
|
128
|
-
onClick={() =>
|
|
205
|
+
onClick={() => select(m.agent, markerKey(m, p.def, line.number))}
|
|
129
206
|
>
|
|
130
207
|
{m.kind === 'at' ? '◀' : m.kind === 'call' ? '◁' : '↩'} {m.agent.agentId}
|
|
131
208
|
</span>
|
|
@@ -168,89 +245,125 @@ export function AlgorithmViewer(props: AlgorithmViewerProps) {
|
|
|
168
245
|
|
|
169
246
|
return (
|
|
170
247
|
<div class="algorithm-viewer">
|
|
171
|
-
<div class=
|
|
172
|
-
<
|
|
248
|
+
<div class={`algorithm-header${props.onHeadPointerDown ? ' algorithm-header-draggable' : ''}`} onPointerDown={e => props.onHeadPointerDown?.(e)}>
|
|
249
|
+
<Icon name="file" />
|
|
173
250
|
<Show when={props.source.file}>
|
|
174
251
|
<span class="algorithm-file">{props.source.file!.split('/').pop()}</span>
|
|
252
|
+
<Icon name="chevronRight" size={12} class="algorithm-crumb" />
|
|
175
253
|
</Show>
|
|
254
|
+
<h3 class="algorithm-caption">{props.source.name}</h3>
|
|
255
|
+
<span class="sim-spacer" />
|
|
256
|
+
<div class="algorithm-agents" aria-label="Agents">
|
|
257
|
+
<For each={props.agents ?? []}>
|
|
258
|
+
{(a) => (
|
|
259
|
+
<button
|
|
260
|
+
onPointerDown={e => e.stopPropagation()}
|
|
261
|
+
class={`algorithm-agent${selected() === a.agentId || props.highlighted === a.agentId ? ' selected' : ''}`}
|
|
262
|
+
style={{ '--agent': a.color }}
|
|
263
|
+
onClick={() => {
|
|
264
|
+
// The card opens under the agent's cursor, scrolled into view.
|
|
265
|
+
const key = `at-${a.agentId}-main-${lineOf(a)}`;
|
|
266
|
+
select(a, key);
|
|
267
|
+
queueMicrotask(() => markerAt(key)?.closest('.algorithm-line')?.scrollIntoView({ block: 'nearest' }));
|
|
268
|
+
}}
|
|
269
|
+
title={`${a.agentId}: line ${lineOf(a)}. Click to explain its last step`}
|
|
270
|
+
aria-pressed={selected() === a.agentId}
|
|
271
|
+
>
|
|
272
|
+
<span class="algorithm-agent-dot" />{a.agentId}<span class="algorithm-agent-line">{lineOf(a)}</span>
|
|
273
|
+
</button>
|
|
274
|
+
)}
|
|
275
|
+
</For>
|
|
276
|
+
</div>
|
|
277
|
+
{props.actions}
|
|
176
278
|
</div>
|
|
177
279
|
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
<
|
|
280
|
+
<div class="algorithm-scroll" ref={scroller}>
|
|
281
|
+
<div class="algorithm-body">
|
|
282
|
+
<Code lines={lines()} marks={mainMarks()} />
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<Show when={defLines().length > 0}>
|
|
286
|
+
<div class="algorithm-defs">
|
|
287
|
+
<div class="defs-title" onClick={() => setDefsOpen(v => !v)} title={defsOpen() ? 'Fold the definitions' : 'Show the definitions'}>
|
|
288
|
+
<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>
|
|
289
|
+
</div>
|
|
290
|
+
<Show when={defsOpen()}>
|
|
291
|
+
<For each={defLines()}>
|
|
292
|
+
{(d) => {
|
|
293
|
+
const marks = () => defMarks().get(d.name) ?? new Map<number, Mark[]>();
|
|
294
|
+
const agents = () => [...new Map([...marks().values()].flat().map(m => [m.agent.index, m.agent])).values()];
|
|
295
|
+
return (
|
|
296
|
+
<div class={`def${flash() === d.name ? ' flash' : ''}`} ref={el => defElements.set(d.name, el)}>
|
|
297
|
+
<Show when={open().has(d.name)} fallback={
|
|
298
|
+
<div class="algorithm-line def-folded" onClick={() => toggle(d.name)} title="Show the definition">
|
|
299
|
+
<span class="line-number">▸</span>
|
|
300
|
+
<span class="line-content">
|
|
301
|
+
<For each={d.tokens[0]?.tokens ?? []}>{(t) => t.kind ? <span class={`tok-${t.kind}`}>{t.text}</span> : t.text}</For>
|
|
302
|
+
</span>
|
|
303
|
+
<Show when={agents().length}>
|
|
304
|
+
<span class="agent-markers">
|
|
305
|
+
<For each={agents()}>{(a) => <span class="agent-marker return-marker" style={{ '--agent': a.color }}>↩ {a.agentId}</span>}</For>
|
|
306
|
+
</span>
|
|
307
|
+
</Show>
|
|
308
|
+
</div>
|
|
309
|
+
}>
|
|
310
|
+
<div class="def-open">
|
|
311
|
+
<div class="def-fold" onClick={() => toggle(d.name)} title="Fold">▾ {d.name}</div>
|
|
312
|
+
<Code lines={d.tokens} marks={marks()} def={d.name} />
|
|
313
|
+
</div>
|
|
314
|
+
</Show>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}}
|
|
318
|
+
</For>
|
|
319
|
+
</Show>
|
|
320
|
+
</div>
|
|
321
|
+
</Show>
|
|
322
|
+
|
|
323
|
+
<Show when={selectedAgent() && place()}>
|
|
324
|
+
<div
|
|
325
|
+
ref={card}
|
|
326
|
+
class="ex-card"
|
|
327
|
+
role="dialog"
|
|
328
|
+
aria-label={`Last step of ${selectedAgent()!.agentId}`}
|
|
329
|
+
style={{ top: `${place()!.top}px`, left: `${place()!.left}px`, width: `${place()!.width}px`, '--arrow': `${place()!.arrow}px`, '--agent': selectedAgent()!.color }}
|
|
330
|
+
>
|
|
181
331
|
<div class="ex-head">
|
|
182
|
-
<
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
last step: <span class="tok-action">move</span> {e().move}
|
|
187
|
-
<Show when={e().line !== undefined}> · decided at line {e().line}</Show>
|
|
188
|
-
<span class="sim-muted"> · frame: {e().frame}</span>
|
|
189
|
-
</span>
|
|
190
|
-
)}
|
|
191
|
-
</Show>
|
|
192
|
-
<span style={{ flex: 1 }} />
|
|
193
|
-
<button class="sim-ghost sim-icon" onClick={() => setSelected(undefined)} title="Close">✕</button>
|
|
332
|
+
<span class="ex-agent"><span class="algorithm-agent-dot" />{selectedAgent()!.agentId}</span>
|
|
333
|
+
<span class="ex-title">Last step</span>
|
|
334
|
+
<span class="sim-spacer" />
|
|
335
|
+
<button class="sim-ghost sim-icon sim-sm" onClick={close} aria-label="Close the explanation" title="Close (Esc)"><Icon name="close" size={14} /></button>
|
|
194
336
|
</div>
|
|
195
|
-
<Show when={
|
|
337
|
+
<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>}>
|
|
196
338
|
{e => (
|
|
197
339
|
<>
|
|
198
|
-
<div class="ex-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
</
|
|
340
|
+
<div class="ex-meta">
|
|
341
|
+
<span><span class="tok-action">move</span> {e().move}</span>
|
|
342
|
+
<Show when={e().line !== undefined}><span>decided at line {e().line}</span></Show>
|
|
343
|
+
<span>frame: {e().frame}</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="ex-section">
|
|
346
|
+
<h4>Sensors</h4>
|
|
347
|
+
<Show when={Object.keys(e().view).length} fallback={<p class="ex-none">No sensor.</p>}>
|
|
348
|
+
<dl class="ex-view">
|
|
349
|
+
<For each={Object.entries(e().view)}>
|
|
350
|
+
{([k, v]) => <><dt class="si-key">{k}</dt><dd class="ex-result">{v}</dd></>}
|
|
351
|
+
</For>
|
|
352
|
+
</dl>
|
|
353
|
+
</Show>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="ex-section">
|
|
356
|
+
<h4>Calls</h4>
|
|
357
|
+
<Show when={e().calls.length} fallback={<p class="ex-none">No definition was called.</p>}>
|
|
358
|
+
<CallTree calls={e().calls} depth={0} />
|
|
359
|
+
</Show>
|
|
202
360
|
</div>
|
|
203
|
-
<Show when={e().calls.length} fallback={<p class="sim-muted ex-none">No definition was called.</p>}>
|
|
204
|
-
<CallTree calls={e().calls} depth={0} />
|
|
205
|
-
</Show>
|
|
206
361
|
</>
|
|
207
362
|
)}
|
|
208
363
|
</Show>
|
|
209
364
|
</div>
|
|
210
|
-
|
|
211
|
-
</Show>
|
|
212
|
-
|
|
213
|
-
<div class="algorithm-body">
|
|
214
|
-
<Code lines={lines()} marks={mainMarks()} />
|
|
365
|
+
</Show>
|
|
215
366
|
</div>
|
|
216
|
-
|
|
217
|
-
<Show when={defLines().length > 0}>
|
|
218
|
-
<div class="algorithm-defs">
|
|
219
|
-
<div class="defs-title" onClick={() => setDefsOpen(v => !v)} title={defsOpen() ? 'Fold the definitions' : 'Show the definitions'}>
|
|
220
|
-
{defsOpen() ? '▾' : '▸'} Definitions <span class="sim-muted">({defLines().length}; a click on a function name opens its definition)</span>
|
|
221
|
-
</div>
|
|
222
|
-
<Show when={defsOpen()}>
|
|
223
|
-
<For each={defLines()}>
|
|
224
|
-
{(d) => {
|
|
225
|
-
const marks = () => defMarks().get(d.name) ?? new Map<number, Mark[]>();
|
|
226
|
-
const agents = () => [...new Map([...marks().values()].flat().map(m => [m.agent.index, m.agent])).values()];
|
|
227
|
-
return (
|
|
228
|
-
<div class={`def${flash() === d.name ? ' flash' : ''}`} ref={el => defElements.set(d.name, el)}>
|
|
229
|
-
<Show when={open().has(d.name)} fallback={
|
|
230
|
-
<div class="algorithm-line def-folded" onClick={() => toggle(d.name)} title="Show the definition">
|
|
231
|
-
<span class="line-number">▸</span>
|
|
232
|
-
<span class="line-content">
|
|
233
|
-
<For each={d.tokens[0]?.tokens ?? []}>{(t) => t.kind ? <span class={`tok-${t.kind}`}>{t.text}</span> : t.text}</For>
|
|
234
|
-
</span>
|
|
235
|
-
<Show when={agents().length}>
|
|
236
|
-
<span class="agent-markers">
|
|
237
|
-
<For each={agents()}>{(a) => <span class="agent-marker return-marker" style={{ color: a.color }}>↩ {a.agentId}</span>}</For>
|
|
238
|
-
</span>
|
|
239
|
-
</Show>
|
|
240
|
-
</div>
|
|
241
|
-
}>
|
|
242
|
-
<div class="def-open">
|
|
243
|
-
<div class="def-fold" onClick={() => toggle(d.name)} title="Fold">▾ {d.name}</div>
|
|
244
|
-
<Code lines={d.tokens} marks={marks()} def={d.name} />
|
|
245
|
-
</div>
|
|
246
|
-
</Show>
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
}}
|
|
250
|
-
</For>
|
|
251
|
-
</Show>
|
|
252
|
-
</div>
|
|
253
|
-
</Show>
|
|
254
367
|
</div>
|
|
255
368
|
);
|
|
256
369
|
}
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { For, Show, createMemo, createSignal, onCleanup, onMount } from 'solid-js';
|
|
7
7
|
import type { ExecutionStatus } from '@bramas/simulant-core/types/project';
|
|
8
|
+
import { Icon } from './Icon';
|
|
8
9
|
import './BranchPicker.css';
|
|
9
10
|
|
|
10
11
|
export type Branch = {
|
|
@@ -62,14 +63,17 @@ export function BranchPicker(props: BranchPickerProps) {
|
|
|
62
63
|
<div class="bp-backdrop" onClick={e => { if (e.target === e.currentTarget) props.onClose(); }}>
|
|
63
64
|
<div class="bp-window" role="dialog" aria-modal="true" aria-label="Branches">
|
|
64
65
|
<div class="bp-header">
|
|
65
|
-
<
|
|
66
|
+
<Icon name="branch" />
|
|
67
|
+
<div class="bp-title">
|
|
68
|
+
<h3>{props.branches.length} branches at this step</h3>
|
|
69
|
+
<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>
|
|
70
|
+
</div>
|
|
71
|
+
<span class="sim-spacer" />
|
|
66
72
|
<Show when={props.branches.length > 8}>
|
|
67
73
|
<input type="search" placeholder="Filter (index or choice)" value={filter()} onInput={e => setFilter(e.currentTarget.value)} aria-label="Filter the branches" />
|
|
68
74
|
</Show>
|
|
69
|
-
<
|
|
70
|
-
<button class="sim-ghost sim-icon" onClick={props.onClose} title="Close (Esc)">✕</button>
|
|
75
|
+
<button class="sim-ghost sim-icon" onClick={props.onClose} aria-label="Close" title="Close (Esc)"><Icon name="close" /></button>
|
|
71
76
|
</div>
|
|
72
|
-
<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>
|
|
73
77
|
<div class="bp-grid">
|
|
74
78
|
<For each={shown()} fallback={<p class="sim-muted">No branch matches.</p>}>
|
|
75
79
|
{b => (
|
|
@@ -77,6 +81,7 @@ export function BranchPicker(props: BranchPickerProps) {
|
|
|
77
81
|
<Show when={props.draw}><Thumb index={b.index} /></Show>
|
|
78
82
|
<span class="bp-caption">
|
|
79
83
|
<strong>Branch {b.index}</strong>
|
|
84
|
+
<Show when={b.index === props.current}><span class="bp-current-tag">current</span></Show>
|
|
80
85
|
<Show when={b.verdict}>{v => <span class={`sim-chip sim-${v()}`}>{VERDICT_TEXT[v()]}</span>}</Show>
|
|
81
86
|
</span>
|
|
82
87
|
<span class="bp-label">{b.short || b.label || '(no choice)'}</span>
|