@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.
Files changed (38) hide show
  1. package/README.md +60 -0
  2. package/dist/SimulatorApp.jsx +782 -174
  3. package/dist/components/AlgorithmViewer.css +268 -74
  4. package/dist/components/AlgorithmViewer.jsx +171 -66
  5. package/dist/components/BranchPicker.css +74 -32
  6. package/dist/components/BranchPicker.jsx +9 -4
  7. package/dist/components/ConfigGraphGenerator.css +180 -38
  8. package/dist/components/ConfigGraphGenerator.jsx +155 -96
  9. package/dist/components/GenericEditor.jsx +18 -44
  10. package/dist/components/Icon.jsx +54 -0
  11. package/dist/components/ModelView.css +15 -15
  12. package/dist/components/SettingsLibrary.jsx +7 -5
  13. package/dist/components/Splitter.jsx +28 -0
  14. package/dist/components/StateInspector.css +59 -33
  15. package/dist/components/StateInspector.jsx +37 -4
  16. package/dist/components/theme.js +28 -0
  17. package/dist/pico.min.css +4 -4
  18. package/dist/renderers/ring.js +19 -10
  19. package/dist/simulator.css +1553 -363
  20. package/package.json +6 -6
  21. package/src/SimulatorApp.tsx +888 -219
  22. package/src/components/AlgorithmViewer.css +268 -74
  23. package/src/components/AlgorithmViewer.tsx +188 -75
  24. package/src/components/BranchPicker.css +74 -32
  25. package/src/components/BranchPicker.tsx +9 -4
  26. package/src/components/ConfigGraphGenerator.css +180 -38
  27. package/src/components/ConfigGraphGenerator.tsx +172 -114
  28. package/src/components/GenericEditor.tsx +25 -59
  29. package/src/components/Icon.tsx +76 -0
  30. package/src/components/ModelView.css +15 -15
  31. package/src/components/SettingsLibrary.tsx +7 -5
  32. package/src/components/Splitter.tsx +55 -0
  33. package/src/components/StateInspector.css +59 -33
  34. package/src/components/StateInspector.tsx +35 -4
  35. package/src/components/theme.ts +33 -0
  36. package/src/pico.min.css +4 -4
  37. package/src/renderers/ring.ts +22 -12
  38. package/src/simulator.css +1553 -363
@@ -96,53 +96,28 @@ function CodeEditor(props) {
96
96
  }
97
97
  }
98
98
  };
99
- return (<div style={{
100
- display: 'grid',
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} style={{
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.label}:{' '}
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.label}:{' '}
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
- {' '}{props.field.label}
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
- {props.field.label}:{' '}
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). The blocks are not <article>s: Pico gives an
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.25rem;
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: 0.8rem;
97
- font-weight: 600;
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.75rem;
251
+ top: 0;
252
252
  display: flex;
253
253
  flex-direction: column;
254
254
  gap: 0.6rem;
255
- max-height: calc(100vh - 1.5rem);
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.1rem 0.5rem;
276
- border-radius: 999px;
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(--pico-font-family, system-ui, sans-serif);
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(--pico-font-family, system-ui, sans-serif);
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(--pico-font-family, system-ui, sans-serif);
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
- font-size: 0.75rem !important;
509
- --pico-form-element-spacing-vertical: 0.05rem;
510
- --pico-form-element-spacing-horizontal: 0.5rem;
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-ghost" onClick={() => props.onLoad(p.s)}>Load</button>
37
- <button class="sim-ghost sim-icon" title="Copy a link to these settings" onClick={() => props.onShare(p.s)}>Share</button>
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)}>Delete</button>
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-muted">Nothing saved yet: save the current settings and path below.</p>}>
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()}>Save</button>
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: 12.5px;
8
- background-color: var(--sim-surface);
6
+ font-size: 12px;
9
7
  color: var(--sim-text);
10
- border: 1px solid var(--sim-border);
11
- border-radius: var(--sim-radius);
12
- padding: 0.6rem 0.85rem;
8
+ }
9
+
10
+ .si-section {
11
+ padding: 4px 0;
13
12
  }
14
13
 
15
14
  .si-section + .si-section {
16
- margin-top: 0.5rem;
17
- padding-top: 0.5rem;
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 0.25rem;
23
- font-size: 0.8rem;
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.04em;
27
+ letter-spacing: 0.06em;
27
28
  color: var(--sim-muted);
28
- font-family: var(--pico-font-family, system-ui, sans-serif);
29
+ font-family: var(--sim-font);
29
30
  display: flex;
30
31
  align-items: center;
31
- gap: 0.75rem;
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: 0.4rem;
43
- line-height: 1.55;
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: 4px;
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-muted);
66
+ color: var(--sim-faint);
67
+ font-size: 10px;
63
68
  }
64
69
 
65
70
  .si-children {
66
- margin-left: 1.1em;
67
- border-left: 1px dotted var(--sim-border);
68
- padding-left: 0.4em;
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-muted);
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: 4px;
95
- border-left: 3px solid transparent;
96
- padding-left: 0.2rem;
99
+ border-radius: 6px;
100
+ scroll-margin: 8px;
97
101
  }
98
102
 
99
103
  .si-agent.si-selected {
100
- border-left-color: var(--sim-highlight);
101
- background-color: var(--sim-highlight-soft);
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: 10px;
116
- height: 10px;
117
- border-radius: 2px;
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
- font-size: 0.72rem;
132
- --pico-form-element-spacing-vertical: 0;
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
- return (<div class="state-inspector" data-state={JSON.stringify(props.state)}>
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={() => toggle(path, false)}>
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
+ }