@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
|
@@ -96,53 +96,28 @@ function CodeEditor(props) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
|
-
return (<div
|
|
100
|
-
|
|
101
|
-
'grid-template-columns': '3.5em minmax(0, 1fr)',
|
|
102
|
-
border: '1px solid #c9d2dc',
|
|
103
|
-
'border-radius': '6px',
|
|
104
|
-
overflow: 'hidden',
|
|
105
|
-
'background-color': '#f8fafc'
|
|
106
|
-
}}>
|
|
107
|
-
<div ref={gutterRef} aria-hidden="true" style={{
|
|
108
|
-
'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
|
|
109
|
-
'font-size': '0.9em',
|
|
110
|
-
'line-height': '1.5',
|
|
111
|
-
padding: '0.75em 0.5em',
|
|
112
|
-
'text-align': 'right',
|
|
113
|
-
color: '#6b7280',
|
|
114
|
-
'background-color': '#eef2f7',
|
|
115
|
-
'border-right': '1px solid #d7dee7',
|
|
116
|
-
overflow: 'hidden',
|
|
117
|
-
'user-select': 'none'
|
|
118
|
-
}}>
|
|
99
|
+
return (<div class="sim-code-editor">
|
|
100
|
+
<div ref={gutterRef} class="sim-code-gutter" aria-hidden="true">
|
|
119
101
|
<For each={lineNumbers()}>
|
|
120
102
|
{(lineNumber) => <div>{lineNumber}</div>}
|
|
121
103
|
</For>
|
|
122
104
|
</div>
|
|
123
|
-
<textarea ref={textareaRef} value={props.value} onInput={(event) => props.onInput(event.currentTarget.value)} onKeyDown={handleKeyDown} onScroll={syncScroll} placeholder={props.placeholder} rows={props.rows ?? 12} spellcheck={false}
|
|
124
|
-
width: '100%',
|
|
125
|
-
margin: '0',
|
|
126
|
-
border: 'none',
|
|
127
|
-
outline: 'none',
|
|
128
|
-
resize: 'vertical',
|
|
129
|
-
padding: '0.75em',
|
|
130
|
-
'background-color': '#fcfdff',
|
|
131
|
-
color: '#17202a',
|
|
132
|
-
'font-family': 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
|
|
133
|
-
'font-size': '0.9em',
|
|
134
|
-
'line-height': '1.5',
|
|
135
|
-
'white-space': 'pre',
|
|
136
|
-
'tab-size': '2'
|
|
137
|
-
}}/>
|
|
105
|
+
<textarea ref={textareaRef} value={props.value} onInput={(event) => props.onInput(event.currentTarget.value)} onKeyDown={handleKeyDown} onScroll={syncScroll} placeholder={props.placeholder} rows={props.rows ?? 12} spellcheck={false}/>
|
|
138
106
|
</div>);
|
|
139
107
|
}
|
|
108
|
+
/** A field's label, with its placeholder as a hint. */
|
|
109
|
+
function FieldLabel(props) {
|
|
110
|
+
const hint = () => 'placeholder' in props.field ? props.field.placeholder : undefined;
|
|
111
|
+
return (<span class="sim-field-label">
|
|
112
|
+
{props.field.label}
|
|
113
|
+
{hint() && <span class="sim-field-hint">{hint()}</span>}
|
|
114
|
+
</span>);
|
|
115
|
+
}
|
|
140
116
|
function renderTextArea(field, fieldStore, setFieldStore) {
|
|
141
117
|
const isCodeEditor = field.type === 'code';
|
|
142
118
|
return (<div class="sim-field sim-field-wide">
|
|
143
119
|
<label>
|
|
144
|
-
{field
|
|
145
|
-
{'placeholder' in field && field.placeholder && (<span class="sim-muted">({field.placeholder})</span>)}
|
|
120
|
+
<FieldLabel field={field}/>
|
|
146
121
|
{isCodeEditor ? (<CodeEditor value={fieldStore[field.key] ?? ''} onInput={(value) => setFieldStore(field.key, value)} placeholder={'placeholder' in field ? field.placeholder : undefined} rows={'rows' in field && field.rows ? field.rows : 12}/>) : (<textarea value={fieldStore[field.key] ?? ''} onInput={(e) => setFieldStore(field.key, e.currentTarget.value)} placeholder={'placeholder' in field ? field.placeholder : undefined} rows={'rows' in field && field.rows ? field.rows : 4} spellcheck={field.type === 'textarea' ? field.spellcheck : false}/>)}
|
|
147
122
|
</label>
|
|
148
123
|
</div>);
|
|
@@ -152,7 +127,7 @@ function RenderField(props) {
|
|
|
152
127
|
<Match when={props.field.type === 'number' && props.field}>
|
|
153
128
|
{(f) => (<div class="sim-field">
|
|
154
129
|
<label>
|
|
155
|
-
{f().label}
|
|
130
|
+
<span class="sim-field-label">{f().label}</span>
|
|
156
131
|
<input type="number" value={props.fieldStore[props.field.key]} onInput={(e) => {
|
|
157
132
|
// An empty or partial number is not a value yet: wait for the next keystroke.
|
|
158
133
|
const n = e.currentTarget.valueAsNumber;
|
|
@@ -166,8 +141,7 @@ function RenderField(props) {
|
|
|
166
141
|
<Match when={props.field.type === 'text' && props.field && !('multiline' in props.field && props.field.multiline)}>
|
|
167
142
|
<div class="sim-field sim-field-wide">
|
|
168
143
|
<label>
|
|
169
|
-
{props.field
|
|
170
|
-
{'placeholder' in props.field && props.field.placeholder && (<span class="sim-muted">({props.field.placeholder})</span>)}
|
|
144
|
+
<FieldLabel field={props.field}/>
|
|
171
145
|
<input type="text" value={props.fieldStore[props.field.key] ?? ''} onInput={(e) => props.setFieldStore(props.field.key, e.currentTarget.value)} placeholder={'placeholder' in props.field ? props.field.placeholder : undefined} pattern={'pattern' in props.field ? props.field.pattern : undefined} spellcheck={false}/>
|
|
172
146
|
</label>
|
|
173
147
|
</div>
|
|
@@ -176,12 +150,12 @@ function RenderField(props) {
|
|
|
176
150
|
<Match when={props.field.type === 'code' || props.field.type === 'textarea' || (props.field.type === 'text' && 'multiline' in props.field && props.field.multiline)}>
|
|
177
151
|
{renderTextArea(props.field, props.fieldStore, props.setFieldStore)}
|
|
178
152
|
</Match>
|
|
179
|
-
|
|
153
|
+
|
|
180
154
|
<Match when={props.field.type === 'boolean' && props.field}>
|
|
181
|
-
{(f) => (<div class="sim-field">
|
|
155
|
+
{(f) => (<div class="sim-field sim-field-check">
|
|
182
156
|
<label>
|
|
183
157
|
<input type="checkbox" checked={props.fieldStore[props.field.key]} onInput={(e) => props.setFieldStore(props.field.key, e.currentTarget.checked)}/>
|
|
184
|
-
{
|
|
158
|
+
{f().label}
|
|
185
159
|
</label>
|
|
186
160
|
</div>)}
|
|
187
161
|
</Match>
|
|
@@ -189,7 +163,7 @@ function RenderField(props) {
|
|
|
189
163
|
<Match when={props.field.type === 'select' && props.field}>
|
|
190
164
|
{(f) => (<div class="sim-field">
|
|
191
165
|
<label>
|
|
192
|
-
{
|
|
166
|
+
<span class="sim-field-label">{f().label}</span>
|
|
193
167
|
<select value={props.fieldStore[props.field.key]} onInput={(e) => props.setFieldStore(props.field.key, typeof props.field.defaultValue === 'number'
|
|
194
168
|
? parseFloat(e.currentTarget.value)
|
|
195
169
|
: e.currentTarget.value)}>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const PATHS = {
|
|
2
|
+
first: () => <><path d="M18 19 9 12l9-7z"/><path d="M6 5v14"/></>,
|
|
3
|
+
last: () => <><path d="m6 5 9 7-9 7z"/><path d="M18 5v14"/></>,
|
|
4
|
+
previous: () => <path d="m15 18-6-6 6-6"/>,
|
|
5
|
+
next: () => <path d="m9 18 6-6-6-6"/>,
|
|
6
|
+
play: () => <path d="M7 4.5v15l12-7.5z"/>,
|
|
7
|
+
pause: () => <><path d="M8 5v14"/><path d="M16 5v14"/></>,
|
|
8
|
+
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"/></>,
|
|
9
|
+
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"/></>,
|
|
10
|
+
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"/>,
|
|
11
|
+
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"/></>,
|
|
12
|
+
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"/></>,
|
|
13
|
+
close: () => <path d="M6 6l12 12M18 6 6 18"/>,
|
|
14
|
+
trash: () => <><path d="M4.5 7h15"/><path d="M9.5 7V4.8h5V7"/><path d="M6.5 7l.9 12.2h9.2L17.5 7"/></>,
|
|
15
|
+
download: () => <><path d="M12 4v11"/><path d="m7.5 10.5 4.5 4.5 4.5-4.5"/><path d="M5 19.5h14"/></>,
|
|
16
|
+
reset: () => <><path d="M4 12a8 8 0 1 0 2.5-5.8L4 8.5"/><path d="M4 4v4.5h4.5"/></>,
|
|
17
|
+
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"/></>,
|
|
18
|
+
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"/></>,
|
|
19
|
+
sliders: () => <><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></>,
|
|
20
|
+
bookmark: () => <path d="M7 3.5h10a1 1 0 0 1 1 1v16l-6-4-6 4v-16a1 1 0 0 1 1-1z"/>,
|
|
21
|
+
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"/></>,
|
|
22
|
+
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"/></>,
|
|
23
|
+
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"/></>,
|
|
24
|
+
chevronDown: () => <path d="m6 9 6 6 6-6"/>,
|
|
25
|
+
chevronRight: () => <path d="m9 6 6 6-6 6"/>,
|
|
26
|
+
check: () => <path d="m5 12.5 4.5 4.5L19 7.5"/>,
|
|
27
|
+
alert: () => <><path d="M12 3.5 2.5 20h19z"/><path d="M12 10v4.5M12 17.2h.01"/></>,
|
|
28
|
+
info: () => <><circle cx="12" cy="12" r="8.5"/><path d="M12 11v5.5M12 7.8h.01"/></>,
|
|
29
|
+
minus: () => <path d="M5 12h14"/>,
|
|
30
|
+
plus: () => <path d="M12 5v14M5 12h14"/>,
|
|
31
|
+
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"/></>,
|
|
32
|
+
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"/></>,
|
|
33
|
+
stop: () => <rect x="6" y="6" width="12" height="12" rx="2"/>,
|
|
34
|
+
dock: () => <><rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M14 4.5v15"/></>,
|
|
35
|
+
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"/></>,
|
|
36
|
+
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"/></>,
|
|
37
|
+
dockBottom: () => <><rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M3.5 14h17"/><path d="M7 17h4"/></>,
|
|
38
|
+
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"/></>,
|
|
39
|
+
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"/></>,
|
|
40
|
+
};
|
|
41
|
+
export function Icon(props) {
|
|
42
|
+
return (<svg class={`sim-svg${props.class ? ` ${props.class}` : ''}`} width={props.size ?? 16} height={props.size ?? 16} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
43
|
+
{PATHS[props.name]()}
|
|
44
|
+
</svg>);
|
|
45
|
+
}
|
|
46
|
+
/** The Simulant mark: agents on a ring. */
|
|
47
|
+
export function Logo() {
|
|
48
|
+
return (<svg class="sim-logo" width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
|
|
49
|
+
<circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2" opacity="0.35"/>
|
|
50
|
+
<circle cx="12" cy="4" r="3" fill="var(--sim-accent)"/>
|
|
51
|
+
<circle cx="18.9" cy="16" r="2.2" fill="currentColor"/>
|
|
52
|
+
<circle cx="5.1" cy="16" r="2.2" fill="currentColor" opacity="0.6"/>
|
|
53
|
+
</svg>);
|
|
54
|
+
}
|
|
@@ -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 {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* shared as a link, and saved settings can be deleted. The current settings are saved by name.
|
|
5
5
|
*/
|
|
6
6
|
import { For, Show, createSignal } from "solid-js";
|
|
7
|
+
import { Icon } from "./Icon.jsx";
|
|
7
8
|
/** `N=10 · site=3`; long values are shortened. */
|
|
8
9
|
function summary(settings) {
|
|
9
10
|
if (!settings || typeof settings !== 'object')
|
|
@@ -28,15 +29,16 @@ export function SettingsLibrary(props) {
|
|
|
28
29
|
<div class="sim-entry-text">
|
|
29
30
|
<span class="sim-entry-name">{p.s.name}</span>
|
|
30
31
|
<span class="sim-muted">
|
|
32
|
+
<Show when={same(p.s.settings, props.current)}><span class="sim-entry-badge">current · </span></Show>
|
|
31
33
|
{summary(p.s.settings)}
|
|
32
34
|
<Show when={p.s.path && p.s.path.length > 1}> · {p.s.path.length - 1} steps</Show>
|
|
33
35
|
</span>
|
|
34
36
|
</div>
|
|
35
37
|
<div class="sim-entry-actions">
|
|
36
|
-
<button class="sim-
|
|
37
|
-
<button class="sim-ghost sim-icon" title="Copy a link to these settings" onClick={() => props.onShare(p.s)}
|
|
38
|
+
<button class="sim-btn sim-sm" onClick={() => props.onLoad(p.s)}>Load</button>
|
|
39
|
+
<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>
|
|
38
40
|
<Show when={p.deletable}>
|
|
39
|
-
<button class="sim-ghost sim-icon sim-danger" title="Delete" onClick={() => props.onDelete(p.s.name)}
|
|
41
|
+
<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>
|
|
40
42
|
</Show>
|
|
41
43
|
</div>
|
|
42
44
|
</li>);
|
|
@@ -48,14 +50,14 @@ export function SettingsLibrary(props) {
|
|
|
48
50
|
</ul>
|
|
49
51
|
</Show>
|
|
50
52
|
<h4>Saved in this browser</h4>
|
|
51
|
-
<Show when={props.saved.length > 0} fallback={<p class="sim-
|
|
53
|
+
<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>}>
|
|
52
54
|
<ul class="sim-entries">
|
|
53
55
|
<For each={props.saved}>{(s) => <Entry s={s} deletable={true}/>}</For>
|
|
54
56
|
</ul>
|
|
55
57
|
</Show>
|
|
56
58
|
<form class="sim-save" onSubmit={save}>
|
|
57
59
|
<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"/>
|
|
58
|
-
<button type="submit" disabled={!name().trim()}
|
|
60
|
+
<button type="submit" class="sim-primary" disabled={!name().trim()}><Icon name="bookmark" size={14}/> Save</button>
|
|
59
61
|
</form>
|
|
60
62
|
</div>);
|
|
61
63
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
export function Splitter(props) {
|
|
8
|
+
let start;
|
|
9
|
+
const [dragging, setDragging] = createSignal(false);
|
|
10
|
+
const at = (e) => props.orientation === 'vertical' ? e.clientX : e.clientY;
|
|
11
|
+
const cursor = () => props.orientation === 'vertical' ? 'sim-resizing' : 'sim-resizing-rows';
|
|
12
|
+
const end = () => {
|
|
13
|
+
if (!start)
|
|
14
|
+
return;
|
|
15
|
+
start = undefined;
|
|
16
|
+
setDragging(false);
|
|
17
|
+
document.documentElement.classList.remove(cursor());
|
|
18
|
+
props.onCommit?.();
|
|
19
|
+
};
|
|
20
|
+
return (<div class={`sim-splitter sim-splitter-${props.orientation}${dragging() ? ' sim-dragging' : ''}`} role="separator" aria-orientation={props.orientation} aria-label={props.label} title="Drag to resize; double-click to reset" onPointerDown={e => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
23
|
+
start = { at: at(e), value: props.value };
|
|
24
|
+
setDragging(true);
|
|
25
|
+
document.documentElement.classList.add(cursor());
|
|
26
|
+
}} onPointerMove={e => { if (start)
|
|
27
|
+
props.onChange(start.value + (props.direction ?? 1) * (at(e) - start.at)); }} onPointerUp={end} onPointerCancel={end} onDblClick={() => props.onReset?.()}/>);
|
|
28
|
+
}
|
|
@@ -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 } from "solid-js";
|
|
6
|
+
import { For, Show, createEffect, createSignal } from "solid-js";
|
|
7
7
|
import "./StateInspector.css";
|
|
8
8
|
const isTree = (v) => typeof v === 'object' && v !== null && (Array.isArray(v) ? v.length > 0 : Object.keys(v).length > 0);
|
|
9
9
|
/** A short one-line form of a value. */
|
|
@@ -52,7 +52,40 @@ export function StateInspector(props) {
|
|
|
52
52
|
const Fields = (p) => (<For each={Object.entries(p.value)} fallback={<div class="si-empty">nothing</div>}>
|
|
53
53
|
{([k, v]) => <Node name={k} value={v} path={`${p.path}/${k}`} depth={1}/>}
|
|
54
54
|
</For>);
|
|
55
|
-
|
|
55
|
+
// An agent selected elsewhere (on the canvas, in the algorithm) is brought into view, within
|
|
56
|
+
// the inspector's scrolling panel only: the page itself does not move.
|
|
57
|
+
let root;
|
|
58
|
+
createEffect(() => {
|
|
59
|
+
const id = props.selected;
|
|
60
|
+
if (id === null || id === undefined)
|
|
61
|
+
return;
|
|
62
|
+
queueMicrotask(() => {
|
|
63
|
+
const row = root?.querySelector(`[data-agent="${id}"]`);
|
|
64
|
+
const pane = root?.parentElement;
|
|
65
|
+
if (!row || !pane)
|
|
66
|
+
return;
|
|
67
|
+
const r = row.getBoundingClientRect(), v = pane.getBoundingClientRect();
|
|
68
|
+
if (r.top < v.top)
|
|
69
|
+
pane.scrollTop += r.top - v.top - 8;
|
|
70
|
+
else if (r.bottom > v.bottom)
|
|
71
|
+
pane.scrollTop += Math.min(r.bottom - v.bottom + 8, r.top - v.top - 8);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
/** A row opens its agent and selects it; a second click folds it again. */
|
|
75
|
+
const clickAgent = (id, path, selected) => {
|
|
76
|
+
if (selected) {
|
|
77
|
+
setOpen(o => ({ ...o, [path]: false }));
|
|
78
|
+
props.onClearSelection?.();
|
|
79
|
+
}
|
|
80
|
+
else if (isOpen(path, false)) {
|
|
81
|
+
toggle(path, false);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
toggle(path, false);
|
|
85
|
+
props.onSelect?.(id);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return (<div class="state-inspector" ref={root} data-state={JSON.stringify(props.state)}>
|
|
56
89
|
<div class="si-section">
|
|
57
90
|
<h4>World</h4>
|
|
58
91
|
<Fields value={props.state.world} path="world"/>
|
|
@@ -62,7 +95,7 @@ export function StateInspector(props) {
|
|
|
62
95
|
<h4>
|
|
63
96
|
Agents
|
|
64
97
|
<Show when={props.selected !== null && props.selected !== undefined}>
|
|
65
|
-
<button class="si-clear" onClick={() => props.onClearSelection?.()}>Clear selection</button>
|
|
98
|
+
<button class="sim-ghost sim-sm si-clear" onClick={() => props.onClearSelection?.()}>Clear selection</button>
|
|
66
99
|
</Show>
|
|
67
100
|
</h4>
|
|
68
101
|
<For each={props.state.agents}>
|
|
@@ -71,7 +104,7 @@ export function StateInspector(props) {
|
|
|
71
104
|
const selected = () => props.selected === agent.id;
|
|
72
105
|
const expanded = () => selected() || isOpen(path, false);
|
|
73
106
|
return (<div class={`si-agent si-${agent.status}${selected() ? ' si-selected' : ''}`} data-agent={agent.id}>
|
|
74
|
-
<div class="si-row si-clickable" onClick={() =>
|
|
107
|
+
<div class="si-row si-clickable" onClick={() => clickAgent(agent.id, path, selected())}>
|
|
75
108
|
<span class="si-toggle">{expanded() ? '▾' : '▸'}</span>
|
|
76
109
|
<span class="si-swatch" style={{ 'background-color': agent.color }}/>
|
|
77
110
|
<span class="si-agent-name">{agent.name}</span>
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
/** Whether the system prefers dark colors, following its changes. */
|
|
7
|
+
export function createSystemDark() {
|
|
8
|
+
const query = typeof window !== 'undefined' ? window.matchMedia?.('(prefers-color-scheme: dark)') : undefined;
|
|
9
|
+
const [dark, setDark] = createSignal(!!query?.matches);
|
|
10
|
+
const onChange = (e) => setDark(e.matches);
|
|
11
|
+
query?.addEventListener?.('change', onChange);
|
|
12
|
+
onCleanup(() => query?.removeEventListener?.('change', onChange));
|
|
13
|
+
return dark;
|
|
14
|
+
}
|
|
15
|
+
/** The drawing colors under `element` (the stylesheet's tokens), for the given theme. */
|
|
16
|
+
export function renderTheme(element, dark) {
|
|
17
|
+
const css = element ? getComputedStyle(element) : undefined;
|
|
18
|
+
const token = (name, light, darkValue) => css?.getPropertyValue(name).trim() || (dark ? darkValue : light);
|
|
19
|
+
return {
|
|
20
|
+
dark,
|
|
21
|
+
background: token('--sim-stage', '#ffffff', '#121214'),
|
|
22
|
+
foreground: token('--sim-text', '#18181b', '#ececee'),
|
|
23
|
+
muted: token('--sim-stage-muted', '#71717a', '#909098'),
|
|
24
|
+
line: token('--sim-stage-line', '#a1a1aa', '#4c4c53'),
|
|
25
|
+
accent: token('--sim-accent', '#5b62c9', '#8b90ec'),
|
|
26
|
+
danger: token('--sim-failure', '#d1242f', '#f85149'),
|
|
27
|
+
};
|
|
28
|
+
}
|