@mantaq/viz 0.2.1 → 0.3.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 +5 -4
- package/dist/{embed-QX7vRXoL.mjs → embed-Dpjn47Qx.mjs} +259 -71
- package/dist/{embed-CISoaFIK.d.mts → embed-OmMnILen.d.mts} +1 -3
- package/dist/page.d.mts +1 -1
- package/dist/page.mjs +1 -1
- package/dist/standalone.mjs +1 -1
- package/dist/style.css +52 -19
- package/dist/viewer.d.mts +2 -2
- package/dist/viewer.mjs +1 -1
- package/package.json +2 -2
- package/src/viewer/viewer.css +52 -19
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Draws a Mantaq machine in the browser and lets you walk it. Input is a viz docum
|
|
|
4
4
|
|
|
5
5
|
- `@mantaq/viz`: the document. Types, `parseViz` / `validateViz` (untrusted JSON → `Either<VizIssue[], VizDocument>`, every issue with its path), `serializeViz`, and the walk (`movesFrom`, `decodePath`, …). No DOM.
|
|
6
6
|
- `@mantaq/viz/viewer`: `mountViewer(root, document)` / `mountVizText(root, { source, text })` to embed it in a page, plus `@mantaq/viz/viewer.css`.
|
|
7
|
-
- `@mantaq/viz/standalone`, `@mantaq/viz/live`: whole-page entries used by
|
|
7
|
+
- `@mantaq/viz/standalone`, `@mantaq/viz/live`: whole-page entries used by the Vite plugin (live page).
|
|
8
8
|
|
|
9
9
|
Dev page with the bundled examples: `vp run viz:export`, then `cd packages/viz && vp dev`. It loads `examples/examples.json`, `?src=<url>`, or a picked file.
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@ The viewer keeps a cursor on one state, starting at the initial one. The panel l
|
|
|
18
18
|
|
|
19
19
|
Take a step with a panel button, by clicking a pulsing edge, or by clicking a state that exactly one next step reaches. An undetermined edge (`to: null`) offers each target its AST explanation names. Without one, it cannot be taken. A final state ends the walk. **Back** (or ←), **Reset** and the path list undo steps. The standalone page keeps the walk in the URL: `?step=` holds one edge id per step, plus `~target` for an undetermined edge.
|
|
20
20
|
|
|
21
|
-
**Static walk** (standalone
|
|
21
|
+
**Static walk** (standalone page): nothing runs. The walk follows declarations, so guards are not evaluated, and a path may not happen at runtime. The logic is in `src/moves.ts`: `movesFrom`, `unhandledSends`, `decodePath`.
|
|
22
22
|
|
|
23
23
|
**Live** (the Vite plugin page, or `mountViewer(root, doc, { driver })`): steps run the real actor through `createDriver` from `@mantaq/inspect/drive`, so guards are evaluated. Timers advance by button, and **Back** replays the path from a fresh actor.
|
|
24
24
|
|
|
@@ -35,7 +35,7 @@ Take a step with a panel button, by clicking a pulsing edge, or by clicking a st
|
|
|
35
35
|
| `edge.to` | `null` = **undetermined**: only for actors that carry no `to` declarations |
|
|
36
36
|
| `edge.origin` | `declared` (builder `to`, vouched for by the type checker) · `probe` (synthetic traversal, undeclared actors) · `fuzz` (observed only while fuzzing) |
|
|
37
37
|
| `edge.explanation` | `null` (no handler: initial/effect, or AST not run) · `{ status: "analyzed", location, registration, branches, payloadReads, contextReads, contextWrites, readsActorState, comparisons, literals }` · `{ status: "not_analyzed", location, reason, snippet }` |
|
|
38
|
-
| `edge.doc` | leading JSDoc of the `m.on` / `m.onAny` call behind the edge; `null` = undocumented, no handler, or AST not run
|
|
38
|
+
| `edge.doc` | leading JSDoc of the `m.on` / `m.onAny` call behind the edge, or of the `m.effect` calls of the state for an `effect` edge (joined in order); `null` = undocumented, no handler, or AST not run |
|
|
39
39
|
| `machine.fuzz` | `null` or `{ seed, iterations, corpus: [{ steps, discovered }], errors: [{ from, event, message, steps }] }`; every sequence is shrunk and replayable from a fresh actor |
|
|
40
40
|
|
|
41
41
|
Bump `VIZ_SCHEMA_VERSION` on any breaking change. The validator accepts only the current version.
|
|
@@ -44,5 +44,6 @@ Bump `VIZ_SCHEMA_VERSION` on any breaking change. The validator accepts only the
|
|
|
44
44
|
|
|
45
45
|
The viewer's "Coverage from" picker dims elements with zero hits. What dimmed means depends on the source:
|
|
46
46
|
|
|
47
|
-
- **
|
|
47
|
+
- **examples + fuzz** (`reached`): neither an example nor fuzzing reached this. The default when both sources are present, and the same merge `expectReached` uses. Fix a miss by adding an example to `tests`.
|
|
48
|
+
- **example tests** (`tested`): no test exercises this. It is the classic "what is untested" view. Supply the event sequences your tests send (`inspect({ tests })`).
|
|
48
49
|
- **fuzz runs** (`coverage`): no fuzz run produced this. Fuzzing reaches almost everything, so little is dimmed. What does stay dimmed is usually a **declared but unreached edge**: the handler lists the target in `to`, but its guards may never allow it. Saga's `compensatingRefund -REFUND_DONE-> failed` is an example. The edge panel calls these out. Fuzzing is not proof, so check the handler.
|
|
@@ -45,42 +45,42 @@ function items(machine) {
|
|
|
45
45
|
/** Modes this machine has data for, best first: tests answer "what is untested?". */
|
|
46
46
|
function availableModes(machine) {
|
|
47
47
|
const modes = [];
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const tested = items(machine).some((item) => item.tested !== null);
|
|
49
|
+
const run = items(machine).some((item) => item.coverage !== null);
|
|
50
|
+
if (tested && run) modes.push("reached");
|
|
51
|
+
if (tested) modes.push("tests");
|
|
52
|
+
if (run) modes.push("run");
|
|
50
53
|
return modes;
|
|
51
54
|
}
|
|
52
55
|
function measure(mode, item) {
|
|
53
56
|
if (mode === "run") return item.coverage;
|
|
54
57
|
if (mode === "tests") return item.tested;
|
|
58
|
+
if (mode === "reached") {
|
|
59
|
+
if (item.coverage === null && item.tested === null) return null;
|
|
60
|
+
return { hits: (item.coverage?.hits ?? 0) + (item.tested?.hits ?? 0) };
|
|
61
|
+
}
|
|
55
62
|
return null;
|
|
56
63
|
}
|
|
57
64
|
function modeLabel(mode, machine) {
|
|
58
65
|
if (mode === "tests") return "example tests";
|
|
66
|
+
if (mode === "reached") return "examples + fuzz";
|
|
59
67
|
if (mode === "run") return machine.fuzz === null ? "supplied run" : "fuzz runs";
|
|
60
68
|
return "off";
|
|
61
69
|
}
|
|
62
|
-
/** Legend text for a dimmed element: what "unreached" means for this source. */
|
|
63
|
-
function unreachedText(mode, machine) {
|
|
64
|
-
if (mode === "tests") return "dimmed and dashed: no test exercises this";
|
|
65
|
-
if (machine.fuzz === null) return "dimmed and dashed: never taken in the supplied run";
|
|
66
|
-
return "dimmed and dashed: never observed while fuzzing (declared edges here may be unreachable)";
|
|
67
|
-
}
|
|
68
70
|
// src/viewer/labels.ts
|
|
69
|
-
/**
|
|
70
|
-
function
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
/** Text drawn on an edge: the event, or for an effect loop the effects it runs. */
|
|
74
|
-
function edgeText(edge, machine) {
|
|
75
|
-
if (edge.kind !== "effect") return edge.event;
|
|
76
|
-
const names = effectNames(edge, machine);
|
|
77
|
-
return names.length === 0 ? "effect" : `runs ${names.join(", ")}`;
|
|
71
|
+
/** Text drawn on an edge: the event it carries. */
|
|
72
|
+
function edgeText(edge) {
|
|
73
|
+
return edge.event;
|
|
78
74
|
}
|
|
79
75
|
// src/viewer/layout.ts
|
|
80
76
|
const CHAR_WIDTH = 7;
|
|
81
77
|
const NODE_HEIGHT = 36;
|
|
78
|
+
const CHIP_ROW_HEIGHT = 20;
|
|
82
79
|
const LABEL_HEIGHT = 16;
|
|
83
80
|
const UNKNOWN_SIZE = 28;
|
|
81
|
+
const MIN_DOT_GAP = 3;
|
|
82
|
+
const MAX_DOT_GAP = 12;
|
|
83
|
+
const DOT_GAP_PER_LENGTH = 1 / 24;
|
|
84
84
|
function anyIds(machine) {
|
|
85
85
|
return [...new Set(machine.edges.map((edge) => edge.from).filter(isAnyState))].sort();
|
|
86
86
|
}
|
|
@@ -90,8 +90,13 @@ function unknownNodeId(edge) {
|
|
|
90
90
|
function regionNodeId(region) {
|
|
91
91
|
return `region:${region}`;
|
|
92
92
|
}
|
|
93
|
+
const HINT_WIDTH = 14;
|
|
93
94
|
function stateWidth(state) {
|
|
94
|
-
|
|
95
|
+
const chips = state.effects.map((effect) => effect.name.length * CHAR_WIDTH + 24 + (effect.sends === null ? HINT_WIDTH : 0));
|
|
96
|
+
return Math.max(80, state.label.length * CHAR_WIDTH + 24, ...chips);
|
|
97
|
+
}
|
|
98
|
+
function stateHeight(state) {
|
|
99
|
+
return NODE_HEIGHT + state.effects.length * CHIP_ROW_HEIGHT;
|
|
95
100
|
}
|
|
96
101
|
function parentRegion(region) {
|
|
97
102
|
const cut = region.lastIndexOf(".");
|
|
@@ -111,7 +116,7 @@ function addStates(graph, machine) {
|
|
|
111
116
|
for (const state of machine.states) {
|
|
112
117
|
graph.setNode(state.id, {
|
|
113
118
|
width: stateWidth(state),
|
|
114
|
-
height:
|
|
119
|
+
height: stateHeight(state)
|
|
115
120
|
});
|
|
116
121
|
if (state.region !== null) graph.setParent(state.id, regionNodeId(state.region));
|
|
117
122
|
}
|
|
@@ -128,7 +133,7 @@ function addAnyNodes(graph, machine) {
|
|
|
128
133
|
}
|
|
129
134
|
function addEdges(graph, machine) {
|
|
130
135
|
for (const edge of machine.edges) {
|
|
131
|
-
if (edge.kind === "initial" || edge.to === edge.from) continue;
|
|
136
|
+
if (edge.kind === "initial" || edge.kind === "effect" || edge.to === edge.from) continue;
|
|
132
137
|
const target = edge.to ?? unknownNodeId(edge);
|
|
133
138
|
if (edge.to === null) {
|
|
134
139
|
graph.setNode(target, {
|
|
@@ -139,7 +144,7 @@ function addEdges(graph, machine) {
|
|
|
139
144
|
if (region !== null) graph.setParent(target, regionNodeId(region));
|
|
140
145
|
}
|
|
141
146
|
const label = {
|
|
142
|
-
width: edgeText(edge
|
|
147
|
+
width: edgeText(edge).length * CHAR_WIDTH + 8,
|
|
143
148
|
height: LABEL_HEIGHT,
|
|
144
149
|
labelpos: "c"
|
|
145
150
|
};
|
|
@@ -161,7 +166,7 @@ function placeNodes(graph, machine) {
|
|
|
161
166
|
state,
|
|
162
167
|
...box(graph.node(state.id))
|
|
163
168
|
}));
|
|
164
|
-
const unknowns = machine.edges.filter((edge) => edge.kind !== "initial" && edge.to === null).map((edge) => ({
|
|
169
|
+
const unknowns = machine.edges.filter((edge) => edge.kind !== "initial" && edge.kind !== "effect" && edge.to === null).map((edge) => ({
|
|
165
170
|
kind: "unknown",
|
|
166
171
|
id: unknownNodeId(edge),
|
|
167
172
|
edgeId: edge.id,
|
|
@@ -198,15 +203,66 @@ function selfLoop(node, nth) {
|
|
|
198
203
|
}
|
|
199
204
|
};
|
|
200
205
|
}
|
|
206
|
+
function pathLength(points) {
|
|
207
|
+
let total = 0;
|
|
208
|
+
for (let index = 1; index < points.length; index++) {
|
|
209
|
+
const from = points[index - 1];
|
|
210
|
+
const to = points[index];
|
|
211
|
+
if (from === void 0 || to === void 0) continue;
|
|
212
|
+
total += Math.hypot(to.x - from.x, to.y - from.y);
|
|
213
|
+
}
|
|
214
|
+
return total;
|
|
215
|
+
}
|
|
216
|
+
function dotGapFor(length) {
|
|
217
|
+
return Math.min(MAX_DOT_GAP, Math.max(MIN_DOT_GAP, length * DOT_GAP_PER_LENGTH));
|
|
218
|
+
}
|
|
219
|
+
const LABEL_GAP = 2;
|
|
220
|
+
function collides(a, b) {
|
|
221
|
+
return Math.abs(a.x - b.x) < (a.width + b.width) / 2 + LABEL_GAP && Math.abs(a.y - b.y) < (a.height + b.height) / 2 + LABEL_GAP;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Moves each label vertically (0, +1, -1, +2, -2 steps) to the first spot that
|
|
225
|
+
* clears every label placed before it. Pure and order-stable.
|
|
226
|
+
*/
|
|
227
|
+
function separateLabels(boxes) {
|
|
228
|
+
const placed = [];
|
|
229
|
+
for (const labelBox of boxes) {
|
|
230
|
+
let candidate = labelBox;
|
|
231
|
+
for (let step = 1; placed.some((other) => collides(candidate, other)); step++) {
|
|
232
|
+
const shift = 18 * Math.ceil(step / 2);
|
|
233
|
+
candidate = {
|
|
234
|
+
...labelBox,
|
|
235
|
+
y: labelBox.y + (step % 2 === 1 ? shift : -shift)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
placed.push(candidate);
|
|
239
|
+
}
|
|
240
|
+
return placed;
|
|
241
|
+
}
|
|
242
|
+
function avoidLabelCollisions(edges) {
|
|
243
|
+
const moved = separateLabels(edges.map((placed) => ({
|
|
244
|
+
...placed.label,
|
|
245
|
+
width: edgeText(placed.edge).length * CHAR_WIDTH + 8,
|
|
246
|
+
height: LABEL_HEIGHT
|
|
247
|
+
})));
|
|
248
|
+
return edges.map((placed, index) => ({
|
|
249
|
+
...placed,
|
|
250
|
+
label: {
|
|
251
|
+
x: placed.label.x,
|
|
252
|
+
y: moved[index]?.y ?? placed.label.y
|
|
253
|
+
}
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
201
256
|
function placeEdges(graph, machine) {
|
|
202
257
|
const loopsSeen = /* @__PURE__ */ new Map();
|
|
203
|
-
return machine.edges.filter((edge) => edge.kind !== "initial").map((edge) => {
|
|
258
|
+
return machine.edges.filter((edge) => edge.kind !== "initial" && edge.kind !== "effect").map((edge) => {
|
|
204
259
|
if (edge.to === edge.from) {
|
|
205
260
|
const nth = loopsSeen.get(edge.from) ?? 0;
|
|
206
261
|
loopsSeen.set(edge.from, nth + 1);
|
|
207
262
|
return {
|
|
208
263
|
edge,
|
|
209
264
|
target: edge.from,
|
|
265
|
+
dotGap: dotGapFor(72),
|
|
210
266
|
...selfLoop(graph.node(edge.from), nth)
|
|
211
267
|
};
|
|
212
268
|
}
|
|
@@ -219,7 +275,8 @@ function placeEdges(graph, machine) {
|
|
|
219
275
|
label: {
|
|
220
276
|
x: label.x ?? 0,
|
|
221
277
|
y: label.y ?? 0
|
|
222
|
-
}
|
|
278
|
+
},
|
|
279
|
+
dotGap: dotGapFor(pathLength(label.points ?? []))
|
|
223
280
|
};
|
|
224
281
|
});
|
|
225
282
|
}
|
|
@@ -249,8 +306,8 @@ function layoutMachine(machine) {
|
|
|
249
306
|
});
|
|
250
307
|
graph.setGraph({
|
|
251
308
|
rankdir: "LR",
|
|
252
|
-
nodesep:
|
|
253
|
-
ranksep:
|
|
309
|
+
nodesep: 36,
|
|
310
|
+
ranksep: 72,
|
|
254
311
|
edgesep: 14,
|
|
255
312
|
marginx: 48,
|
|
256
313
|
marginy: 64
|
|
@@ -265,7 +322,7 @@ function layoutMachine(machine) {
|
|
|
265
322
|
width: size.width ?? 0,
|
|
266
323
|
height: size.height ?? 0,
|
|
267
324
|
nodes: placeNodes(graph, machine),
|
|
268
|
-
edges: placeEdges(graph, machine),
|
|
325
|
+
edges: avoidLabelCollisions(placeEdges(graph, machine)),
|
|
269
326
|
regions: machine.regions.map((region) => ({
|
|
270
327
|
region,
|
|
271
328
|
...box(graph.node(regionNodeId(region)))
|
|
@@ -312,6 +369,39 @@ function regionGroup(doc, placed) {
|
|
|
312
369
|
y: placed.y - placed.height / 2 + 14
|
|
313
370
|
}, [placed.region])]);
|
|
314
371
|
}
|
|
372
|
+
function chipGroups(doc, placed) {
|
|
373
|
+
const top = placed.y - placed.height / 2;
|
|
374
|
+
const width = placed.width - 16;
|
|
375
|
+
return placed.state.effects.map((effect, index) => {
|
|
376
|
+
const y = top + 36 + index * 20 - 8;
|
|
377
|
+
return svg(doc, "g", {
|
|
378
|
+
class: "chip",
|
|
379
|
+
"data-effect": effect.name,
|
|
380
|
+
"data-state": placed.state.id,
|
|
381
|
+
tabindex: 0,
|
|
382
|
+
"aria-label": effect.sends === null ? `effect ${effect.name} (declare sends to see links)` : `effect ${effect.name}`
|
|
383
|
+
}, [
|
|
384
|
+
svg(doc, "rect", {
|
|
385
|
+
x: placed.x - width / 2,
|
|
386
|
+
y,
|
|
387
|
+
width,
|
|
388
|
+
height: 16,
|
|
389
|
+
rx: 8
|
|
390
|
+
}),
|
|
391
|
+
svg(doc, "text", {
|
|
392
|
+
x: placed.x,
|
|
393
|
+
y: y + 12,
|
|
394
|
+
"text-anchor": "middle"
|
|
395
|
+
}, [effect.name]),
|
|
396
|
+
effect.sends === null ? svg(doc, "text", {
|
|
397
|
+
class: "hint",
|
|
398
|
+
x: placed.x + width / 2 - 8,
|
|
399
|
+
y: y + 12,
|
|
400
|
+
"text-anchor": "middle"
|
|
401
|
+
}, ["?", svg(doc, "title", {}, ["declare `sends` to see links"])]) : null
|
|
402
|
+
]);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
315
405
|
function stateGroup(doc, placed, callbacks) {
|
|
316
406
|
const { state } = placed;
|
|
317
407
|
const left = placed.x - placed.width / 2;
|
|
@@ -347,9 +437,10 @@ function stateGroup(doc, placed, callbacks) {
|
|
|
347
437
|
}) : null,
|
|
348
438
|
svg(doc, "text", {
|
|
349
439
|
x: placed.x,
|
|
350
|
-
y:
|
|
440
|
+
y: top + 22,
|
|
351
441
|
"text-anchor": "middle"
|
|
352
|
-
}, [state.label])
|
|
442
|
+
}, [state.label]),
|
|
443
|
+
...chipGroups(doc, placed)
|
|
353
444
|
]);
|
|
354
445
|
activatable(group, () => callbacks.select({
|
|
355
446
|
kind: "state",
|
|
@@ -394,18 +485,16 @@ function nodeGroup(doc, node, callbacks) {
|
|
|
394
485
|
return node.kind === "any" ? anyGroup(doc, node) : unknownGroup(doc, node);
|
|
395
486
|
}
|
|
396
487
|
function edgeAria(edge, text) {
|
|
397
|
-
if (edge.kind === "effect") return `${edge.from} ${text} on entry`;
|
|
398
488
|
if (edge.kind === "stay") return `${sourceText$1(edge.from)} on ${text} stays`;
|
|
399
489
|
return `${sourceText$1(edge.from)} on ${text} to ${edge.to ?? "undetermined"}`;
|
|
400
490
|
}
|
|
401
491
|
function edgeGroup(doc, placed, context) {
|
|
402
492
|
const { edge } = placed;
|
|
403
493
|
const internal = context.machine.events.find((event) => event.id === edge.event)?.internal;
|
|
404
|
-
const text = edgeText(edge
|
|
494
|
+
const text = edgeText(edge);
|
|
405
495
|
const group = svg(doc, "g", {
|
|
406
496
|
class: classes({
|
|
407
497
|
edge: true,
|
|
408
|
-
effect: edge.kind === "effect",
|
|
409
498
|
stay: edge.kind === "stay",
|
|
410
499
|
fuzz: edge.origin === "fuzz",
|
|
411
500
|
undetermined: edge.to === null,
|
|
@@ -418,7 +507,8 @@ function edgeGroup(doc, placed, context) {
|
|
|
418
507
|
"aria-label": edgeAria(edge, text)
|
|
419
508
|
}, [svg(doc, "path", {
|
|
420
509
|
d: placed.path,
|
|
421
|
-
"marker-end": "url(#viz-arrow)"
|
|
510
|
+
"marker-end": "url(#viz-arrow)",
|
|
511
|
+
style: internal === true || edge.kind === "stay" ? `--viz-dot-gap: ${placed.dotGap.toFixed(1)}` : null
|
|
422
512
|
}), svg(doc, "text", {
|
|
423
513
|
x: placed.label.x,
|
|
424
514
|
y: placed.label.y + 4,
|
|
@@ -456,6 +546,35 @@ function wireHover(root) {
|
|
|
456
546
|
});
|
|
457
547
|
}
|
|
458
548
|
}
|
|
549
|
+
/** Edges a chip's effect sends, including `onAny` handlers, by edge id. */
|
|
550
|
+
function sentEdges(root, machine, chip) {
|
|
551
|
+
const effect = chip.dataset["effect"] ?? "";
|
|
552
|
+
const ids = new Set(movesFrom(machine, chip.dataset["state"] ?? "").filter((move) => move.sentBy.includes(effect)).map((move) => move.edge.id));
|
|
553
|
+
return [...root.querySelectorAll(".edge")].filter((edge) => ids.has(edge.dataset["edgeId"] ?? ""));
|
|
554
|
+
}
|
|
555
|
+
/** Hovering or focusing a chip highlights the edges for its `sends` and dims the rest. */
|
|
556
|
+
function wireChips(root, doc, machine) {
|
|
557
|
+
const emitters = /* @__PURE__ */ new Map();
|
|
558
|
+
for (const chip of root.querySelectorAll(".chip")) {
|
|
559
|
+
const edges = sentEdges(root, machine, chip);
|
|
560
|
+
const effect = chip.dataset["effect"] ?? "";
|
|
561
|
+
for (const edge of edges) emitters.set(edge, [...emitters.get(edge) ?? [], effect]);
|
|
562
|
+
const on = () => {
|
|
563
|
+
if (edges.length === 0) return;
|
|
564
|
+
root.classList.add("chip-active");
|
|
565
|
+
for (const edge of edges) edge.classList.add("sent");
|
|
566
|
+
};
|
|
567
|
+
const off = () => {
|
|
568
|
+
root.classList.remove("chip-active");
|
|
569
|
+
for (const edge of edges) edge.classList.remove("sent");
|
|
570
|
+
};
|
|
571
|
+
chip.addEventListener("mouseenter", on);
|
|
572
|
+
chip.addEventListener("mouseleave", off);
|
|
573
|
+
chip.addEventListener("focusin", on);
|
|
574
|
+
chip.addEventListener("focusout", off);
|
|
575
|
+
}
|
|
576
|
+
for (const [edge, names] of emitters) edge.prepend(svg(doc, "title", {}, [`emitted by ${names.join(", ")}`]));
|
|
577
|
+
}
|
|
459
578
|
/** Static SVG for one machine. Regions first, then edges, then nodes, so nodes sit on top. */
|
|
460
579
|
function renderGraph(doc, machine, callbacks) {
|
|
461
580
|
const placed = layoutMachine(machine);
|
|
@@ -478,6 +597,7 @@ function renderGraph(doc, machine, callbacks) {
|
|
|
478
597
|
...placed.nodes.map((node) => nodeGroup(doc, node, callbacks))
|
|
479
598
|
]);
|
|
480
599
|
wireHover(root);
|
|
600
|
+
wireChips(root, doc, machine);
|
|
481
601
|
return root;
|
|
482
602
|
}
|
|
483
603
|
// src/viewer/explanation.ts
|
|
@@ -703,6 +823,59 @@ function renderPanel(doc, machine, context) {
|
|
|
703
823
|
}
|
|
704
824
|
return html(doc, "div", {}, [html(doc, "p", { class: "hint" }, ["Select a state or edge."]), fuzzSummary(doc, machine)]);
|
|
705
825
|
}
|
|
826
|
+
// src/viewer/context-diff.ts
|
|
827
|
+
function same(left, right) {
|
|
828
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
829
|
+
}
|
|
830
|
+
function childOf(value, key) {
|
|
831
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
832
|
+
if (Array.isArray(value)) return /^\d+$/.test(key) ? value[Number(key)] : void 0;
|
|
833
|
+
return Object.hasOwn(value, key) ? value[key] : void 0;
|
|
834
|
+
}
|
|
835
|
+
function lines(value, context) {
|
|
836
|
+
const { before, depth, label, comma } = context;
|
|
837
|
+
const pad = " ".repeat(depth);
|
|
838
|
+
const entries = Array.isArray(value) ? value.map((item, index) => [String(index), item]) : value !== null && typeof value === "object" ? Object.entries(value) : [];
|
|
839
|
+
if (entries.length === 0) return [{
|
|
840
|
+
text: `${pad}${label}${JSON.stringify(value) ?? "undefined"}${comma}`,
|
|
841
|
+
changed: !same(value, before)
|
|
842
|
+
}];
|
|
843
|
+
const [open, close] = Array.isArray(value) ? ["[", "]"] : ["{", "}"];
|
|
844
|
+
const body = entries.flatMap(([key, item], index) => lines(item, {
|
|
845
|
+
before: childOf(before, key),
|
|
846
|
+
depth: depth + 1,
|
|
847
|
+
label: Array.isArray(value) ? "" : `${JSON.stringify(key)}: `,
|
|
848
|
+
comma: index === entries.length - 1 ? "" : ","
|
|
849
|
+
}));
|
|
850
|
+
return [
|
|
851
|
+
{
|
|
852
|
+
text: `${pad}${label}${open}`,
|
|
853
|
+
changed: false
|
|
854
|
+
},
|
|
855
|
+
...body,
|
|
856
|
+
{
|
|
857
|
+
text: `${pad}${close}${comma}`,
|
|
858
|
+
changed: false
|
|
859
|
+
}
|
|
860
|
+
];
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* The context pretty-printed like `JSON.stringify(value, null, 2)`, one entry per
|
|
864
|
+
* line, with the lines whose value is new or different from `before` marked.
|
|
865
|
+
* `before` of undefined marks nothing.
|
|
866
|
+
*/
|
|
867
|
+
function contextLines(value, before) {
|
|
868
|
+
const marked = lines(value, {
|
|
869
|
+
before,
|
|
870
|
+
depth: 0,
|
|
871
|
+
label: "",
|
|
872
|
+
comma: ""
|
|
873
|
+
});
|
|
874
|
+
return before === void 0 ? marked.map((line) => ({
|
|
875
|
+
...line,
|
|
876
|
+
changed: false
|
|
877
|
+
})) : marked;
|
|
878
|
+
}
|
|
706
879
|
// src/viewer/explore.ts
|
|
707
880
|
const GROUP_TITLES = {
|
|
708
881
|
input: "Send an input",
|
|
@@ -765,8 +938,13 @@ function effectsSection(doc, machine, current) {
|
|
|
765
938
|
if (effects.length === 0) return null;
|
|
766
939
|
const unhandled = unhandledSends(machine, current);
|
|
767
940
|
const isUnhandled = (effect, event) => unhandled.some((each) => each.effect === effect && each.event === event);
|
|
941
|
+
const effectDoc = machine.edges.find((edge) => edge.kind === "effect" && edge.from === current)?.doc;
|
|
768
942
|
return html(doc, "div", { class: "explore-effects" }, [
|
|
769
943
|
html(doc, "p", {}, [`Entering ${current} runs:`]),
|
|
944
|
+
docNote(doc, {
|
|
945
|
+
text: effectDoc ?? null,
|
|
946
|
+
scope: "edge"
|
|
947
|
+
}),
|
|
770
948
|
html(doc, "ul", {}, effects.map((effect) => {
|
|
771
949
|
const sends = effect.sends ?? [];
|
|
772
950
|
const summary = effect.sends === null ? html(doc, "span", { class: "muted" }, [" (sends not declared)"]) : sends.length === 0 ? " sends nothing" : " sends ";
|
|
@@ -943,8 +1121,26 @@ function outcomeText(last) {
|
|
|
943
1121
|
if (last.outcome === "stay") return ` handled in ${last.from}: stays`;
|
|
944
1122
|
return ` ignored in ${last.from}: no handler`;
|
|
945
1123
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1124
|
+
const collapsedContexts = /* @__PURE__ */ new WeakSet();
|
|
1125
|
+
/** Collapsible, pretty-printed context. The fold survives the panel redraw after each step. */
|
|
1126
|
+
function contextSection(doc, value, context) {
|
|
1127
|
+
const { viewer, previous } = context;
|
|
1128
|
+
const section = html(doc, "details", {
|
|
1129
|
+
class: "explore-context",
|
|
1130
|
+
"data-live-context": ""
|
|
1131
|
+
}, [html(doc, "summary", {}, ["Context"]), html(doc, "pre", {}, contextLines(value, previous).flatMap((line, index) => [index === 0 ? "" : "\n", line.changed ? html(doc, "mark", {
|
|
1132
|
+
class: "context-changed",
|
|
1133
|
+
"data-context-changed": ""
|
|
1134
|
+
}, [line.text]) : line.text]))]);
|
|
1135
|
+
section.open = !collapsedContexts.has(viewer);
|
|
1136
|
+
section.addEventListener("toggle", () => {
|
|
1137
|
+
if (section.open) collapsedContexts.delete(viewer);
|
|
1138
|
+
else collapsedContexts.add(viewer);
|
|
1139
|
+
});
|
|
1140
|
+
return section;
|
|
1141
|
+
}
|
|
1142
|
+
function liveResultSection(doc, live) {
|
|
1143
|
+
const { last } = live.state;
|
|
948
1144
|
return html(doc, "div", { class: "explore-live" }, [last === null ? html(doc, "p", { class: "muted" }, ["Nothing has run yet."]) : html(doc, "div", {
|
|
949
1145
|
class: "explore-outcome",
|
|
950
1146
|
"data-outcome": last.outcome
|
|
@@ -956,7 +1152,10 @@ function liveResultSection(doc, state) {
|
|
|
956
1152
|
role: "alert",
|
|
957
1153
|
"data-live-error": ""
|
|
958
1154
|
}, [`Handler threw: ${last.error}`])
|
|
959
|
-
]),
|
|
1155
|
+
]), contextSection(doc, live.state.context, {
|
|
1156
|
+
viewer: live.send,
|
|
1157
|
+
previous: live.previous
|
|
1158
|
+
})]);
|
|
960
1159
|
}
|
|
961
1160
|
function livePathSection(doc, context, state) {
|
|
962
1161
|
return html(doc, "div", { class: "explore-path" }, [html(doc, "h3", {}, ["Path"]), html(doc, "ol", {}, [html(doc, "li", { "aria-current": state.steps.length === 0 ? "step" : null }, [button(doc, ["start"], {
|
|
@@ -995,7 +1194,7 @@ function explorePanel(doc, context) {
|
|
|
995
1194
|
final
|
|
996
1195
|
}),
|
|
997
1196
|
...live ? [
|
|
998
|
-
liveResultSection(doc, live
|
|
1197
|
+
liveResultSection(doc, live),
|
|
999
1198
|
liveMovesSection(doc, machine, {
|
|
1000
1199
|
state: live.state,
|
|
1001
1200
|
send: live.send,
|
|
@@ -1049,8 +1248,8 @@ function markExplore(root, context) {
|
|
|
1049
1248
|
const id = element.dataset["edgeId"] ?? "";
|
|
1050
1249
|
element.classList.toggle("taken", taken.has(id));
|
|
1051
1250
|
element.classList.toggle("next", next.has(id));
|
|
1052
|
-
element.classList.toggle("running", element.classList.contains("effect") && element.dataset["from"] === current);
|
|
1053
1251
|
}
|
|
1252
|
+
for (const element of root.querySelectorAll(".graph g.chip")) element.classList.toggle("running", element.dataset["state"] === current);
|
|
1054
1253
|
for (const element of root.querySelectorAll(".graph g[data-node-id]")) {
|
|
1055
1254
|
const id = element.dataset["nodeId"];
|
|
1056
1255
|
element.classList.toggle("current", id === current);
|
|
@@ -1058,29 +1257,13 @@ function markExplore(root, context) {
|
|
|
1058
1257
|
}
|
|
1059
1258
|
}
|
|
1060
1259
|
// src/viewer/viewer.ts
|
|
1061
|
-
const
|
|
1062
|
-
const LIVE_BANNER = "Live: the real machine runs. Send an input and the cursor follows its actual state; guards and effects run.";
|
|
1063
|
-
const FUZZ_NOTE = "Green edges were observed while fuzzing but not declared. Fuzzing finds behaviour; it never proves an edge is impossible.";
|
|
1260
|
+
const DOCS_URL = "https://anderscan.github.io/mantaq/guides/visualizer/";
|
|
1064
1261
|
function effectiveMode(view, machine) {
|
|
1065
1262
|
const modes = availableModes(machine);
|
|
1066
1263
|
if (view.coverage === "off") return "off";
|
|
1067
1264
|
if (view.coverage !== null && modes.includes(view.coverage)) return view.coverage;
|
|
1068
1265
|
return modes[0] ?? "off";
|
|
1069
1266
|
}
|
|
1070
|
-
function legend(doc, { mode, machine }) {
|
|
1071
|
-
const entries = [
|
|
1072
|
-
["legend-final", "double border: final state"],
|
|
1073
|
-
["legend-undetermined", "dashed edge to ?: target undetermined"],
|
|
1074
|
-
["legend-internal", "dotted edge, italic label: internal event"],
|
|
1075
|
-
["legend-effect", "faint self-loop: state runs effects on entry"],
|
|
1076
|
-
["legend-current", "thick accent border: where the walk is"],
|
|
1077
|
-
["legend-next", "pulsing: a possible next step"],
|
|
1078
|
-
["legend-taken", "bold accent: path taken"]
|
|
1079
|
-
];
|
|
1080
|
-
if (machine.fuzz !== null) entries.push(["legend-fuzz", "green: observed only while fuzzing"], ["legend-stay", "dotted self-loop: handled, no state change"]);
|
|
1081
|
-
if (mode !== "off") entries.push(["legend-unreached", unreachedText(mode, machine)]);
|
|
1082
|
-
return html(doc, "ul", { class: "legend" }, entries.map(([name, text]) => html(doc, "li", { class: name }, [text])));
|
|
1083
|
-
}
|
|
1084
1267
|
function machineSelect(doc, viz, context) {
|
|
1085
1268
|
if (viz.machines.length < 2) return null;
|
|
1086
1269
|
const select = html(doc, "select", { class: "machine-select" }, viz.machines.map((machine) => html(doc, "option", {
|
|
@@ -1091,7 +1274,7 @@ function machineSelect(doc, viz, context) {
|
|
|
1091
1274
|
return html(doc, "label", {}, ["Machine ", select]);
|
|
1092
1275
|
}
|
|
1093
1276
|
function isMode(value) {
|
|
1094
|
-
return value === "off" || value === "run" || value === "tests";
|
|
1277
|
+
return value === "off" || value === "run" || value === "tests" || value === "reached";
|
|
1095
1278
|
}
|
|
1096
1279
|
function coveragePicker(doc, context) {
|
|
1097
1280
|
const modes = availableModes(context.machine);
|
|
@@ -1117,10 +1300,11 @@ function mountViewer(root, viz, options = {}) {
|
|
|
1117
1300
|
selection: null,
|
|
1118
1301
|
coverage: null,
|
|
1119
1302
|
path: first === void 0 || options.driver !== void 0 ? [] : decodePath(first, options.steps ?? []),
|
|
1120
|
-
live: null
|
|
1303
|
+
live: null,
|
|
1304
|
+
previous: void 0
|
|
1121
1305
|
};
|
|
1122
1306
|
const { driver } = options;
|
|
1123
|
-
if (driver !== void 0) view
|
|
1307
|
+
if (driver !== void 0) replayInto(view, driver, decodeLiveSteps(options.steps ?? []));
|
|
1124
1308
|
let back = () => void 0;
|
|
1125
1309
|
root.addEventListener("keydown", (event) => {
|
|
1126
1310
|
if (!("key" in event) || event.target instanceof HTMLSelectElement) return;
|
|
@@ -1148,7 +1332,8 @@ function mountViewer(root, viz, options = {}) {
|
|
|
1148
1332
|
rewind,
|
|
1149
1333
|
...view.live === null ? {} : { live: {
|
|
1150
1334
|
state: view.live,
|
|
1151
|
-
send
|
|
1335
|
+
send,
|
|
1336
|
+
previous: view.previous
|
|
1152
1337
|
} }
|
|
1153
1338
|
}), renderPanel(doc, machine, {
|
|
1154
1339
|
selection: view.selection,
|
|
@@ -1171,6 +1356,7 @@ function mountViewer(root, viz, options = {}) {
|
|
|
1171
1356
|
};
|
|
1172
1357
|
const send = (step) => {
|
|
1173
1358
|
if (driver === void 0) return;
|
|
1359
|
+
view.previous = view.live?.context;
|
|
1174
1360
|
view.live = stepSafely(driver, {
|
|
1175
1361
|
before: view.live,
|
|
1176
1362
|
step
|
|
@@ -1192,7 +1378,7 @@ function mountViewer(root, viz, options = {}) {
|
|
|
1192
1378
|
};
|
|
1193
1379
|
const rewind = (length) => {
|
|
1194
1380
|
if (driver !== void 0 && view.live !== null) {
|
|
1195
|
-
view
|
|
1381
|
+
replayInto(view, driver, view.live.steps.slice(0, Math.max(0, length)));
|
|
1196
1382
|
walked();
|
|
1197
1383
|
return;
|
|
1198
1384
|
}
|
|
@@ -1219,12 +1405,6 @@ function mountViewer(root, viz, options = {}) {
|
|
|
1219
1405
|
render,
|
|
1220
1406
|
onNavigate: options.onNavigate,
|
|
1221
1407
|
live: driver !== void 0
|
|
1222
|
-
}), html(doc, "p", {
|
|
1223
|
-
class: "banner",
|
|
1224
|
-
role: "note"
|
|
1225
|
-
}, [driver === void 0 ? banner(machine) : LIVE_BANNER]), legend(doc, {
|
|
1226
|
-
mode,
|
|
1227
|
-
machine
|
|
1228
1408
|
}), html(doc, "div", { class: "viz-main" }, [html(doc, "div", { class: "canvas" }, [renderGraph(doc, machine, {
|
|
1229
1409
|
select,
|
|
1230
1410
|
mode
|
|
@@ -1234,6 +1414,11 @@ function mountViewer(root, viz, options = {}) {
|
|
|
1234
1414
|
render();
|
|
1235
1415
|
}
|
|
1236
1416
|
/** A driver that throws leaves the state where it was and shows the message as a handler error. */
|
|
1417
|
+
/** Sets `view.live` to the state after `steps` and `view.previous` to the context before the last of them. */
|
|
1418
|
+
function replayInto(view, driver, steps) {
|
|
1419
|
+
view.previous = steps.length === 0 ? void 0 : replay(driver, steps.slice(0, -1)).context;
|
|
1420
|
+
view.live = replay(driver, steps);
|
|
1421
|
+
}
|
|
1237
1422
|
function stepSafely(driver, context) {
|
|
1238
1423
|
const { before, step } = context;
|
|
1239
1424
|
try {
|
|
@@ -1278,12 +1463,15 @@ function headerOf(doc, context) {
|
|
|
1278
1463
|
view.coverage = next;
|
|
1279
1464
|
render();
|
|
1280
1465
|
}
|
|
1281
|
-
})
|
|
1466
|
+
}),
|
|
1467
|
+
html(doc, "a", {
|
|
1468
|
+
class: "help-link",
|
|
1469
|
+
href: DOCS_URL,
|
|
1470
|
+
target: "_blank",
|
|
1471
|
+
rel: "noopener noreferrer"
|
|
1472
|
+
}, ["How this works"])
|
|
1282
1473
|
]);
|
|
1283
1474
|
}
|
|
1284
|
-
function banner(machine) {
|
|
1285
|
-
return machine.fuzz === null ? STRUCTURE_BANNER : `${STRUCTURE_BANNER} ${FUZZ_NOTE}`;
|
|
1286
|
-
}
|
|
1287
1475
|
function markSelection(root, selection) {
|
|
1288
1476
|
const attribute = selection?.kind === "edge" ? "edgeId" : "nodeId";
|
|
1289
1477
|
for (const element of root.querySelectorAll(".graph g")) element.classList.toggle("selected", selection !== null && element.dataset[attribute] === selection.id);
|
|
@@ -1329,4 +1517,4 @@ function mountVizText(root, context, options = {}) {
|
|
|
1329
1517
|
}
|
|
1330
1518
|
mountViewer(root, result[1], options);
|
|
1331
1519
|
}
|
|
1332
|
-
export {
|
|
1520
|
+
export { showIssues as n, mountViewer as r, mountVizText as t };
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { d as VizDriver, r as VizIssue, u as VizDocument } from "./validate-C_EXBtem.mjs";
|
|
2
|
-
declare const STRUCTURE_BANNER = "Click a pulsing edge, or a state it leads to, to walk the machine. Edges come from each handler's declared `to` list and effects' `sends`; guards are not evaluated, so a path here may not happen at runtime.";
|
|
3
|
-
declare const FUZZ_NOTE = "Green edges were observed while fuzzing but not declared. Fuzzing finds behaviour; it never proves an edge is impossible.";
|
|
4
2
|
interface ViewerOptions {
|
|
5
3
|
/** Title element. Embedded viewers use `h2`/`h3` to fit the host page's outline. */
|
|
6
4
|
heading?: "h1" | "h2" | "h3";
|
|
@@ -34,4 +32,4 @@ declare function mountVizText(root: HTMLElement, context: {
|
|
|
34
32
|
source: string;
|
|
35
33
|
text: string;
|
|
36
34
|
}, options?: ViewerOptions): void;
|
|
37
|
-
export {
|
|
35
|
+
export { mountViewer as i, showIssues as n, ViewerOptions as r, mountVizText as t };
|
package/dist/page.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as ViewerOptions } from "./embed-OmMnILen.mjs";
|
|
2
2
|
/** Options that open the walk named in the address bar and keep it there. */
|
|
3
3
|
export declare function linkOptions(): ViewerOptions;
|
|
4
4
|
/** Mount the document embedded in the page itself. */
|
package/dist/page.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./html-BfvuzJs6.mjs";
|
|
2
|
-
import { n as showIssues, t as mountVizText } from "./embed-
|
|
2
|
+
import { n as showIssues, t as mountVizText } from "./embed-Dpjn47Qx.mjs";
|
|
3
3
|
// src/viewer/page.ts
|
|
4
4
|
/**
|
|
5
5
|
* Whole-page entries: a self-contained HTML file (`mountEmbedded`) and a live
|
package/dist/standalone.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mountEmbedded } from "./page.mjs";
|
|
2
2
|
// src/viewer/standalone.ts
|
|
3
|
-
/** Entry of the self-contained
|
|
3
|
+
/** Entry of the self-contained static page. */
|
|
4
4
|
const root = document.querySelector("#app");
|
|
5
5
|
if (root) mountEmbedded(root);
|
|
6
6
|
export {};
|
package/dist/style.css
CHANGED
|
@@ -46,21 +46,10 @@
|
|
|
46
46
|
font-size: 18px;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
.viz .
|
|
50
|
-
background: var(--banner-bg);
|
|
51
|
-
color: var(--banner-fg);
|
|
52
|
-
border-radius: 6px;
|
|
53
|
-
padding: 8px 12px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.viz .legend {
|
|
49
|
+
.viz .help-link {
|
|
57
50
|
color: var(--muted);
|
|
58
|
-
|
|
59
|
-
gap: 4px 18px;
|
|
60
|
-
padding: 0;
|
|
51
|
+
margin-left: auto;
|
|
61
52
|
font-size: 12px;
|
|
62
|
-
list-style: none;
|
|
63
|
-
display: flex;
|
|
64
53
|
}
|
|
65
54
|
|
|
66
55
|
.viz .viz-main {
|
|
@@ -158,7 +147,7 @@
|
|
|
158
147
|
}
|
|
159
148
|
|
|
160
149
|
.viz .edge.internal path {
|
|
161
|
-
stroke-dasharray: 1 3;
|
|
150
|
+
stroke-dasharray: 1 var(--viz-dot-gap, 3);
|
|
162
151
|
stroke-linecap: round;
|
|
163
152
|
}
|
|
164
153
|
|
|
@@ -166,8 +155,20 @@
|
|
|
166
155
|
font-style: italic;
|
|
167
156
|
}
|
|
168
157
|
|
|
169
|
-
.viz .
|
|
170
|
-
|
|
158
|
+
.viz .chip rect {
|
|
159
|
+
fill: var(--accent);
|
|
160
|
+
fill-opacity: .12;
|
|
161
|
+
stroke: var(--accent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.viz .chip text {
|
|
165
|
+
font-size: 11px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.viz .chip .hint {
|
|
169
|
+
fill: var(--unknown);
|
|
170
|
+
cursor: help;
|
|
171
|
+
font-weight: bold;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
.viz .edge.undetermined path {
|
|
@@ -184,7 +185,7 @@
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
.viz .edge.stay path {
|
|
187
|
-
stroke-dasharray: 1 3;
|
|
188
|
+
stroke-dasharray: 1 var(--viz-dot-gap, 3);
|
|
188
189
|
stroke-linecap: round;
|
|
189
190
|
}
|
|
190
191
|
|
|
@@ -240,6 +241,19 @@
|
|
|
240
241
|
stroke-width: 2.5px;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
.viz .chip {
|
|
245
|
+
cursor: help;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.viz .graph.chip-active .edge:not(.sent) {
|
|
249
|
+
opacity: .2;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.viz .edge.sent path {
|
|
253
|
+
stroke: var(--accent);
|
|
254
|
+
stroke-width: 2.5px;
|
|
255
|
+
}
|
|
256
|
+
|
|
243
257
|
.viz .state:focus-visible .frame, .viz .edge:focus-visible path {
|
|
244
258
|
stroke: var(--accent);
|
|
245
259
|
stroke-width: 3px;
|
|
@@ -306,8 +320,8 @@
|
|
|
306
320
|
font-weight: 700;
|
|
307
321
|
}
|
|
308
322
|
|
|
309
|
-
.viz .
|
|
310
|
-
opacity:
|
|
323
|
+
.viz .chip.running rect {
|
|
324
|
+
fill-opacity: .35;
|
|
311
325
|
}
|
|
312
326
|
|
|
313
327
|
.viz .running path {
|
|
@@ -393,6 +407,25 @@
|
|
|
393
407
|
stroke: var(--accent);
|
|
394
408
|
stroke-dasharray: 4 3;
|
|
395
409
|
}
|
|
410
|
+
|
|
411
|
+
.viz .explore-context summary {
|
|
412
|
+
cursor: pointer;
|
|
413
|
+
font-weight: 600;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.viz .explore-context pre {
|
|
417
|
+
background: var(--region-bg);
|
|
418
|
+
max-height: 40vh;
|
|
419
|
+
margin: 4px 0 0;
|
|
420
|
+
font-size: 12px;
|
|
421
|
+
overflow: auto;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.viz .explore-context mark.context-changed {
|
|
425
|
+
background: var(--banner-bg);
|
|
426
|
+
color: var(--banner-fg);
|
|
427
|
+
font-weight: 600;
|
|
428
|
+
}
|
|
396
429
|
body {
|
|
397
430
|
margin: 0;
|
|
398
431
|
padding: 16px;
|
package/dist/viewer.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { i as mountViewer, n as showIssues, r as ViewerOptions, t as mountVizText } from "./embed-OmMnILen.mjs";
|
|
2
|
+
export { type ViewerOptions, mountViewer, mountVizText, showIssues };
|
package/dist/viewer.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { r as
|
|
1
|
+
export { r as mountViewer, t as mountVizText, n as showIssues } from "./embed-Dpjn47Qx.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/viz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Draw and walk a Mantaq machine in the browser, from a versioned, deterministic JSON document.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@dagrejs/dagre": "^3.1.2",
|
|
32
|
-
"@mantaq/utils": "0.0.
|
|
32
|
+
"@mantaq/utils": "0.0.5"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
package/src/viewer/viewer.css
CHANGED
|
@@ -46,19 +46,8 @@
|
|
|
46
46
|
margin: 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
.viz .
|
|
50
|
-
|
|
51
|
-
color: var(--banner-fg);
|
|
52
|
-
padding: 8px 12px;
|
|
53
|
-
border-radius: 6px;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.viz .legend {
|
|
57
|
-
display: flex;
|
|
58
|
-
flex-wrap: wrap;
|
|
59
|
-
gap: 4px 18px;
|
|
60
|
-
padding: 0;
|
|
61
|
-
list-style: none;
|
|
49
|
+
.viz .help-link {
|
|
50
|
+
margin-left: auto;
|
|
62
51
|
color: var(--muted);
|
|
63
52
|
font-size: 12px;
|
|
64
53
|
}
|
|
@@ -159,7 +148,7 @@
|
|
|
159
148
|
}
|
|
160
149
|
|
|
161
150
|
.viz .edge.internal path {
|
|
162
|
-
stroke-dasharray: 1 3;
|
|
151
|
+
stroke-dasharray: 1 var(--viz-dot-gap, 3);
|
|
163
152
|
stroke-linecap: round;
|
|
164
153
|
}
|
|
165
154
|
|
|
@@ -167,8 +156,20 @@
|
|
|
167
156
|
font-style: italic;
|
|
168
157
|
}
|
|
169
158
|
|
|
170
|
-
.viz .
|
|
171
|
-
|
|
159
|
+
.viz .chip rect {
|
|
160
|
+
fill: var(--accent);
|
|
161
|
+
fill-opacity: 0.12;
|
|
162
|
+
stroke: var(--accent);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.viz .chip text {
|
|
166
|
+
font-size: 11px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.viz .chip .hint {
|
|
170
|
+
font-weight: bold;
|
|
171
|
+
fill: var(--unknown);
|
|
172
|
+
cursor: help;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
.viz .edge.undetermined path {
|
|
@@ -185,7 +186,7 @@
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
.viz .edge.stay path {
|
|
188
|
-
stroke-dasharray: 1 3;
|
|
189
|
+
stroke-dasharray: 1 var(--viz-dot-gap, 3);
|
|
189
190
|
stroke-linecap: round;
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -246,6 +247,19 @@
|
|
|
246
247
|
stroke-width: 2.5;
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
.viz .chip {
|
|
251
|
+
cursor: help;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.viz .graph.chip-active .edge:not(.sent) {
|
|
255
|
+
opacity: 0.2;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.viz .edge.sent path {
|
|
259
|
+
stroke: var(--accent);
|
|
260
|
+
stroke-width: 2.5;
|
|
261
|
+
}
|
|
262
|
+
|
|
249
263
|
.viz .state:focus-visible .frame,
|
|
250
264
|
.viz .edge:focus-visible path {
|
|
251
265
|
stroke: var(--accent);
|
|
@@ -316,8 +330,8 @@
|
|
|
316
330
|
font-weight: 700;
|
|
317
331
|
}
|
|
318
332
|
|
|
319
|
-
.viz .
|
|
320
|
-
opacity:
|
|
333
|
+
.viz .chip.running rect {
|
|
334
|
+
fill-opacity: 0.35;
|
|
321
335
|
}
|
|
322
336
|
|
|
323
337
|
.viz .running path {
|
|
@@ -404,3 +418,22 @@
|
|
|
404
418
|
stroke: var(--accent);
|
|
405
419
|
stroke-dasharray: 4 3;
|
|
406
420
|
}
|
|
421
|
+
|
|
422
|
+
.viz .explore-context summary {
|
|
423
|
+
cursor: pointer;
|
|
424
|
+
font-weight: 600;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.viz .explore-context pre {
|
|
428
|
+
margin: 4px 0 0;
|
|
429
|
+
overflow: auto;
|
|
430
|
+
max-height: 40vh;
|
|
431
|
+
font-size: 12px;
|
|
432
|
+
background: var(--region-bg);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.viz .explore-context mark.context-changed {
|
|
436
|
+
background: var(--banner-bg);
|
|
437
|
+
color: var(--banner-fg);
|
|
438
|
+
font-weight: 600;
|
|
439
|
+
}
|