@mantaq/viz 0.0.0 → 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.
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,6 @@
1
+ import { mountEmbedded } from "./page.mjs";
2
+ // src/viewer/standalone.ts
3
+ /** Entry of the self-contained HTML page `mantaq-inspect` writes. */
4
+ const root = document.querySelector("#app");
5
+ if (root) mountEmbedded(root);
6
+ export {};
package/dist/style.css ADDED
@@ -0,0 +1,404 @@
1
+ .viz {
2
+ --fuzz: #138a4a;
3
+ --bg: #fbfbfa;
4
+ --fg: #1d1d1f;
5
+ --muted: #6b6b70;
6
+ --line: #4a4a52;
7
+ --state-bg: #fff;
8
+ --state-border: #3b3b44;
9
+ --region-bg: #f1f1ef;
10
+ --accent: #2463eb;
11
+ --unknown: #a35c00;
12
+ --banner-bg: #fff6e0;
13
+ --banner-fg: #5a3e00;
14
+ color-scheme: light;
15
+ background: var(--bg);
16
+ color: var(--fg);
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ .viz {
21
+ --bg: #141416;
22
+ --fg: #ececf0;
23
+ --muted: #9a9aa3;
24
+ --line: #b4b4bf;
25
+ --state-bg: #1f1f23;
26
+ --state-border: #c9c9d2;
27
+ --region-bg: #1a1a1e;
28
+ --accent: #7aa7ff;
29
+ --unknown: #f0a742;
30
+ --fuzz: #4cc98a;
31
+ --banner-bg: #2e2510;
32
+ --banner-fg: #f3d58c;
33
+ color-scheme: dark;
34
+ }
35
+ }
36
+
37
+ .viz .viz-header {
38
+ flex-wrap: wrap;
39
+ align-items: center;
40
+ gap: 16px;
41
+ display: flex;
42
+ }
43
+
44
+ .viz .viz-header :is(h1, h2, h3) {
45
+ margin: 0;
46
+ font-size: 18px;
47
+ }
48
+
49
+ .viz .banner {
50
+ background: var(--banner-bg);
51
+ color: var(--banner-fg);
52
+ border-radius: 6px;
53
+ padding: 8px 12px;
54
+ }
55
+
56
+ .viz .legend {
57
+ color: var(--muted);
58
+ flex-wrap: wrap;
59
+ gap: 4px 18px;
60
+ padding: 0;
61
+ font-size: 12px;
62
+ list-style: none;
63
+ display: flex;
64
+ }
65
+
66
+ .viz .viz-main {
67
+ grid-template-columns: minmax(0, 1fr) 320px;
68
+ gap: 16px;
69
+ display: grid;
70
+ }
71
+
72
+ @media (width <= 760px) {
73
+ .viz .viz-main {
74
+ grid-template-columns: minmax(0, 1fr);
75
+ }
76
+ }
77
+
78
+ .viz .canvas {
79
+ overflow: auto;
80
+ }
81
+
82
+ .viz .panel {
83
+ border-left: 1px solid var(--region-bg);
84
+ padding-left: 16px;
85
+ }
86
+
87
+ .viz .panel dl {
88
+ grid-template-columns: auto 1fr;
89
+ gap: 4px 12px;
90
+ display: grid;
91
+ }
92
+
93
+ .viz .panel dt {
94
+ color: var(--muted);
95
+ }
96
+
97
+ .viz .panel dd {
98
+ margin: 0;
99
+ }
100
+
101
+ .viz .panel ul {
102
+ margin: 0;
103
+ padding-left: 16px;
104
+ }
105
+
106
+ .viz .link {
107
+ color: var(--accent);
108
+ cursor: pointer;
109
+ }
110
+
111
+ .viz .graph {
112
+ width: 100%;
113
+ height: auto;
114
+ }
115
+
116
+ .viz .graph text {
117
+ fill: var(--fg);
118
+ paint-order: stroke;
119
+ stroke: var(--bg);
120
+ stroke-width: 4px;
121
+ stroke-linejoin: round;
122
+ font-size: 12px;
123
+ }
124
+
125
+ .viz .graph .arrow-head {
126
+ fill: var(--line);
127
+ }
128
+
129
+ .viz .region rect {
130
+ fill: var(--region-bg);
131
+ stroke: var(--muted);
132
+ stroke-dasharray: 2 3;
133
+ }
134
+
135
+ .viz .region text {
136
+ fill: var(--muted);
137
+ }
138
+
139
+ .viz .state .frame {
140
+ fill: var(--state-bg);
141
+ stroke: var(--state-border);
142
+ stroke-width: 1.5px;
143
+ }
144
+
145
+ .viz .state.final .inner {
146
+ fill: none;
147
+ }
148
+
149
+ .viz .state, .viz .edge {
150
+ cursor: pointer;
151
+ outline: none;
152
+ }
153
+
154
+ .viz .edge path {
155
+ fill: none;
156
+ stroke: var(--line);
157
+ stroke-width: 1.5px;
158
+ }
159
+
160
+ .viz .edge.internal path {
161
+ stroke-dasharray: 1 3;
162
+ stroke-linecap: round;
163
+ }
164
+
165
+ .viz .edge.internal text {
166
+ font-style: italic;
167
+ }
168
+
169
+ .viz .edge.effect {
170
+ opacity: .45;
171
+ }
172
+
173
+ .viz .edge.undetermined path {
174
+ stroke: var(--unknown);
175
+ stroke-dasharray: 6 4;
176
+ }
177
+
178
+ .viz .edge.fuzz path {
179
+ stroke: var(--fuzz);
180
+ }
181
+
182
+ .viz .edge.fuzz text {
183
+ fill: var(--fuzz);
184
+ }
185
+
186
+ .viz .edge.stay path {
187
+ stroke-dasharray: 1 3;
188
+ stroke-linecap: round;
189
+ }
190
+
191
+ .viz .steps {
192
+ margin: 0;
193
+ padding-left: 20px;
194
+ font-family: ui-monospace, monospace;
195
+ font-size: 12px;
196
+ }
197
+
198
+ .viz .unknown-node circle {
199
+ fill: var(--state-bg);
200
+ stroke: var(--unknown);
201
+ stroke-dasharray: 4 3;
202
+ }
203
+
204
+ .viz .unknown-node text {
205
+ fill: var(--unknown);
206
+ font-weight: 700;
207
+ }
208
+
209
+ .viz .any-node rect {
210
+ fill: none;
211
+ stroke: var(--state-border);
212
+ stroke-width: 1.5px;
213
+ stroke-dasharray: 2 3;
214
+ }
215
+
216
+ .viz .any-node text {
217
+ fill: var(--muted);
218
+ font-style: italic;
219
+ }
220
+
221
+ .viz .entry circle {
222
+ fill: var(--line);
223
+ }
224
+
225
+ .viz .entry path {
226
+ stroke: var(--line);
227
+ stroke-width: 1.5px;
228
+ }
229
+
230
+ .viz.coverage-on .unreached {
231
+ opacity: .35;
232
+ }
233
+
234
+ .viz.coverage-on .state.unreached .frame, .viz.coverage-on .edge.unreached path {
235
+ stroke-dasharray: 3 3;
236
+ }
237
+
238
+ .viz .highlight path, .viz .highlight .frame, .viz .highlight circle, .viz .selected path, .viz .selected .frame {
239
+ stroke: var(--accent);
240
+ stroke-width: 2.5px;
241
+ }
242
+
243
+ .viz .state:focus-visible .frame, .viz .edge:focus-visible path {
244
+ stroke: var(--accent);
245
+ stroke-width: 3px;
246
+ }
247
+
248
+ .viz .load-error {
249
+ color: var(--unknown);
250
+ }
251
+
252
+ .viz .muted {
253
+ color: var(--muted);
254
+ }
255
+
256
+ .viz .explanation p {
257
+ margin: 2px 0;
258
+ }
259
+
260
+ .viz .explanation pre {
261
+ background: var(--region-bg);
262
+ border-radius: 4px;
263
+ padding: 6px;
264
+ font-size: 12px;
265
+ overflow-x: auto;
266
+ }
267
+
268
+ .viz .branches {
269
+ padding-left: 16px;
270
+ }
271
+
272
+ .viz .branch[data-branch-status="not in graph"] .branch-status {
273
+ color: var(--unknown);
274
+ font-weight: 600;
275
+ }
276
+
277
+ .viz .branch[data-branch-status="this edge"] .branch-status {
278
+ color: var(--accent);
279
+ }
280
+
281
+ .viz .prediction-note, .viz .routing-note {
282
+ background: var(--banner-bg);
283
+ color: var(--banner-fg);
284
+ border-radius: 4px;
285
+ padding: 6px 8px;
286
+ }
287
+
288
+ .viz .taken path, .viz .current .frame {
289
+ stroke: var(--accent);
290
+ stroke-width: 2.5px;
291
+ }
292
+
293
+ .viz .current .frame {
294
+ stroke-width: 3.5px;
295
+ }
296
+
297
+ .viz .next path {
298
+ stroke: var(--accent);
299
+ stroke-width: 3px;
300
+ stroke-dasharray: 8 5;
301
+ animation: .8s linear infinite viz-march;
302
+ }
303
+
304
+ .viz .next text {
305
+ fill: var(--accent);
306
+ font-weight: 700;
307
+ }
308
+
309
+ .viz .edge.effect.running {
310
+ opacity: 1;
311
+ }
312
+
313
+ .viz .running path {
314
+ stroke: var(--accent);
315
+ stroke-width: 2.5px;
316
+ }
317
+
318
+ .viz .running text {
319
+ fill: var(--accent);
320
+ font-weight: 700;
321
+ }
322
+
323
+ @keyframes viz-march {
324
+ to {
325
+ stroke-dashoffset: -13px;
326
+ }
327
+ }
328
+
329
+ @media (prefers-reduced-motion: reduce) {
330
+ .viz .next path {
331
+ animation: none;
332
+ }
333
+ }
334
+
335
+ .viz .explore {
336
+ background: var(--region-bg);
337
+ border-radius: 6px;
338
+ margin-bottom: 12px;
339
+ padding: 6px 10px;
340
+ }
341
+
342
+ .viz .explore h3 {
343
+ color: var(--muted);
344
+ margin: 10px 0 4px;
345
+ font-size: 13px;
346
+ }
347
+
348
+ .viz .explore ul, .viz .explore ol {
349
+ margin: 0;
350
+ padding-left: 18px;
351
+ }
352
+
353
+ .viz .explore li {
354
+ margin: 3px 0;
355
+ }
356
+
357
+ .viz .explore li > button + button {
358
+ margin-left: 6px;
359
+ }
360
+
361
+ .viz .explore button {
362
+ cursor: pointer;
363
+ }
364
+
365
+ .viz .explore-path li[aria-current="step"] button {
366
+ font-weight: 700;
367
+ }
368
+
369
+ .viz .explore-controls {
370
+ gap: 6px;
371
+ margin: 8px 0;
372
+ display: flex;
373
+ }
374
+
375
+ .viz .explore .unhandled code {
376
+ color: var(--unknown);
377
+ text-decoration: line-through;
378
+ }
379
+
380
+ .viz .explore-done {
381
+ border-left: 3px solid var(--accent);
382
+ padding: 4px 8px;
383
+ }
384
+
385
+ .viz .explore-warning {
386
+ background: var(--banner-bg);
387
+ color: var(--banner-fg);
388
+ border-radius: 4px;
389
+ padding: 4px 8px;
390
+ }
391
+
392
+ .viz .reachable .frame {
393
+ stroke: var(--accent);
394
+ stroke-dasharray: 4 3;
395
+ }
396
+ body {
397
+ margin: 0;
398
+ padding: 16px;
399
+ font: 14px / 1.4 system-ui, sans-serif;
400
+ }
401
+
402
+ .toolbar {
403
+ margin-bottom: 12px;
404
+ }
@@ -0,0 +1,230 @@
1
+ import { Either } from "@mantaq/utils";
2
+ /**
3
+ * Viz document schema. Bump `VIZ_SCHEMA_VERSION` on any breaking change.
4
+ * Everything not observable at runtime is `null`, never guessed.
5
+ */
6
+ declare const VIZ_SCHEMA_VERSION = 8;
7
+ /**
8
+ * Edge source of an `onAny` registration: any non-final state of the machine,
9
+ * or `region.*` inside a region. Never listed in `states`.
10
+ */
11
+ declare const ANY_STATE = "*";
12
+ /** Region prefix of a state id (`""` at the root). */
13
+ declare function scopeOf(id: string): string;
14
+ declare function isAnyState(id: string): boolean;
15
+ /** True when `stateId` is one of the states an `anyId` source stands for. */
16
+ declare function coveredByAny(anyId: string, stateId: string): boolean;
17
+ interface VizCoverage {
18
+ hits: number;
19
+ }
20
+ /** An effect a state runs on entry. Effects never transition; the events they send do. */
21
+ interface VizEffect {
22
+ name: string;
23
+ /** Event ids from the effect's `sends` declaration. null = not declared. */
24
+ sends: string[] | null;
25
+ }
26
+ interface VizState {
27
+ /** Path-prefixed id, e.g. `payment` or `region1.childA`. */
28
+ id: string;
29
+ label: string;
30
+ /** Region path this state belongs to, or null for the root machine. */
31
+ region: string | null;
32
+ initial: boolean;
33
+ final: boolean;
34
+ /** Effects this state runs on entry, in registration order. */
35
+ effects: VizEffect[];
36
+ /** Visits in the supplied run (history or fuzz corpus replay). null = no run. */
37
+ coverage: VizCoverage | null;
38
+ /** Visits by the supplied test sequences. null = no tests supplied. */
39
+ tested: VizCoverage | null;
40
+ /** Leading JSDoc of the `state(...)` declaration. null = undocumented or AST not run. */
41
+ doc: string | null;
42
+ }
43
+ interface VizEvent {
44
+ id: string;
45
+ internal: boolean;
46
+ /** Leading JSDoc of the `event(...)` declaration. null = undocumented or AST not run. */
47
+ doc: string | null;
48
+ /** Payload types are erased at runtime; always null. */
49
+ payloadType: null;
50
+ }
51
+ /**
52
+ * `stay`: the event was handled but the state did not change. Declared by a
53
+ * registration without `to`, or observed while fuzzing.
54
+ */
55
+ type VizEdgeKind = "initial" | "transition" | "effect" | "stay";
56
+ /**
57
+ * `declared`: from the builder's `to` list, so the type checker vouches for it.
58
+ * `probe`: an actor without declarations, found by synthetic traversal.
59
+ * `fuzz`: observed only while fuzzing.
60
+ */
61
+ type VizEdgeOrigin = "declared" | "probe" | "fuzz";
62
+ interface VizLocation {
63
+ /** POSIX path relative to the analysis root. */
64
+ file: string;
65
+ /** 1-based. */
66
+ line: number;
67
+ /** 1-based. */
68
+ column: number;
69
+ }
70
+ /** Where a handler return path goes. `stay` = no `state` in the result. */
71
+ type VizBranchTarget = {
72
+ kind: "state";
73
+ state: string;
74
+ } | {
75
+ kind: "stay";
76
+ } | {
77
+ kind: "unresolved";
78
+ text: string;
79
+ };
80
+ interface VizBranch {
81
+ /** Source text of each condition on the path, outermost first; `!(...)` = else side. */
82
+ conditions: string[];
83
+ target: VizBranchTarget;
84
+ }
85
+ /** `payload.amount > 50000` style comparison against a literal, for search guidance. */
86
+ interface VizComparison {
87
+ source: "payload" | "context";
88
+ /** Dotted path below payload / context, e.g. `result.reservationId`. */
89
+ path: string;
90
+ operator: "<" | "<=" | ">" | ">=" | "===" | "!==" | "==" | "!=";
91
+ literal: number | string | boolean;
92
+ }
93
+ /**
94
+ * Static reading of the handler behind an edge (TypeScript compiler API).
95
+ * Annotates runtime edges; never creates them.
96
+ */
97
+ type VizExplanation = {
98
+ status: "analyzed";
99
+ location: VizLocation;
100
+ registration: "on" | "onAny";
101
+ branches: VizBranch[];
102
+ payloadReads: string[];
103
+ contextReads: string[];
104
+ contextWrites: string[];
105
+ /** Reads `<actor>.state`: target depends on live state, which probing cannot see. */
106
+ readsActorState: boolean;
107
+ comparisons: VizComparison[];
108
+ literals: Array<number | string>;
109
+ } | {
110
+ status: "not_analyzed";
111
+ location: VizLocation | null;
112
+ reason: string;
113
+ /** First lines of the source, when there is any. */
114
+ snippet: string | null;
115
+ };
116
+ interface VizEdge {
117
+ id: string;
118
+ from: string;
119
+ /** Event id; empty string for the initial edge. */
120
+ event: string;
121
+ /** null = handler target depends on payload or context (undetermined). */
122
+ to: string | null;
123
+ kind: VizEdgeKind;
124
+ origin: VizEdgeOrigin;
125
+ emits: string[];
126
+ /** Sample-context names under which this edge was observed. */
127
+ contexts: string[];
128
+ /** null = no handler behind this edge (initial, effect) or analysis not run. */
129
+ explanation: VizExplanation | null;
130
+ /** Leading JSDoc of the `m.on` / `m.onAny` call. null = undocumented, no handler, or AST not run. */
131
+ doc: string | null;
132
+ /** Times taken in the supplied run (history or fuzz corpus replay). null = no run. */
133
+ coverage: VizCoverage | null;
134
+ /** Times taken by the supplied test sequences. null = no tests supplied. */
135
+ tested: VizCoverage | null;
136
+ }
137
+ interface VizMachine {
138
+ id: string;
139
+ initial: string | null;
140
+ /** Region paths, sorted. Empty for flat machines. */
141
+ regions: string[];
142
+ states: VizState[];
143
+ events: VizEvent[];
144
+ edges: VizEdge[];
145
+ /** null = not fuzzed. */
146
+ fuzz: VizFuzz | null;
147
+ }
148
+ type VizJson = null | boolean | number | string | VizJson[] | {
149
+ [key: string]: VizJson;
150
+ };
151
+ /** One input to a machine: an event (`send` for inputs, `inject` for internal) or a clock advance. */
152
+ type VizStep = {
153
+ kind: "event";
154
+ event: string;
155
+ payload: VizJson;
156
+ } | {
157
+ kind: "advance";
158
+ ms: number;
159
+ };
160
+ /** What one step did: the actor moved, handled it without moving, or had no handler. */
161
+ type VizLiveOutcome = "transition" | "stay" | "ignored";
162
+ interface VizLiveStep {
163
+ from: string;
164
+ event: string;
165
+ to: string;
166
+ outcome: VizLiveOutcome;
167
+ /** Names of the effects the step ran. */
168
+ effects: string[];
169
+ context: VizJson;
170
+ /** Message of a handler that threw; null when none did. */
171
+ error: string | null;
172
+ }
173
+ interface VizLiveTimer {
174
+ id: string;
175
+ ms: number;
176
+ deadline: number;
177
+ eventName: string | null;
178
+ }
179
+ /** Where a running actor is, after the steps in `steps`. */
180
+ interface VizLiveState {
181
+ state: string;
182
+ steps: VizStep[];
183
+ last: VizLiveStep | null;
184
+ timers: VizLiveTimer[];
185
+ context: VizJson;
186
+ }
187
+ /**
188
+ * A running actor behind the viewer. JSON in, JSON out, so viz needs no core:
189
+ * `@mantaq/inspect` implements it on the step function the fuzzer uses.
190
+ */
191
+ interface VizDriver {
192
+ reset(): VizLiveState;
193
+ step(step: VizStep): VizLiveState;
194
+ }
195
+ interface VizFuzzSequence {
196
+ /** Shrunk replayable sequence, starting from a fresh actor. */
197
+ steps: VizStep[];
198
+ /** Coverage features this sequence was kept for, e.g. `edge:a>go>b`. */
199
+ discovered: string[];
200
+ }
201
+ /** A handler that threw: the actor entered its error state. */
202
+ interface VizFuzzError {
203
+ from: string;
204
+ event: string;
205
+ message: string;
206
+ steps: VizStep[];
207
+ }
208
+ interface VizFuzz {
209
+ seed: number;
210
+ iterations: number;
211
+ corpus: VizFuzzSequence[];
212
+ errors: VizFuzzError[];
213
+ }
214
+ interface VizDocument {
215
+ schemaVersion: typeof VIZ_SCHEMA_VERSION;
216
+ machines: VizMachine[];
217
+ }
218
+ /** One problem found while reading a document, with a JSON-path-like location. */
219
+ interface VizIssue {
220
+ path: string;
221
+ message: string;
222
+ }
223
+ /**
224
+ * Read an untrusted value (parsed JSON) as an viz document. Returns every
225
+ * issue found, not just the first, so a broken file can be fixed in one pass.
226
+ */
227
+ declare function validateViz(value: unknown): Either<VizIssue[], VizDocument>;
228
+ /** Parse JSON text and validate it. JSON syntax errors become an issue at `$`. */
229
+ declare function parseViz(text: string): Either<VizIssue[], VizDocument>;
230
+ export { coveredByAny as A, VizLiveState as C, VizMachine as D, VizLocation as E, scopeOf as M, VizState as O, VizLiveOutcome as S, VizLiveTimer as T, VizExplanation as _, VIZ_SCHEMA_VERSION as a, VizFuzzSequence as b, VizComparison as c, VizDriver as d, VizEdge as f, VizEvent as g, VizEffect as h, ANY_STATE as i, isAnyState as j, VizStep as k, VizCoverage as l, VizEdgeOrigin as m, validateViz as n, VizBranch as o, VizEdgeKind as p, VizIssue as r, VizBranchTarget as s, parseViz as t, VizDocument as u, VizFuzz as v, VizLiveStep as w, VizJson as x, VizFuzzError as y };
@@ -0,0 +1,2 @@
1
+ import { a as ViewerOptions, i as STRUCTURE_BANNER, n as showIssues, o as mountViewer, r as FUZZ_NOTE, t as mountVizText } from "./embed-CISoaFIK.mjs";
2
+ export { FUZZ_NOTE, STRUCTURE_BANNER, type ViewerOptions, mountViewer, mountVizText, showIssues };
@@ -0,0 +1 @@
1
+ export { r as FUZZ_NOTE, i as STRUCTURE_BANNER, a as mountViewer, t as mountVizText, n as showIssues } from "./embed-QX7vRXoL.mjs";
package/package.json CHANGED
@@ -1,7 +1,46 @@
1
1
  {
2
2
  "name": "@mantaq/viz",
3
- "version": "0.0.0",
4
- "description": "Placeholder to enable npm trusted publishing.",
5
- "repository": { "type": "git", "url": "git+https://github.com/AndersCan/mantaq.git" },
6
- "license": "MIT"
7
- }
3
+ "version": "0.2.0",
4
+ "description": "Draw and walk a Mantaq machine in the browser, from a versioned, deterministic JSON document.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/AndersCan/mantaq",
9
+ "directory": "packages/viz"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src/viewer/viewer.css",
14
+ "src/viewer/page.css"
15
+ ],
16
+ "type": "module",
17
+ "exports": {
18
+ ".": "./dist/index.mjs",
19
+ "./viewer": "./dist/viewer.mjs",
20
+ "./viewer.css": "./src/viewer/viewer.css",
21
+ "./page.css": "./src/viewer/page.css",
22
+ "./page": "./dist/page.mjs",
23
+ "./standalone": "./dist/standalone.mjs",
24
+ "./live": "./dist/live.mjs",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "@dagrejs/dagre": "^3.1.2",
32
+ "@mantaq/utils": "0.0.4"
33
+ },
34
+ "devDependencies": {
35
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
36
+ "happy-dom": "^21.0.4",
37
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
38
+ "vite-plus": "1.4.0"
39
+ },
40
+ "scripts": {
41
+ "dev": "vp dev",
42
+ "build": "vp pack",
43
+ "test": "vp test",
44
+ "check": "vp check"
45
+ }
46
+ }
@@ -0,0 +1,12 @@
1
+ /* Standalone viewer page chrome (index.html). Embeds bring their own. */
2
+ body {
3
+ margin: 0;
4
+ padding: 16px;
5
+ font:
6
+ 14px/1.4 system-ui,
7
+ sans-serif;
8
+ }
9
+
10
+ .toolbar {
11
+ margin-bottom: 12px;
12
+ }