@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** The interface's icons: inline strokes on a 24-unit grid, in the current text color. */
|
|
2
|
+
import type { JSX } from 'solid-js';
|
|
3
|
+
|
|
4
|
+
const PATHS = {
|
|
5
|
+
first: () => <><path d="M18 19 9 12l9-7z" /><path d="M6 5v14" /></>,
|
|
6
|
+
last: () => <><path d="m6 5 9 7-9 7z" /><path d="M18 5v14" /></>,
|
|
7
|
+
previous: () => <path d="m15 18-6-6 6-6" />,
|
|
8
|
+
next: () => <path d="m9 18 6-6-6-6" />,
|
|
9
|
+
play: () => <path d="M7 4.5v15l12-7.5z" />,
|
|
10
|
+
pause: () => <><path d="M8 5v14" /><path d="M16 5v14" /></>,
|
|
11
|
+
link: () => <><path d="M10 13.5a4.5 4.5 0 0 0 6.4.4l2.8-2.8a4.5 4.5 0 0 0-6.4-6.4l-1.3 1.3" /><path d="M14 10.5a4.5 4.5 0 0 0-6.4-.4l-2.8 2.8a4.5 4.5 0 0 0 6.4 6.4l1.3-1.3" /></>,
|
|
12
|
+
sun: () => <><circle cx="12" cy="12" r="4" /><path d="M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4" /></>,
|
|
13
|
+
moon: () => <path d="M20.5 14.2A8.5 8.5 0 1 1 9.8 3.5a6.8 6.8 0 0 0 10.7 10.7z" />,
|
|
14
|
+
grid: () => <><rect x="4" y="4" width="6.5" height="6.5" rx="1.5" /><rect x="13.5" y="4" width="6.5" height="6.5" rx="1.5" /><rect x="4" y="13.5" width="6.5" height="6.5" rx="1.5" /><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.5" /></>,
|
|
15
|
+
branch: () => <><circle cx="6" cy="5.5" r="2" /><circle cx="6" cy="18.5" r="2" /><circle cx="18" cy="8" r="2" /><path d="M6 7.5v9" /><path d="M18 10c0 4-6 3.5-11 7" /></>,
|
|
16
|
+
close: () => <path d="M6 6l12 12M18 6 6 18" />,
|
|
17
|
+
trash: () => <><path d="M4.5 7h15" /><path d="M9.5 7V4.8h5V7" /><path d="M6.5 7l.9 12.2h9.2L17.5 7" /></>,
|
|
18
|
+
download: () => <><path d="M12 4v11" /><path d="m7.5 10.5 4.5 4.5 4.5-4.5" /><path d="M5 19.5h14" /></>,
|
|
19
|
+
reset: () => <><path d="M4 12a8 8 0 1 0 2.5-5.8L4 8.5" /><path d="M4 4v4.5h4.5" /></>,
|
|
20
|
+
keyboard: () => <><rect x="2.5" y="6" width="19" height="12" rx="2" /><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M8 14h8" /></>,
|
|
21
|
+
file: () => <><path d="M14 3.5H7a1.5 1.5 0 0 0-1.5 1.5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8z" /><path d="M14 3.5V8h4.5" /></>,
|
|
22
|
+
sliders: () => <><path d="M4 7h9M17 7h3M4 17h3M11 17h9" /><circle cx="15" cy="7" r="2" /><circle cx="9" cy="17" r="2" /></>,
|
|
23
|
+
bookmark: () => <path d="M7 3.5h10a1 1 0 0 1 1 1v16l-6-4-6 4v-16a1 1 0 0 1 1-1z" />,
|
|
24
|
+
graph: () => <><circle cx="12" cy="5" r="2.2" /><circle cx="5.5" cy="18.5" r="2.2" /><circle cx="18.5" cy="18.5" r="2.2" /><path d="M10.8 6.9 6.7 16.6M13.2 6.9l4.1 9.7" /></>,
|
|
25
|
+
ring: () => <><circle cx="12" cy="12" r="7.5" /><circle cx="12" cy="4.5" r="2" fill="currentColor" /><circle cx="18.5" cy="15.8" r="1.4" /><circle cx="5.5" cy="15.8" r="1.4" /></>,
|
|
26
|
+
layers: () => <><path d="m12 3.5 8.5 4.5-8.5 4.5L3.5 8z" /><path d="m3.5 12 8.5 4.5 8.5-4.5" /><path d="m3.5 16 8.5 4.5 8.5-4.5" /></>,
|
|
27
|
+
chevronDown: () => <path d="m6 9 6 6 6-6" />,
|
|
28
|
+
chevronRight: () => <path d="m9 6 6 6-6 6" />,
|
|
29
|
+
check: () => <path d="m5 12.5 4.5 4.5L19 7.5" />,
|
|
30
|
+
alert: () => <><path d="M12 3.5 2.5 20h19z" /><path d="M12 10v4.5M12 17.2h.01" /></>,
|
|
31
|
+
info: () => <><circle cx="12" cy="12" r="8.5" /><path d="M12 11v5.5M12 7.8h.01" /></>,
|
|
32
|
+
minus: () => <path d="M5 12h14" />,
|
|
33
|
+
plus: () => <path d="M12 5v14M5 12h14" />,
|
|
34
|
+
fit: () => <><path d="M4 9V5.5A1.5 1.5 0 0 1 5.5 4H9M15 4h3.5A1.5 1.5 0 0 1 20 5.5V9M20 15v3.5a1.5 1.5 0 0 1-1.5 1.5H15M9 20H5.5A1.5 1.5 0 0 1 4 18.5V15" /></>,
|
|
35
|
+
inspect: () => <><path d="M4 6h16M4 12h10M4 18h7" /><circle cx="17.5" cy="16.5" r="2.5" /><path d="m19.3 18.3 1.7 1.7" /></>,
|
|
36
|
+
stop: () => <rect x="6" y="6" width="12" height="12" rx="2" />,
|
|
37
|
+
dock: () => <><rect x="3.5" y="4.5" width="17" height="15" rx="2" /><path d="M14 4.5v15" /></>,
|
|
38
|
+
dockLeft: () => <><rect x="3.5" y="4.5" width="17" height="15" rx="2" /><path d="M9 4.5v15" /><path d="M5.5 8h1.5M5.5 11h1.5" /></>,
|
|
39
|
+
dockRight: () => <><rect x="3.5" y="4.5" width="17" height="15" rx="2" /><path d="M15 4.5v15" /><path d="M17 8h1.5M17 11h1.5" /></>,
|
|
40
|
+
dockBottom: () => <><rect x="3.5" y="4.5" width="17" height="15" rx="2" /><path d="M3.5 14h17" /><path d="M7 17h4" /></>,
|
|
41
|
+
dice: () => <><rect x="4" y="4" width="16" height="16" rx="3.5" /><circle cx="8.6" cy="8.6" r="1.25" fill="currentColor" stroke="none" /><circle cx="15.4" cy="8.6" r="1.25" fill="currentColor" stroke="none" /><circle cx="12" cy="12" r="1.25" fill="currentColor" stroke="none" /><circle cx="8.6" cy="15.4" r="1.25" fill="currentColor" stroke="none" /><circle cx="15.4" cy="15.4" r="1.25" fill="currentColor" stroke="none" /></>,
|
|
42
|
+
grip: () => <><circle cx="9" cy="6" r="1.2" fill="currentColor" /><circle cx="15" cy="6" r="1.2" fill="currentColor" /><circle cx="9" cy="12" r="1.2" fill="currentColor" /><circle cx="15" cy="12" r="1.2" fill="currentColor" /><circle cx="9" cy="18" r="1.2" fill="currentColor" /><circle cx="15" cy="18" r="1.2" fill="currentColor" /></>,
|
|
43
|
+
} satisfies Record<string, () => JSX.Element>;
|
|
44
|
+
|
|
45
|
+
export type IconName = keyof typeof PATHS;
|
|
46
|
+
|
|
47
|
+
export function Icon(props: { name: IconName; size?: number; class?: string }) {
|
|
48
|
+
return (
|
|
49
|
+
<svg
|
|
50
|
+
class={`sim-svg${props.class ? ` ${props.class}` : ''}`}
|
|
51
|
+
width={props.size ?? 16}
|
|
52
|
+
height={props.size ?? 16}
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
stroke-width="1.8"
|
|
57
|
+
stroke-linecap="round"
|
|
58
|
+
stroke-linejoin="round"
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
>
|
|
61
|
+
{PATHS[props.name]()}
|
|
62
|
+
</svg>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The Simulant mark: agents on a ring. */
|
|
67
|
+
export function Logo() {
|
|
68
|
+
return (
|
|
69
|
+
<svg class="sim-logo" width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
|
|
70
|
+
<circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2" opacity="0.35" />
|
|
71
|
+
<circle cx="12" cy="4" r="3" fill="var(--sim-accent)" />
|
|
72
|
+
<circle cx="18.9" cy="16" r="2.2" fill="currentColor" />
|
|
73
|
+
<circle cx="5.1" cy="16" r="2.2" fill="currentColor" opacity="0.6" />
|
|
74
|
+
</svg>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model view: facts on the left, the declarations as written on the right (tokens and code
|
|
3
|
-
* lines from simulator.css and AlgorithmViewer.css).
|
|
4
|
-
* article's <header> negative margins, which pushed the header over the block's border.
|
|
3
|
+
* lines from simulator.css and AlgorithmViewer.css).
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
.model-view {
|
|
@@ -19,8 +18,9 @@
|
|
|
19
18
|
|
|
20
19
|
.mv-intro h2 {
|
|
21
20
|
margin: 0;
|
|
22
|
-
font-size: 1.
|
|
21
|
+
font-size: 1.2rem;
|
|
23
22
|
font-family: var(--sim-mono);
|
|
23
|
+
letter-spacing: -0.01em;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.mv-chips {
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
.mv-facts dt {
|
|
96
|
-
font-size:
|
|
97
|
-
font-weight:
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
font-weight: 500;
|
|
98
98
|
color: var(--sim-muted);
|
|
99
99
|
padding-top: 0.12rem;
|
|
100
100
|
}
|
|
@@ -248,11 +248,11 @@
|
|
|
248
248
|
|
|
249
249
|
.mv-sources {
|
|
250
250
|
position: sticky;
|
|
251
|
-
top: 0
|
|
251
|
+
top: 0;
|
|
252
252
|
display: flex;
|
|
253
253
|
flex-direction: column;
|
|
254
254
|
gap: 0.6rem;
|
|
255
|
-
max-height: calc(100vh -
|
|
255
|
+
max-height: calc(100vh - var(--sim-topbar-h) - var(--sim-statusbar-h) - 2 * var(--sim-gap));
|
|
256
256
|
min-width: 0;
|
|
257
257
|
}
|
|
258
258
|
|
|
@@ -272,8 +272,8 @@
|
|
|
272
272
|
gap: 0.35rem;
|
|
273
273
|
font-family: var(--sim-mono);
|
|
274
274
|
font-size: 0.75rem;
|
|
275
|
-
padding: 0.
|
|
276
|
-
border-radius:
|
|
275
|
+
padding: 0.2rem 0.55rem;
|
|
276
|
+
border-radius: 6px;
|
|
277
277
|
border: 1px solid var(--sim-border);
|
|
278
278
|
background: var(--sim-surface);
|
|
279
279
|
color: var(--sim-text);
|
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
.mv-kind {
|
|
323
|
-
font-family: var(--
|
|
323
|
+
font-family: var(--sim-font);
|
|
324
324
|
font-size: 0.68rem;
|
|
325
325
|
font-weight: 600;
|
|
326
326
|
text-transform: uppercase;
|
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
.sim-app button.mv-defs-head .sim-muted {
|
|
363
|
-
font-family: var(--
|
|
363
|
+
font-family: var(--sim-font);
|
|
364
364
|
font-size: 0.8rem;
|
|
365
365
|
}
|
|
366
366
|
|
|
@@ -478,7 +478,7 @@
|
|
|
478
478
|
flex-direction: column;
|
|
479
479
|
align-items: flex-end;
|
|
480
480
|
padding: 0 0.6rem 0 0.4rem;
|
|
481
|
-
font-family: var(--
|
|
481
|
+
font-family: var(--sim-font);
|
|
482
482
|
font-size: 0.7rem;
|
|
483
483
|
line-height: 1.35;
|
|
484
484
|
text-align: right;
|
|
@@ -505,9 +505,9 @@
|
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
.sim-app button.mv-lean-button {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
508
|
+
height: 24px;
|
|
509
|
+
font-size: 12px;
|
|
510
|
+
padding: 0 8px;
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
.mv-source .algorithm-line.mv-to-lean {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { For, Show, createSignal } from 'solid-js';
|
|
7
7
|
import type { PredefinedSetting } from '@bramas/simulant-core/types/project';
|
|
8
|
+
import { Icon } from './Icon';
|
|
8
9
|
|
|
9
10
|
export type SavedSetting = { name: string; settings: any; path?: number[] };
|
|
10
11
|
|
|
@@ -47,15 +48,16 @@ export function SettingsLibrary(props: SettingsLibraryProps) {
|
|
|
47
48
|
<div class="sim-entry-text">
|
|
48
49
|
<span class="sim-entry-name">{p.s.name}</span>
|
|
49
50
|
<span class="sim-muted">
|
|
51
|
+
<Show when={same(p.s.settings, props.current)}><span class="sim-entry-badge">current · </span></Show>
|
|
50
52
|
{summary(p.s.settings)}
|
|
51
53
|
<Show when={p.s.path && p.s.path.length > 1}> · {p.s.path!.length - 1} steps</Show>
|
|
52
54
|
</span>
|
|
53
55
|
</div>
|
|
54
56
|
<div class="sim-entry-actions">
|
|
55
|
-
<button class="sim-
|
|
56
|
-
<button class="sim-ghost sim-icon" title="Copy a link to these settings" onClick={() => props.onShare(p.s)}
|
|
57
|
+
<button class="sim-btn sim-sm" onClick={() => props.onLoad(p.s)}>Load</button>
|
|
58
|
+
<button class="sim-ghost sim-icon sim-sm" aria-label="Copy a link to these settings" title="Copy a link to these settings" onClick={() => props.onShare(p.s)}><Icon name="link" size={14} /></button>
|
|
57
59
|
<Show when={p.deletable}>
|
|
58
|
-
<button class="sim-ghost sim-icon sim-danger" title="Delete" onClick={() => props.onDelete(p.s.name)}
|
|
60
|
+
<button class="sim-ghost sim-icon sim-sm sim-danger" aria-label={`Delete ${p.s.name}`} title="Delete" onClick={() => props.onDelete(p.s.name)}><Icon name="trash" size={14} /></button>
|
|
59
61
|
</Show>
|
|
60
62
|
</div>
|
|
61
63
|
</li>
|
|
@@ -70,14 +72,14 @@ export function SettingsLibrary(props: SettingsLibraryProps) {
|
|
|
70
72
|
</ul>
|
|
71
73
|
</Show>
|
|
72
74
|
<h4>Saved in this browser</h4>
|
|
73
|
-
<Show when={props.saved.length > 0} fallback={<p class="sim-
|
|
75
|
+
<Show when={props.saved.length > 0} fallback={<p class="sim-library-empty">Nothing saved yet: name the current settings and path below to keep them.</p>}>
|
|
74
76
|
<ul class="sim-entries">
|
|
75
77
|
<For each={props.saved}>{(s) => <Entry s={s} deletable={true} />}</For>
|
|
76
78
|
</ul>
|
|
77
79
|
</Show>
|
|
78
80
|
<form class="sim-save" onSubmit={save}>
|
|
79
81
|
<input type="text" placeholder="Name the current settings and path" value={name()} onInput={e => setName(e.currentTarget.value)} aria-label="Name of the saved settings" />
|
|
80
|
-
<button type="submit" disabled={!name().trim()}
|
|
82
|
+
<button type="submit" class="sim-primary" disabled={!name().trim()}><Icon name="bookmark" size={14} /> Save</button>
|
|
81
83
|
</form>
|
|
82
84
|
</div>
|
|
83
85
|
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A draggable separator between two panels. It reports the size its panel would take,
|
|
3
|
+
* `value` plus the pointer's travel (reversed by `direction: -1` for a panel on the far side);
|
|
4
|
+
* the owner clamps it. A double click restores the default size.
|
|
5
|
+
*/
|
|
6
|
+
import { createSignal } from 'solid-js';
|
|
7
|
+
|
|
8
|
+
export interface SplitterProps {
|
|
9
|
+
/** `vertical`: a bar between columns, dragged sideways; `horizontal`: between rows. */
|
|
10
|
+
orientation: 'vertical' | 'horizontal';
|
|
11
|
+
/** The size of the panel being resized when the drag starts. */
|
|
12
|
+
value: number;
|
|
13
|
+
direction?: 1 | -1;
|
|
14
|
+
onChange: (size: number) => void;
|
|
15
|
+
/** Called when the drag ends (to remember the size). */
|
|
16
|
+
onCommit?: () => void;
|
|
17
|
+
onReset?: () => void;
|
|
18
|
+
label: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Splitter(props: SplitterProps) {
|
|
22
|
+
let start: { at: number; value: number } | undefined;
|
|
23
|
+
const [dragging, setDragging] = createSignal(false);
|
|
24
|
+
const at = (e: PointerEvent) => props.orientation === 'vertical' ? e.clientX : e.clientY;
|
|
25
|
+
const cursor = () => props.orientation === 'vertical' ? 'sim-resizing' : 'sim-resizing-rows';
|
|
26
|
+
|
|
27
|
+
const end = () => {
|
|
28
|
+
if (!start) return;
|
|
29
|
+
start = undefined;
|
|
30
|
+
setDragging(false);
|
|
31
|
+
document.documentElement.classList.remove(cursor());
|
|
32
|
+
props.onCommit?.();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
class={`sim-splitter sim-splitter-${props.orientation}${dragging() ? ' sim-dragging' : ''}`}
|
|
38
|
+
role="separator"
|
|
39
|
+
aria-orientation={props.orientation}
|
|
40
|
+
aria-label={props.label}
|
|
41
|
+
title="Drag to resize; double-click to reset"
|
|
42
|
+
onPointerDown={e => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
45
|
+
start = { at: at(e), value: props.value };
|
|
46
|
+
setDragging(true);
|
|
47
|
+
document.documentElement.classList.add(cursor());
|
|
48
|
+
}}
|
|
49
|
+
onPointerMove={e => { if (start) props.onChange(start.value + (props.direction ?? 1) * (at(e) - start.at)); }}
|
|
50
|
+
onPointerUp={end}
|
|
51
|
+
onPointerCancel={end}
|
|
52
|
+
onDblClick={() => props.onReset?.()}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -1,46 +1,50 @@
|
|
|
1
|
-
/* The state inspector: the configuration as expandable trees (tokens in simulator.css). */
|
|
1
|
+
/* The state inspector: the configuration as expandable trees, inside its panel (tokens in simulator.css). */
|
|
2
2
|
|
|
3
3
|
.state-inspector {
|
|
4
|
-
margin-top: 0.75rem;
|
|
5
4
|
text-align: left;
|
|
6
5
|
font-family: var(--sim-mono);
|
|
7
|
-
font-size:
|
|
8
|
-
background-color: var(--sim-surface);
|
|
6
|
+
font-size: 12px;
|
|
9
7
|
color: var(--sim-text);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.si-section {
|
|
11
|
+
padding: 4px 0;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
.si-section + .si-section {
|
|
16
|
-
margin-top:
|
|
17
|
-
padding-top:
|
|
15
|
+
margin-top: 4px;
|
|
16
|
+
padding-top: 8px;
|
|
18
17
|
border-top: 1px solid var(--sim-border);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
.state-inspector h4 {
|
|
22
|
-
margin: 0 0
|
|
23
|
-
|
|
21
|
+
margin: 0 0 4px;
|
|
22
|
+
padding: 0 4px;
|
|
23
|
+
min-height: 24px;
|
|
24
|
+
font-size: 11px;
|
|
24
25
|
font-weight: 600;
|
|
25
26
|
text-transform: uppercase;
|
|
26
|
-
letter-spacing: 0.
|
|
27
|
+
letter-spacing: 0.06em;
|
|
27
28
|
color: var(--sim-muted);
|
|
28
|
-
font-family: var(--
|
|
29
|
+
font-family: var(--sim-font);
|
|
29
30
|
display: flex;
|
|
30
31
|
align-items: center;
|
|
31
|
-
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
gap: 12px;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
.state-inspector h4.si-inline {
|
|
35
37
|
display: inline;
|
|
36
38
|
margin: 0;
|
|
39
|
+
padding: 0;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
.si-row {
|
|
40
43
|
display: flex;
|
|
41
44
|
align-items: baseline;
|
|
42
|
-
gap:
|
|
43
|
-
|
|
45
|
+
gap: 6px;
|
|
46
|
+
padding: 1px 4px;
|
|
47
|
+
line-height: 1.65;
|
|
44
48
|
white-space: nowrap;
|
|
45
49
|
overflow: hidden;
|
|
46
50
|
text-overflow: ellipsis;
|
|
@@ -48,7 +52,7 @@
|
|
|
48
52
|
|
|
49
53
|
.si-clickable {
|
|
50
54
|
cursor: pointer;
|
|
51
|
-
border-radius:
|
|
55
|
+
border-radius: 5px;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
.si-clickable:hover {
|
|
@@ -59,13 +63,14 @@
|
|
|
59
63
|
display: inline-block;
|
|
60
64
|
width: 1em;
|
|
61
65
|
flex-shrink: 0;
|
|
62
|
-
color: var(--sim-
|
|
66
|
+
color: var(--sim-faint);
|
|
67
|
+
font-size: 10px;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
.si-children {
|
|
66
|
-
margin-left:
|
|
67
|
-
border-left: 1px
|
|
68
|
-
padding-left:
|
|
71
|
+
margin-left: 11px;
|
|
72
|
+
border-left: 1px solid var(--sim-border);
|
|
73
|
+
padding-left: 6px;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
.si-key {
|
|
@@ -74,7 +79,7 @@
|
|
|
74
79
|
|
|
75
80
|
.si-key::after {
|
|
76
81
|
content: ':';
|
|
77
|
-
color: var(--sim-
|
|
82
|
+
color: var(--sim-faint);
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
.si-summary,
|
|
@@ -91,14 +96,17 @@
|
|
|
91
96
|
.si-compound { color: var(--sim-text); }
|
|
92
97
|
|
|
93
98
|
.si-agent {
|
|
94
|
-
border-radius:
|
|
95
|
-
|
|
96
|
-
padding-left: 0.2rem;
|
|
99
|
+
border-radius: 6px;
|
|
100
|
+
scroll-margin: 8px;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
.si-agent.si-selected {
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
background-color: var(--sim-accent-soft);
|
|
105
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sim-accent) 40%, transparent);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.si-agent.si-selected > .si-row:hover {
|
|
109
|
+
background-color: transparent;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
.si-agent.si-hidden .si-agent-name,
|
|
@@ -112,11 +120,12 @@
|
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
.si-swatch {
|
|
115
|
-
width:
|
|
116
|
-
height:
|
|
117
|
-
border-radius:
|
|
123
|
+
width: 9px;
|
|
124
|
+
height: 9px;
|
|
125
|
+
border-radius: 50%;
|
|
118
126
|
flex-shrink: 0;
|
|
119
127
|
align-self: center;
|
|
128
|
+
box-shadow: 0 0 0 2px var(--sim-surface);
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
.si-agent-name {
|
|
@@ -128,9 +137,26 @@
|
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
.state-inspector .si-clear {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
--pico-form-element-spacing-horizontal: 0.45rem;
|
|
140
|
+
height: 22px;
|
|
141
|
+
font-size: 11.5px;
|
|
134
142
|
text-transform: none;
|
|
135
143
|
letter-spacing: 0;
|
|
136
144
|
}
|
|
145
|
+
|
|
146
|
+
/* In a wide panel (below the configuration), the world, the agents and the parameters side by side. */
|
|
147
|
+
@container (min-width: 720px) {
|
|
148
|
+
.state-inspector {
|
|
149
|
+
display: grid;
|
|
150
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
|
|
151
|
+
align-items: start;
|
|
152
|
+
gap: 0 16px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.si-section + .si-section {
|
|
156
|
+
margin-top: 0;
|
|
157
|
+
padding-top: 4px;
|
|
158
|
+
padding-left: 16px;
|
|
159
|
+
border-top: none;
|
|
160
|
+
border-left: 1px solid var(--sim-border);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* component fields, public state, memory) and the parameters, as trees whose nodes expand. What
|
|
4
4
|
* is expanded is kept while stepping through the execution.
|
|
5
5
|
*/
|
|
6
|
-
import { For, Show, createSignal, type JSX } from 'solid-js';
|
|
6
|
+
import { For, Show, createEffect, createSignal, type JSX } from 'solid-js';
|
|
7
7
|
import type { InspectedState } from '@bramas/simulant-core/types/project';
|
|
8
8
|
import './StateInspector.css';
|
|
9
9
|
|
|
@@ -11,6 +11,8 @@ export interface StateInspectorProps {
|
|
|
11
11
|
state: InspectedState;
|
|
12
12
|
/** The agent selected on the canvas: shown expanded and highlighted. */
|
|
13
13
|
selected?: number | null;
|
|
14
|
+
/** Selects an agent (opening its row). */
|
|
15
|
+
onSelect?: (id: number) => void;
|
|
14
16
|
onClearSelection?: () => void;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -70,8 +72,37 @@ export function StateInspector(props: StateInspectorProps) {
|
|
|
70
72
|
</For>
|
|
71
73
|
);
|
|
72
74
|
|
|
75
|
+
// An agent selected elsewhere (on the canvas, in the algorithm) is brought into view, within
|
|
76
|
+
// the inspector's scrolling panel only: the page itself does not move.
|
|
77
|
+
let root: HTMLDivElement | undefined;
|
|
78
|
+
createEffect(() => {
|
|
79
|
+
const id = props.selected;
|
|
80
|
+
if (id === null || id === undefined) return;
|
|
81
|
+
queueMicrotask(() => {
|
|
82
|
+
const row = root?.querySelector(`[data-agent="${id}"]`);
|
|
83
|
+
const pane = root?.parentElement;
|
|
84
|
+
if (!row || !pane) return;
|
|
85
|
+
const r = row.getBoundingClientRect(), v = pane.getBoundingClientRect();
|
|
86
|
+
if (r.top < v.top) pane.scrollTop += r.top - v.top - 8;
|
|
87
|
+
else if (r.bottom > v.bottom) pane.scrollTop += Math.min(r.bottom - v.bottom + 8, r.top - v.top - 8);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** A row opens its agent and selects it; a second click folds it again. */
|
|
92
|
+
const clickAgent = (id: number, path: string, selected: boolean) => {
|
|
93
|
+
if (selected) {
|
|
94
|
+
setOpen(o => ({ ...o, [path]: false }));
|
|
95
|
+
props.onClearSelection?.();
|
|
96
|
+
} else if (isOpen(path, false)) {
|
|
97
|
+
toggle(path, false);
|
|
98
|
+
} else {
|
|
99
|
+
toggle(path, false);
|
|
100
|
+
props.onSelect?.(id);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
73
104
|
return (
|
|
74
|
-
<div class="state-inspector" data-state={JSON.stringify(props.state)}>
|
|
105
|
+
<div class="state-inspector" ref={root} data-state={JSON.stringify(props.state)}>
|
|
75
106
|
<div class="si-section">
|
|
76
107
|
<h4>World</h4>
|
|
77
108
|
<Fields value={props.state.world} path="world" />
|
|
@@ -81,7 +112,7 @@ export function StateInspector(props: StateInspectorProps) {
|
|
|
81
112
|
<h4>
|
|
82
113
|
Agents
|
|
83
114
|
<Show when={props.selected !== null && props.selected !== undefined}>
|
|
84
|
-
<button class="si-clear" onClick={() => props.onClearSelection?.()}>Clear selection</button>
|
|
115
|
+
<button class="sim-ghost sim-sm si-clear" onClick={() => props.onClearSelection?.()}>Clear selection</button>
|
|
85
116
|
</Show>
|
|
86
117
|
</h4>
|
|
87
118
|
<For each={props.state.agents}>
|
|
@@ -91,7 +122,7 @@ export function StateInspector(props: StateInspectorProps) {
|
|
|
91
122
|
const expanded = () => selected() || isOpen(path, false);
|
|
92
123
|
return (
|
|
93
124
|
<div class={`si-agent si-${agent.status}${selected() ? ' si-selected' : ''}`} data-agent={agent.id}>
|
|
94
|
-
<div class="si-row si-clickable" onClick={() =>
|
|
125
|
+
<div class="si-row si-clickable" onClick={() => clickAgent(agent.id, path, selected())}>
|
|
95
126
|
<span class="si-toggle">{expanded() ? '▾' : '▸'}</span>
|
|
96
127
|
<span class="si-swatch" style={{ 'background-color': agent.color }} />
|
|
97
128
|
<span class="si-agent-name">{agent.name}</span>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's theme, for what is drawn on canvases: renderers receive these colors (read from the
|
|
3
|
+
* stylesheet's tokens) so that drawings follow the light or dark theme.
|
|
4
|
+
*/
|
|
5
|
+
import { createSignal, onCleanup } from 'solid-js';
|
|
6
|
+
import type { RenderTheme } from '@bramas/simulant-core/types/project';
|
|
7
|
+
|
|
8
|
+
export type { RenderTheme } from '@bramas/simulant-core/types/project';
|
|
9
|
+
|
|
10
|
+
/** Whether the system prefers dark colors, following its changes. */
|
|
11
|
+
export function createSystemDark() {
|
|
12
|
+
const query = typeof window !== 'undefined' ? window.matchMedia?.('(prefers-color-scheme: dark)') : undefined;
|
|
13
|
+
const [dark, setDark] = createSignal(!!query?.matches);
|
|
14
|
+
const onChange = (e: MediaQueryListEvent) => setDark(e.matches);
|
|
15
|
+
query?.addEventListener?.('change', onChange);
|
|
16
|
+
onCleanup(() => query?.removeEventListener?.('change', onChange));
|
|
17
|
+
return dark;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The drawing colors under `element` (the stylesheet's tokens), for the given theme. */
|
|
21
|
+
export function renderTheme(element: Element | undefined, dark: boolean): RenderTheme {
|
|
22
|
+
const css = element ? getComputedStyle(element) : undefined;
|
|
23
|
+
const token = (name: string, light: string, darkValue: string) => css?.getPropertyValue(name).trim() || (dark ? darkValue : light);
|
|
24
|
+
return {
|
|
25
|
+
dark,
|
|
26
|
+
background: token('--sim-stage', '#ffffff', '#121214'),
|
|
27
|
+
foreground: token('--sim-text', '#18181b', '#ececee'),
|
|
28
|
+
muted: token('--sim-stage-muted', '#71717a', '#909098'),
|
|
29
|
+
line: token('--sim-stage-line', '#a1a1aa', '#4c4c53'),
|
|
30
|
+
accent: token('--sim-accent', '#5b62c9', '#8b90ec'),
|
|
31
|
+
danger: token('--sim-failure', '#d1242f', '#f85149'),
|
|
32
|
+
};
|
|
33
|
+
}
|