@bramas/simulant-ui 0.0.0-stage → 0.1.1

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.
Files changed (53) hide show
  1. package/LICENCE.txt +515 -0
  2. package/dist/SimulatorApp.jsx +601 -0
  3. package/dist/components/AlgorithmViewer.css +270 -0
  4. package/dist/components/AlgorithmViewer.jsx +218 -0
  5. package/dist/components/BranchPicker.css +96 -0
  6. package/dist/components/BranchPicker.jsx +65 -0
  7. package/dist/components/ConfigGraphGenerator.css +129 -0
  8. package/dist/components/ConfigGraphGenerator.jsx +408 -0
  9. package/dist/components/GenericEditor.jsx +228 -0
  10. package/dist/components/LeanView.jsx +133 -0
  11. package/dist/components/ModelView.css +520 -0
  12. package/dist/components/ModelView.jsx +272 -0
  13. package/dist/components/SettingsLibrary.jsx +61 -0
  14. package/dist/components/StateInspector.css +136 -0
  15. package/dist/components/StateInspector.jsx +103 -0
  16. package/dist/components/highlight.js +99 -0
  17. package/dist/pico.min.css +4 -0
  18. package/dist/renderers/define.js +28 -0
  19. package/dist/renderers/ring.js +103 -0
  20. package/dist/sim/branches.js +28 -0
  21. package/dist/sim/describe.js +408 -0
  22. package/dist/sim/execution.js +42 -0
  23. package/dist/sim/project.js +349 -0
  24. package/dist/sim/snapshot.js +29 -0
  25. package/dist/sim/types.js +1 -0
  26. package/dist/simulator.css +593 -0
  27. package/package.json +50 -3
  28. package/src/SimulatorApp.tsx +661 -0
  29. package/src/components/AlgorithmViewer.css +270 -0
  30. package/src/components/AlgorithmViewer.tsx +256 -0
  31. package/src/components/BranchPicker.css +96 -0
  32. package/src/components/BranchPicker.tsx +90 -0
  33. package/src/components/ConfigGraphGenerator.css +129 -0
  34. package/src/components/ConfigGraphGenerator.tsx +448 -0
  35. package/src/components/GenericEditor.tsx +413 -0
  36. package/src/components/LeanView.tsx +162 -0
  37. package/src/components/ModelView.css +520 -0
  38. package/src/components/ModelView.tsx +317 -0
  39. package/src/components/SettingsLibrary.tsx +84 -0
  40. package/src/components/StateInspector.css +136 -0
  41. package/src/components/StateInspector.tsx +125 -0
  42. package/src/components/highlight.ts +100 -0
  43. package/src/pico.min.css +4 -0
  44. package/src/renderers/define.ts +54 -0
  45. package/src/renderers/ring.ts +112 -0
  46. package/src/sim/branches.ts +30 -0
  47. package/src/sim/describe.ts +416 -0
  48. package/src/sim/execution.ts +65 -0
  49. package/src/sim/project.ts +377 -0
  50. package/src/sim/snapshot.ts +60 -0
  51. package/src/sim/types.ts +28 -0
  52. package/src/simulator.css +593 -0
  53. package/README.md +0 -3
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Algorithm Viewer Styles: the algorithm's .sim source, with agent markers (tokens in simulator.css).
3
+ */
4
+
5
+ .algorithm-viewer {
6
+ font-family: var(--sim-mono);
7
+ font-size: 13px;
8
+ background-color: var(--sim-surface);
9
+ color: var(--sim-text);
10
+ border: 1px solid var(--sim-border);
11
+ border-radius: var(--sim-radius);
12
+ padding: 0.75rem 0 0.5rem;
13
+ max-width: 100%;
14
+ overflow-x: auto;
15
+ text-align: left;
16
+ }
17
+
18
+ .algorithm-header {
19
+ display: flex;
20
+ align-items: baseline;
21
+ gap: 0.75rem;
22
+ padding: 0 1rem 0.6rem;
23
+ border-bottom: 1px solid var(--sim-border);
24
+ }
25
+
26
+ .algorithm-caption {
27
+ margin: 0;
28
+ font-size: 1rem;
29
+ font-weight: 600;
30
+ color: var(--sim-text);
31
+ font-family: var(--pico-font-family, system-ui, sans-serif);
32
+ }
33
+
34
+ .algorithm-file {
35
+ color: var(--sim-muted);
36
+ font-size: 0.8rem;
37
+ }
38
+
39
+ .algorithm-body {
40
+ padding: 0.4rem 0;
41
+ line-height: 1.6;
42
+ }
43
+
44
+ .algorithm-line {
45
+ display: flex;
46
+ align-items: baseline;
47
+ min-height: 1.6em;
48
+ border-left: 3px solid transparent;
49
+ padding-right: 1rem;
50
+ }
51
+
52
+ .algorithm-line:hover {
53
+ background-color: var(--sim-surface-2);
54
+ }
55
+
56
+ .algorithm-line.highlighted {
57
+ border-left-color: var(--sim-highlight);
58
+ background-color: var(--sim-highlight-soft);
59
+ }
60
+
61
+ /* The call that led to an agent's line. */
62
+ .algorithm-line.calling {
63
+ border-left-color: var(--sim-highlight-soft);
64
+ background-color: var(--sim-highlight-faint);
65
+ }
66
+
67
+ .line-number {
68
+ display: inline-block;
69
+ width: 3.5em;
70
+ padding-right: 1em;
71
+ text-align: right;
72
+ color: var(--sim-muted);
73
+ opacity: 0.7;
74
+ user-select: none;
75
+ flex-shrink: 0;
76
+ }
77
+
78
+ .line-content {
79
+ white-space: pre;
80
+ }
81
+
82
+ .agent-markers {
83
+ display: inline-flex;
84
+ gap: 6px;
85
+ margin-left: 1.5em;
86
+ flex-shrink: 0;
87
+ font-family: var(--pico-font-family, system-ui, sans-serif);
88
+ font-size: 12px;
89
+ }
90
+
91
+ .agent-marker {
92
+ font-weight: 600;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .agent-marker.call-marker {
97
+ font-weight: 400;
98
+ opacity: 0.6;
99
+ }
100
+
101
+ /* Syntax highlighting */
102
+ .tok-keyword { color: var(--tok-keyword); font-weight: 600; }
103
+ .tok-action { color: var(--tok-action); font-weight: 700; }
104
+ .tok-function { color: var(--tok-function); }
105
+ .tok-comment { color: var(--tok-comment); font-style: italic; }
106
+ .tok-constant { color: var(--tok-constant); }
107
+ .tok-number { color: var(--tok-number); }
108
+ .tok-type { color: var(--tok-type); }
109
+ .tok-string { color: var(--tok-string); }
110
+
111
+ @media (max-width: 768px) {
112
+ .algorithm-viewer { font-size: 12px; }
113
+ }
114
+
115
+ /* The line a definition returned from, in an agent's last step. */
116
+ .algorithm-line.returned {
117
+ border-left-color: var(--sim-accent);
118
+ background-color: var(--sim-accent-soft);
119
+ }
120
+
121
+ .algorithm-line.flash {
122
+ animation: flash 1.6s ease-out;
123
+ }
124
+
125
+ @keyframes flash {
126
+ 0%, 40% { background-color: var(--sim-highlight-soft); }
127
+ 100% { background-color: transparent; }
128
+ }
129
+
130
+ .agent-marker {
131
+ cursor: pointer;
132
+ border-radius: 4px;
133
+ padding: 0 0.2rem;
134
+ }
135
+
136
+ .agent-marker:hover,
137
+ .agent-marker.selected {
138
+ background-color: var(--sim-surface-2);
139
+ outline: 1px solid currentColor;
140
+ }
141
+
142
+ .agent-marker.return-marker {
143
+ font-weight: 500;
144
+ }
145
+
146
+ .tok-link {
147
+ cursor: pointer;
148
+ }
149
+
150
+ .tok-link:hover {
151
+ text-decoration: underline;
152
+ }
153
+
154
+ /* The definitions the algorithm uses */
155
+
156
+ .algorithm-defs {
157
+ border-top: 1px solid var(--sim-border);
158
+ padding-top: 0.4rem;
159
+ }
160
+
161
+ .defs-title {
162
+ cursor: pointer;
163
+ user-select: none;
164
+ padding: 0.2rem 1rem 0.4rem;
165
+ font-family: var(--pico-font-family, system-ui, sans-serif);
166
+ font-size: 0.85rem;
167
+ font-weight: 600;
168
+ }
169
+
170
+ .def-folded {
171
+ cursor: pointer;
172
+ opacity: 0.85;
173
+ }
174
+
175
+ .def-folded .line-number {
176
+ opacity: 0.6;
177
+ }
178
+
179
+ .def-open {
180
+ margin: 0.2rem 0;
181
+ border-top: 1px dashed var(--sim-border);
182
+ border-bottom: 1px dashed var(--sim-border);
183
+ }
184
+
185
+ .def-fold {
186
+ cursor: pointer;
187
+ padding: 0.1rem 1rem;
188
+ font-size: 0.75rem;
189
+ color: var(--sim-muted);
190
+ font-family: var(--pico-font-family, system-ui, sans-serif);
191
+ }
192
+
193
+ /* The explanation of an agent's last step */
194
+
195
+ .ex-panel {
196
+ margin: 0.5rem 0.75rem;
197
+ padding: 0.5rem 0.75rem;
198
+ border: 1px solid var(--sim-border);
199
+ border-radius: 8px;
200
+ background: var(--sim-surface-2);
201
+ font-size: 12px;
202
+ }
203
+
204
+ .ex-head {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 0.6rem;
208
+ font-family: var(--pico-font-family, system-ui, sans-serif);
209
+ font-size: 0.85rem;
210
+ }
211
+
212
+ .ex-view {
213
+ margin: 0.4rem 0;
214
+ display: grid;
215
+ gap: 0.1rem;
216
+ }
217
+
218
+ .ex-view > div,
219
+ .ex-call {
220
+ white-space: nowrap;
221
+ overflow: hidden;
222
+ text-overflow: ellipsis;
223
+ }
224
+
225
+ .ex-calls {
226
+ list-style: none;
227
+ margin: 0;
228
+ padding-left: 1rem;
229
+ }
230
+
231
+ .ex-panel > .ex-calls {
232
+ padding-left: 0;
233
+ }
234
+
235
+ .ex-calls li {
236
+ list-style: none;
237
+ margin: 0;
238
+ }
239
+
240
+ .ex-call {
241
+ display: flex;
242
+ gap: 0.35rem;
243
+ align-items: baseline;
244
+ }
245
+
246
+ .ex-toggle {
247
+ width: 0.8rem;
248
+ cursor: pointer;
249
+ color: var(--sim-muted);
250
+ flex-shrink: 0;
251
+ }
252
+
253
+ .ex-args,
254
+ .ex-eq {
255
+ color: var(--sim-muted);
256
+ }
257
+
258
+ .ex-result {
259
+ color: var(--tok-number);
260
+ }
261
+
262
+ .ex-line {
263
+ color: var(--sim-accent);
264
+ cursor: pointer;
265
+ flex-shrink: 0;
266
+ }
267
+
268
+ .ex-none {
269
+ margin: 0.3rem 0 0;
270
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * The algorithm's `.sim` source, as written, with a cursor on each agent's line: the statement
3
+ * that decided its last step (or its program counter). An agent inside a procedure is also marked,
4
+ * more lightly, on the lines of the calls that led there. Below, the definitions the algorithm
5
+ * uses, folded to their signatures (a click on a function name opens one); an agent is marked
6
+ * (↩) on the lines its last step returned from. Clicking a cursor explains that agent's last
7
+ * step: its view, and each definition it called with its arguments and result.
8
+ */
9
+ import { For, Show, createMemo, createSignal, onCleanup, type JSX } from 'solid-js';
10
+ import type { AgentExecutionState, AlgorithmSource, TraceCall } from '@bramas/simulant-core/types/project';
11
+ import { tokenizeAll, type Line } from './highlight';
12
+ import './AlgorithmViewer.css';
13
+
14
+ export type { AgentExecutionState, AlgorithmSource } from '@bramas/simulant-core/types/project';
15
+
16
+ export interface AlgorithmViewerProps {
17
+ source: AlgorithmSource;
18
+ agents?: AgentExecutionState[];
19
+ }
20
+
21
+ type Mark = { agent: AgentExecutionState; kind: 'at' | 'call' | 'return' };
22
+ const SHORT = 48;
23
+ const shorten = (s: string) => s.length > SHORT ? s.slice(0, SHORT - 1) + '…' : s;
24
+
25
+ export function AlgorithmViewer(props: AlgorithmViewerProps) {
26
+ const definitions = () => props.source.definitions ?? [];
27
+ const defNames = createMemo(() => new Set(definitions().map(d => d.name)));
28
+ const procedures = createMemo(() => {
29
+ const out = new Set(['main', ...defNames()]);
30
+ for (const l of props.source.lines) {
31
+ const m = /^\s*def\s+(\w+)/.exec(l.text);
32
+ if (m) out.add(m[1]);
33
+ }
34
+ return out;
35
+ });
36
+ const lines = createMemo(() => tokenizeAll(props.source.lines, procedures()));
37
+ const defLines = createMemo(() => definitions().map(d => ({ ...d, tokens: tokenizeAll(d.lines, procedures()) })));
38
+
39
+ /** The line an agent is shown at: the statement that decided its last step, or its program counter. */
40
+ const lineOf = (a: AgentExecutionState) => a.explanation?.line ?? a.line;
41
+
42
+ const mainMarks = createMemo(() => {
43
+ const out = new Map<number, Mark[]>();
44
+ const add = (line: number, m: Mark) => out.set(line, [...(out.get(line) ?? []), m]);
45
+ for (const a of props.agents ?? []) {
46
+ add(lineOf(a), { agent: a, kind: 'at' });
47
+ for (const c of new Set(a.calls ?? [])) add(c, { agent: a, kind: 'call' });
48
+ }
49
+ return out;
50
+ });
51
+ /** Per definition and line, the agents whose last step returned from that line (once per agent). */
52
+ const defMarks = createMemo(() => {
53
+ const out = new Map<string, Map<number, Mark[]>>();
54
+ for (const a of props.agents ?? []) {
55
+ const seen = new Set<string>();
56
+ const walk = (calls: TraceCall[]) => {
57
+ for (const c of calls) {
58
+ const key = `${c.fn}:${c.line}`;
59
+ if (c.line !== undefined && !seen.has(key)) {
60
+ seen.add(key);
61
+ const lines = out.get(c.fn) ?? new Map<number, Mark[]>();
62
+ lines.set(c.line, [...(lines.get(c.line) ?? []), { agent: a, kind: 'return' }]);
63
+ out.set(c.fn, lines);
64
+ }
65
+ walk(c.calls);
66
+ }
67
+ };
68
+ walk(a.explanation?.calls ?? []);
69
+ }
70
+ return out;
71
+ });
72
+
73
+ // Folding, the selected agent, and the line to flash after a jump: kept while stepping.
74
+ const [open, setOpen] = createSignal<Set<string>>(new Set());
75
+ // The whole group of definitions is folded at first.
76
+ const [defsOpen, setDefsOpen] = createSignal(false);
77
+ const [selected, setSelected] = createSignal<number | undefined>();
78
+ const [flash, setFlash] = createSignal<string | undefined>();
79
+ const defElements = new Map<string, HTMLElement>();
80
+ let flashTimer: ReturnType<typeof setTimeout> | undefined;
81
+ onCleanup(() => clearTimeout(flashTimer));
82
+ const toggle = (name: string) => setOpen(o => { const n = new Set(o); if (n.has(name)) n.delete(name); else n.add(name); return n; });
83
+ /** Opens a definition and scrolls to it (and to a line of it). */
84
+ const jump = (name: string, line?: number) => {
85
+ if (!defNames().has(name)) return;
86
+ setDefsOpen(true);
87
+ setOpen(o => new Set(o).add(name));
88
+ setFlash(line !== undefined ? `${name}:${line}` : name);
89
+ clearTimeout(flashTimer);
90
+ flashTimer = setTimeout(() => setFlash(undefined), 1600);
91
+ queueMicrotask(() => {
92
+ const el = defElements.get(name);
93
+ const target = line !== undefined ? el?.querySelector(`[data-line-number="${line}"]`) : el;
94
+ (target ?? el)?.scrollIntoView({ block: 'center', behavior: 'smooth' });
95
+ });
96
+ };
97
+ const selectedAgent = createMemo(() => (props.agents ?? []).find(a => a.index !== undefined && a.index === selected()));
98
+
99
+ const Code = (p: { lines: Line[]; marks: Map<number, Mark[]>; def?: string }): JSX.Element => (
100
+ <For each={p.lines}>
101
+ {(line) => {
102
+ const marks = () => p.marks.get(line.number) ?? [];
103
+ const classes = () => [
104
+ 'algorithm-line',
105
+ marks().some(m => m.kind === 'at') ? 'highlighted' : marks().some(m => m.kind === 'return') ? 'returned' : marks().length ? 'calling' : '',
106
+ p.def && flash() === `${p.def}:${line.number}` ? 'flash' : '',
107
+ ].filter(Boolean).join(' ');
108
+ return (
109
+ <div class={classes()} data-line-number={line.number}>
110
+ <span class="line-number">{line.number}</span>
111
+ <span class="line-content">
112
+ <For each={line.tokens}>
113
+ {(t) => t.kind === 'function' && defNames().has(t.text)
114
+ ? <span class="tok-function tok-link" onClick={() => jump(t.text)} title={`Show the definition of ${t.text}`}>{t.text}</span>
115
+ : t.kind ? <span class={`tok-${t.kind}`}>{t.text}</span> : t.text}
116
+ </For>
117
+ </span>
118
+ <Show when={marks().length > 0}>
119
+ <span class="agent-markers">
120
+ <For each={marks()}>
121
+ {(m) => (
122
+ <span
123
+ class={`agent-marker${m.kind === 'call' ? ' call-marker' : m.kind === 'return' ? ' return-marker' : ''}${selected() === m.agent.index ? ' selected' : ''}`}
124
+ style={{ color: m.agent.color }}
125
+ title={m.kind === 'at' ? `${m.agent.agentId} is at this line: click to explain its last step`
126
+ : m.kind === 'call' ? `${m.agent.agentId} is inside the procedure called here`
127
+ : `${m.agent.agentId}'s last step returned from this line: click to explain it`}
128
+ onClick={() => setSelected(s => s === m.agent.index ? undefined : m.agent.index)}
129
+ >
130
+ {m.kind === 'at' ? '◀' : m.kind === 'call' ? '◁' : '↩'} {m.agent.agentId}
131
+ </span>
132
+ )}
133
+ </For>
134
+ </span>
135
+ </Show>
136
+ </div>
137
+ );
138
+ }}
139
+ </For>
140
+ );
141
+
142
+ const CallTree = (p: { calls: TraceCall[]; depth: number }): JSX.Element => (
143
+ <ul class="ex-calls">
144
+ <For each={p.calls}>
145
+ {(c) => {
146
+ const [expanded, setExpanded] = createSignal(false);
147
+ return (
148
+ <li>
149
+ <div class="ex-call">
150
+ <span class="ex-toggle" onClick={() => setExpanded(v => !v)}>{c.calls.length ? (expanded() ? '▾' : '▸') : ''}</span>
151
+ <span class="tok-function tok-link" onClick={() => jump(c.fn, c.line)}>{c.fn}</span>
152
+ <span class="ex-args" title={c.args.join(', ')}>({c.args.map(shorten).join(', ')})</span>
153
+ <span class="ex-eq">=</span>
154
+ <span class="ex-result" title={c.result}>{shorten(c.result)}</span>
155
+ <Show when={c.line !== undefined}>
156
+ <span class="ex-line" onClick={() => jump(c.fn, c.line)} title="Show where it returned">↩ {c.line}</span>
157
+ </Show>
158
+ </div>
159
+ <Show when={expanded() && c.calls.length}>
160
+ <CallTree calls={c.calls} depth={p.depth + 1} />
161
+ </Show>
162
+ </li>
163
+ );
164
+ }}
165
+ </For>
166
+ </ul>
167
+ );
168
+
169
+ return (
170
+ <div class="algorithm-viewer">
171
+ <div class="algorithm-header">
172
+ <h3 class="algorithm-caption">{props.source.name}</h3>
173
+ <Show when={props.source.file}>
174
+ <span class="algorithm-file">{props.source.file!.split('/').pop()}</span>
175
+ </Show>
176
+ </div>
177
+
178
+ <Show when={selectedAgent()}>
179
+ {a => (
180
+ <div class="ex-panel">
181
+ <div class="ex-head">
182
+ <strong style={{ color: a().color }}>{a().agentId}</strong>
183
+ <Show when={a().explanation} fallback={<span class="sim-muted">did not take a decision in the last step (not activated, or the initial configuration).</span>}>
184
+ {e => (
185
+ <span>
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>
194
+ </div>
195
+ <Show when={a().explanation}>
196
+ {e => (
197
+ <>
198
+ <div class="ex-view">
199
+ <For each={Object.entries(e().view)}>
200
+ {([k, v]) => <div><span class="si-key">{k}</span> <span class="ex-result" title={v}>{v}</span></div>}
201
+ </For>
202
+ </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
+ </>
207
+ )}
208
+ </Show>
209
+ </div>
210
+ )}
211
+ </Show>
212
+
213
+ <div class="algorithm-body">
214
+ <Code lines={lines()} marks={mainMarks()} />
215
+ </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
+ </div>
255
+ );
256
+ }
@@ -0,0 +1,96 @@
1
+ /* The window listing the branches of a step (tokens in simulator.css). */
2
+
3
+ .bp-backdrop {
4
+ position: fixed;
5
+ inset: 0;
6
+ background: rgba(0, 0, 0, 0.45);
7
+ z-index: 50;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ padding: 2rem;
12
+ }
13
+
14
+ .bp-window {
15
+ background: var(--sim-surface);
16
+ color: var(--sim-text);
17
+ border: 1px solid var(--sim-border);
18
+ border-radius: var(--sim-radius);
19
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
20
+ width: min(1100px, 100%);
21
+ max-height: 100%;
22
+ display: flex;
23
+ flex-direction: column;
24
+ padding: 1rem 1.25rem;
25
+ }
26
+
27
+ .bp-header {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 0.75rem;
31
+ }
32
+
33
+ .bp-header h3 {
34
+ margin: 0;
35
+ font-size: 1.05rem;
36
+ }
37
+
38
+ .bp-header input {
39
+ max-width: 18rem;
40
+ margin: 0;
41
+ }
42
+
43
+ .bp-help {
44
+ margin: 0.4rem 0 0.75rem;
45
+ }
46
+
47
+ .bp-grid {
48
+ display: grid;
49
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
50
+ gap: 0.75rem;
51
+ overflow: auto;
52
+ padding: 2px;
53
+ }
54
+
55
+ .sim-app .bp-branch,
56
+ .bp-branch {
57
+ display: flex;
58
+ flex-direction: column;
59
+ align-items: center;
60
+ gap: 0.35rem;
61
+ width: 100%;
62
+ padding: 0.5rem;
63
+ background: var(--sim-surface-2);
64
+ color: var(--sim-text);
65
+ border: 2px solid var(--sim-border);
66
+ border-radius: 8px;
67
+ text-align: center;
68
+ font-size: 0.8rem;
69
+ }
70
+
71
+ .bp-branch:hover {
72
+ border-color: var(--sim-accent);
73
+ }
74
+
75
+ .bp-branch.bp-current {
76
+ border-color: var(--sim-highlight);
77
+ background: var(--sim-highlight-soft);
78
+ }
79
+
80
+ .bp-thumb {
81
+ background: white;
82
+ border-radius: 6px;
83
+ }
84
+
85
+ .bp-caption {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 0.4rem;
89
+ }
90
+
91
+ .bp-label {
92
+ font-family: var(--sim-mono);
93
+ font-size: 0.72rem;
94
+ color: var(--sim-muted);
95
+ word-break: break-word;
96
+ }