@markbrutx/promptbook-viewer 0.2.0 → 0.4.1
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/web/assets/index-DCrrFaqQ.css +1 -0
- package/dist/web/assets/index-_hu1t2tv.js +51 -0
- package/dist/web/index.html +2 -2
- package/dist/web/promptbook-logo.png +0 -0
- package/dist/web/promptbook-mini.png +0 -0
- package/package.json +8 -3
- package/src/index.ts +1 -0
- package/src/web/App.tsx +51 -40
- package/src/web/api.ts +46 -12
- package/src/web/colors.ts +3 -3
- package/src/web/components/Addons.tsx +95 -33
- package/src/web/components/FragmentView.tsx +2 -2
- package/src/web/main.tsx +3 -8
- package/src/web/mount.tsx +27 -0
- package/src/web/public/promptbook-logo.png +0 -0
- package/src/web/public/promptbook-mini.png +0 -0
- package/src/web/styles.css +253 -61
- package/src/web/types.ts +1 -1
- package/dist/web/assets/index-BCBuW76o.css +0 -1
- package/dist/web/assets/index-BwIAKPNq.js +0 -51
package/dist/web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>promptbook viewer</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-_hu1t2tv.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DCrrFaqQ.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markbrutx/promptbook-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Storybook-for-prompts: a local web app that renders a prompts folder via @markbrutx/promptbook-core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
".": {
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"import": "./dist/index.js"
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"./web": {
|
|
30
|
+
"types": "./src/web/mount.tsx",
|
|
31
|
+
"default": "./src/web/mount.tsx"
|
|
32
|
+
},
|
|
33
|
+
"./web/styles.css": "./src/web/styles.css"
|
|
29
34
|
},
|
|
30
35
|
"main": "./dist/index.js",
|
|
31
36
|
"types": "./dist/index.d.ts",
|
|
@@ -43,7 +48,7 @@
|
|
|
43
48
|
"check": "biome check ."
|
|
44
49
|
},
|
|
45
50
|
"dependencies": {
|
|
46
|
-
"@markbrutx/promptbook-core": "^0.
|
|
51
|
+
"@markbrutx/promptbook-core": "^0.4.1"
|
|
47
52
|
},
|
|
48
53
|
"devDependencies": {
|
|
49
54
|
"@biomejs/biome": "latest",
|
package/src/index.ts
CHANGED
package/src/web/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { Api } from "./api.js";
|
|
3
3
|
import { Addons } from "./components/Addons.js";
|
|
4
4
|
import { Canvas } from "./components/Canvas.js";
|
|
5
5
|
import { CodePromptView } from "./components/CodePromptView.js";
|
|
@@ -47,7 +47,17 @@ function controlKeys(composition: CompositionSummary | undefined): string[] {
|
|
|
47
47
|
return [...keys];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/** Props passed to the viewer's root component. */
|
|
51
|
+
export interface AppProps {
|
|
52
|
+
/** API surface the viewer talks to. The CLI viewer wires the default fetch
|
|
53
|
+
* implementation; the static demo site supplies an in-browser one. */
|
|
54
|
+
api: Api;
|
|
55
|
+
/** Subscribe to hot-reload events. When omitted, no live-reload wiring runs
|
|
56
|
+
* (e.g. the static-mount path). The unsubscribe callback is returned. */
|
|
57
|
+
subscribeReload?: (callback: (changedBook: string | undefined) => void) => () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function App({ api, subscribeReload }: AppProps) {
|
|
51
61
|
const [books, setBooks] = useState<WorkspaceBook[]>([]);
|
|
52
62
|
const [activeBook, setActiveBook] = useState<string | null>(null);
|
|
53
63
|
const [book, setBook] = useState<BookResponse | null>(null);
|
|
@@ -75,28 +85,34 @@ export function App() {
|
|
|
75
85
|
setBooks([]);
|
|
76
86
|
return [];
|
|
77
87
|
}
|
|
78
|
-
}, []);
|
|
88
|
+
}, [api.books]);
|
|
79
89
|
|
|
80
|
-
const loadAnnotations = useCallback(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const loadAnnotations = useCallback(
|
|
91
|
+
async (which: string | null) => {
|
|
92
|
+
try {
|
|
93
|
+
const { annotations: next } = await api.annotations(which);
|
|
94
|
+
setAnnotations(next);
|
|
95
|
+
} catch {
|
|
96
|
+
setAnnotations([]);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[api.annotations],
|
|
100
|
+
);
|
|
88
101
|
|
|
89
|
-
const loadBook = useCallback(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const loadBook = useCallback(
|
|
103
|
+
async (which: string | null) => {
|
|
104
|
+
try {
|
|
105
|
+
const next = await api.book(which);
|
|
106
|
+
setBook(next);
|
|
107
|
+
setError(null);
|
|
108
|
+
return next;
|
|
109
|
+
} catch (e) {
|
|
110
|
+
setError((e as Error).message);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[api.book],
|
|
115
|
+
);
|
|
100
116
|
|
|
101
117
|
// Discover the workspace's books, then activate the first one.
|
|
102
118
|
useEffect(() => {
|
|
@@ -127,25 +143,20 @@ export function App() {
|
|
|
127
143
|
}, [activeBook, loadBook, loadAnnotations]);
|
|
128
144
|
|
|
129
145
|
// Hot-reload: refetch the book list, and the active book's tree when it (or
|
|
130
|
-
// an unknown path) changed. The
|
|
131
|
-
//
|
|
146
|
+
// an unknown path) changed. The CLI viewer wires this to its SSE stream; the
|
|
147
|
+
// static demo site omits it entirely (no live reload). The selection persists.
|
|
132
148
|
useEffect(() => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
changed = (JSON.parse((event as MessageEvent).data) as { book?: string }).book;
|
|
138
|
-
} catch {
|
|
139
|
-
changed = undefined;
|
|
140
|
-
}
|
|
149
|
+
if (subscribeReload === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
return subscribeReload((changed) => {
|
|
141
153
|
void loadBooks();
|
|
142
154
|
const active = activeBookRef.current;
|
|
143
155
|
if (active !== null && (changed === undefined || changed === active)) {
|
|
144
156
|
void loadBook(active);
|
|
145
157
|
}
|
|
146
158
|
});
|
|
147
|
-
|
|
148
|
-
}, [loadBook, loadBooks]);
|
|
159
|
+
}, [loadBook, loadBooks, subscribeReload]);
|
|
149
160
|
|
|
150
161
|
const compositions = book?.compositions ?? [];
|
|
151
162
|
const selectedComposition =
|
|
@@ -176,7 +187,7 @@ export function App() {
|
|
|
176
187
|
setError((e as Error).message);
|
|
177
188
|
}
|
|
178
189
|
});
|
|
179
|
-
}, [book, selection, context, activeBook]);
|
|
190
|
+
}, [book, selection, context, activeBook, api.lint, api.resolve]);
|
|
180
191
|
|
|
181
192
|
useEffect(() => {
|
|
182
193
|
if (book === null || selection?.kind !== "fragment") {
|
|
@@ -187,7 +198,7 @@ export function App() {
|
|
|
187
198
|
.usedIn(activeBook, selection.id)
|
|
188
199
|
.then(setUsedIn)
|
|
189
200
|
.catch(() => setUsedIn(null));
|
|
190
|
-
}, [book, selection, activeBook]);
|
|
201
|
+
}, [book, selection, activeBook, api.usedIn]);
|
|
191
202
|
|
|
192
203
|
// Resolve the comparison variant for the Diff panel.
|
|
193
204
|
useEffect(() => {
|
|
@@ -200,7 +211,7 @@ export function App() {
|
|
|
200
211
|
.resolve(activeBook, selection.composition, ctx)
|
|
201
212
|
.then(setCompareResolved)
|
|
202
213
|
.catch(() => setCompareResolved(null));
|
|
203
|
-
}, [selection, compareVariant, selectedComposition, activeBook]);
|
|
214
|
+
}, [selection, compareVariant, selectedComposition, activeBook, api.resolve]);
|
|
204
215
|
|
|
205
216
|
const selectBook = useCallback((name: string) => {
|
|
206
217
|
setActiveBook(name);
|
|
@@ -238,7 +249,7 @@ export function App() {
|
|
|
238
249
|
});
|
|
239
250
|
await loadAnnotations(activeBook);
|
|
240
251
|
},
|
|
241
|
-
[selection, context, activeBook, loadAnnotations],
|
|
252
|
+
[selection, context, activeBook, loadAnnotations, api.annotate],
|
|
242
253
|
);
|
|
243
254
|
|
|
244
255
|
const resolveAnnotation = useCallback(
|
|
@@ -246,7 +257,7 @@ export function App() {
|
|
|
246
257
|
await api.resolveAnnotation(activeBook, id);
|
|
247
258
|
await loadAnnotations(activeBook);
|
|
248
259
|
},
|
|
249
|
-
[activeBook, loadAnnotations],
|
|
260
|
+
[activeBook, loadAnnotations, api.resolveAnnotation],
|
|
250
261
|
);
|
|
251
262
|
|
|
252
263
|
// Annotations belonging to exactly the variant on screen (composition + context).
|
|
@@ -293,7 +304,7 @@ export function App() {
|
|
|
293
304
|
title={selection.composition}
|
|
294
305
|
subtitle={selection.variant}
|
|
295
306
|
segments={resolved.segments}
|
|
296
|
-
|
|
307
|
+
{...(lint?.tokens !== undefined ? { tokens: lint.tokens } : {})}
|
|
297
308
|
annotations={variantAnnotations}
|
|
298
309
|
onAnnotate={addAnnotation}
|
|
299
310
|
/>
|
package/src/web/api.ts
CHANGED
|
@@ -10,6 +10,22 @@ import type {
|
|
|
10
10
|
UsedInResponse,
|
|
11
11
|
} from "./types.js";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The viewer's UI talks to the world through this small interface only. The
|
|
15
|
+
* CLI viewer wires the fetch-based implementation below; the static demo site
|
|
16
|
+
* passes an in-browser implementation that resolves against a bundled book.
|
|
17
|
+
*/
|
|
18
|
+
export interface Api {
|
|
19
|
+
books(): Promise<BooksResponse>;
|
|
20
|
+
book(book: string | null): Promise<BookResponse>;
|
|
21
|
+
resolve(book: string | null, prompt: string, context: Context): Promise<ResolveResponse>;
|
|
22
|
+
lint(book: string | null, prompt: string, context: Context): Promise<LintResponse>;
|
|
23
|
+
usedIn(book: string | null, fragmentId: string): Promise<UsedInResponse>;
|
|
24
|
+
annotations(book: string | null): Promise<AnnotationsResponse>;
|
|
25
|
+
annotate(book: string | null, body: AnnotateRequest): Promise<Annotation>;
|
|
26
|
+
resolveAnnotation(book: string | null, id: string): Promise<{ id: string; removed: boolean }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
async function getJson<T>(path: string): Promise<T> {
|
|
14
30
|
const res = await fetch(path);
|
|
15
31
|
if (!res.ok) {
|
|
@@ -19,11 +35,14 @@ async function getJson<T>(path: string): Promise<T> {
|
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
async function sendJson<T>(method: "POST" | "DELETE", path: string, body?: unknown): Promise<T> {
|
|
22
|
-
const
|
|
38
|
+
const init: RequestInit = {
|
|
23
39
|
method,
|
|
24
40
|
headers: { "content-type": "application/json" },
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
};
|
|
42
|
+
if (body !== undefined) {
|
|
43
|
+
init.body = JSON.stringify(body);
|
|
44
|
+
}
|
|
45
|
+
const res = await fetch(path, init);
|
|
27
46
|
if (!res.ok) {
|
|
28
47
|
const payload = (await res.json().catch(() => ({}))) as { error?: string };
|
|
29
48
|
throw new Error(payload.error ?? `${path} → ${res.status}`);
|
|
@@ -40,21 +59,36 @@ function scoped(path: string, book: string | null): string {
|
|
|
40
59
|
return `${path}${sep}book=${encodeURIComponent(book)}`;
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
/** The fetch-based default API the CLI viewer uses. */
|
|
63
|
+
export const api: Api = {
|
|
44
64
|
books: () => getJson<BooksResponse>("/api/books"),
|
|
45
|
-
book: (book
|
|
46
|
-
resolve: (book
|
|
65
|
+
book: (book) => getJson<BookResponse>(scoped("/api/book", book)),
|
|
66
|
+
resolve: (book, prompt, context) =>
|
|
47
67
|
sendJson<ResolveResponse>("POST", scoped("/api/resolve", book), { prompt, context }),
|
|
48
|
-
lint: (book
|
|
68
|
+
lint: (book, prompt, context) =>
|
|
49
69
|
sendJson<LintResponse>("POST", scoped("/api/lint", book), { prompt, context }),
|
|
50
|
-
usedIn: (book
|
|
70
|
+
usedIn: (book, fragmentId) =>
|
|
51
71
|
getJson<UsedInResponse>(scoped(`/api/used-in/${encodeURIComponent(fragmentId)}`, book)),
|
|
52
|
-
annotations: (book
|
|
53
|
-
annotate: (book
|
|
54
|
-
|
|
55
|
-
resolveAnnotation: (book: string | null, id: string) =>
|
|
72
|
+
annotations: (book) => getJson<AnnotationsResponse>(scoped("/api/annotations", book)),
|
|
73
|
+
annotate: (book, body) => sendJson<Annotation>("POST", scoped("/api/annotate", book), body),
|
|
74
|
+
resolveAnnotation: (book, id) =>
|
|
56
75
|
sendJson<{ id: string; removed: boolean }>(
|
|
57
76
|
"DELETE",
|
|
58
77
|
scoped(`/api/annotations/${encodeURIComponent(id)}`, book),
|
|
59
78
|
),
|
|
60
79
|
};
|
|
80
|
+
|
|
81
|
+
/** Subscribe to the CLI viewer's SSE hot-reload stream. */
|
|
82
|
+
export function subscribeFetchReload(callback: (changedBook: string | undefined) => void): () => void {
|
|
83
|
+
const source = new EventSource("/api/events");
|
|
84
|
+
source.addEventListener("reload", (event) => {
|
|
85
|
+
let changed: string | undefined;
|
|
86
|
+
try {
|
|
87
|
+
changed = (JSON.parse((event as MessageEvent).data) as { book?: string }).book;
|
|
88
|
+
} catch {
|
|
89
|
+
changed = undefined;
|
|
90
|
+
}
|
|
91
|
+
callback(changed);
|
|
92
|
+
});
|
|
93
|
+
return () => source.close();
|
|
94
|
+
}
|
package/src/web/colors.ts
CHANGED
|
@@ -10,12 +10,12 @@ function hueOf(id: string): number {
|
|
|
10
10
|
return (hash + 360) % 360;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** Soft background tint for a fragment's segment. */
|
|
13
|
+
/** Soft background tint for a fragment's segment. Dark mode native. */
|
|
14
14
|
export function fragmentColor(id: string): string {
|
|
15
|
-
return `hsl(${hueOf(id)}deg
|
|
15
|
+
return `hsl(${hueOf(id)}deg 55% 14%)`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Saturated variant of {@link fragmentColor} for borders/legend swatches. */
|
|
19
19
|
export function fragmentAccent(id: string): string {
|
|
20
|
-
return `hsl(${hueOf(id)}deg
|
|
20
|
+
return `hsl(${hueOf(id)}deg 70% 62%)`;
|
|
21
21
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { kvLabel } from "../format.js";
|
|
2
|
-
import type { LintResponse, Trace, When } from "../types.js";
|
|
3
|
+
import type { LintResponse, RuleTrace, Trace, When } from "../types.js";
|
|
3
4
|
|
|
4
5
|
interface AddonsProps {
|
|
5
6
|
trace: Trace;
|
|
@@ -13,7 +14,7 @@ function whenLabel(when: When): string {
|
|
|
13
14
|
/** Lint findings panel: severity-tagged messages with the fragment/rule origin. */
|
|
14
15
|
function LintPanel({ lint }: { lint: LintResponse | null }) {
|
|
15
16
|
if (lint === null) {
|
|
16
|
-
return <p className="muted"
|
|
17
|
+
return <p className="muted">no data</p>;
|
|
17
18
|
}
|
|
18
19
|
if (lint.findings.length === 0) {
|
|
19
20
|
return <p className="muted">No findings ({lint.tokens} tokens).</p>;
|
|
@@ -32,46 +33,107 @@ function LintPanel({ lint }: { lint: LintResponse | null }) {
|
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
function FiredRule({ rule }: { rule: RuleTrace }) {
|
|
37
|
+
return (
|
|
38
|
+
<li className="rule rule-fired">
|
|
39
|
+
<span className="rule-mark" aria-hidden>
|
|
40
|
+
✓
|
|
41
|
+
</span>
|
|
42
|
+
<div className="rule-body">
|
|
43
|
+
<div className="rule-head">
|
|
44
|
+
<span className="rule-action">{rule.action}</span>
|
|
45
|
+
<span className="rule-when">when {whenLabel(rule.when)}</span>
|
|
46
|
+
</div>
|
|
47
|
+
{rule.effect ? <p className="rule-effect">{rule.effect}</p> : null}
|
|
48
|
+
</div>
|
|
49
|
+
</li>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SkippedRule({ rule }: { rule: RuleTrace }) {
|
|
54
|
+
return (
|
|
55
|
+
<li className="rule rule-skipped">
|
|
56
|
+
<span className="rule-mark" aria-hidden>
|
|
57
|
+
·
|
|
58
|
+
</span>
|
|
59
|
+
<div className="rule-body">
|
|
60
|
+
<div className="rule-head">
|
|
61
|
+
<span className="rule-action">{rule.action}</span>
|
|
62
|
+
<span className="rule-when">when {whenLabel(rule.when)}</span>
|
|
63
|
+
</div>
|
|
64
|
+
{rule.reason ? <p className="rule-reason">{rule.reason}</p> : null}
|
|
65
|
+
</div>
|
|
66
|
+
</li>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Explain trace panel: which rules fired, what they did, and (collapsed) the
|
|
71
|
+
* rules that did not match — kept out of the way so the trace reads as a
|
|
72
|
+
* trace, not as a list of errors. */
|
|
36
73
|
function ExplainPanel({ trace }: { trace: Trace }) {
|
|
74
|
+
const fired = trace.rules.filter((r) => r.fired);
|
|
75
|
+
const skipped = trace.rules.filter((r) => !r.fired);
|
|
76
|
+
const [showSkipped, setShowSkipped] = useState(false);
|
|
77
|
+
|
|
37
78
|
return (
|
|
38
79
|
<div className="explain">
|
|
39
|
-
<h4>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
<h4>Fired rules ({fired.length})</h4>
|
|
81
|
+
{fired.length === 0 ? (
|
|
82
|
+
<p className="muted small">No rules matched this context, so the base order is the final order.</p>
|
|
83
|
+
) : (
|
|
84
|
+
<ul className="rules">
|
|
85
|
+
{fired.map((rule) => (
|
|
86
|
+
<FiredRule key={rule.index} rule={rule} />
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{skipped.length > 0 ? (
|
|
92
|
+
<details
|
|
93
|
+
className="skipped-block"
|
|
94
|
+
open={showSkipped}
|
|
95
|
+
onToggle={(event) => setShowSkipped((event.target as HTMLDetailsElement).open)}
|
|
96
|
+
>
|
|
97
|
+
<summary className="skipped-summary">
|
|
98
|
+
{skipped.length} rule{skipped.length === 1 ? "" : "s"} didn't match
|
|
99
|
+
</summary>
|
|
100
|
+
<ul className="rules">
|
|
101
|
+
{skipped.map((rule) => (
|
|
102
|
+
<SkippedRule key={rule.index} rule={rule} />
|
|
103
|
+
))}
|
|
104
|
+
</ul>
|
|
105
|
+
</details>
|
|
106
|
+
) : null}
|
|
52
107
|
|
|
53
108
|
<h4>Final order</h4>
|
|
54
109
|
<p className="order">{trace.finalOrder.join(" → ") || "(empty)"}</p>
|
|
55
110
|
|
|
56
|
-
{trace.replaced.length > 0 ? (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
{trace.replaced.length > 0 || trace.added.length > 0 || trace.forbidden.length > 0 ? (
|
|
112
|
+
<dl className="effects">
|
|
113
|
+
{trace.replaced.length > 0 ? (
|
|
114
|
+
<>
|
|
115
|
+
<dt>replaced</dt>
|
|
116
|
+
<dd>{trace.replaced.map((r) => `${r.from} → ${r.to}`).join(", ")}</dd>
|
|
117
|
+
</>
|
|
118
|
+
) : null}
|
|
119
|
+
{trace.added.length > 0 ? (
|
|
120
|
+
<>
|
|
121
|
+
<dt>added</dt>
|
|
122
|
+
<dd>{trace.added.map((a) => a.id).join(", ")}</dd>
|
|
123
|
+
</>
|
|
124
|
+
) : null}
|
|
125
|
+
{trace.forbidden.length > 0 ? (
|
|
126
|
+
<>
|
|
127
|
+
<dt>forbidden</dt>
|
|
128
|
+
<dd>{trace.forbidden.map((f) => f.id).join(", ")}</dd>
|
|
129
|
+
</>
|
|
130
|
+
) : null}
|
|
131
|
+
</dl>
|
|
70
132
|
) : null}
|
|
71
133
|
|
|
72
134
|
{trace.unmatchedAxes.length > 0 ? (
|
|
73
|
-
<p className="
|
|
74
|
-
<strong>
|
|
135
|
+
<p className="notice">
|
|
136
|
+
<strong>Context axes some rule names but none matched:</strong>{" "}
|
|
75
137
|
{trace.unmatchedAxes.map((axis) => `${axis.key}=${axis.value}`).join(", ")}
|
|
76
138
|
</p>
|
|
77
139
|
) : null}
|
|
@@ -94,7 +156,7 @@ export function Addons({ trace, lint }: AddonsProps) {
|
|
|
94
156
|
<aside className="addons">
|
|
95
157
|
<section>
|
|
96
158
|
<h3>Tokens</h3>
|
|
97
|
-
<p className="tokens">{lint ? `~${lint.tokens}` : "
|
|
159
|
+
<p className="tokens">{lint ? `~${lint.tokens}` : "·"}</p>
|
|
98
160
|
</section>
|
|
99
161
|
<section>
|
|
100
162
|
<h3>Lint</h3>
|
|
@@ -18,13 +18,13 @@ export function FragmentView({ fragment, usedIn, onSelectVariant }: FragmentView
|
|
|
18
18
|
{fragment.id}
|
|
19
19
|
</h1>
|
|
20
20
|
<p className="muted">
|
|
21
|
-
{fragment.kind ?? "
|
|
21
|
+
{fragment.kind ?? "·"}
|
|
22
22
|
{fragment.tags.length > 0 ? ` · ${fragment.tags.join(", ")}` : ""} · {fragment.sourceFile}
|
|
23
23
|
</p>
|
|
24
24
|
</div>
|
|
25
25
|
</header>
|
|
26
26
|
|
|
27
|
-
<pre className="segment" style={{ background: "
|
|
27
|
+
<pre className="segment" style={{ background: "var(--panel)" }}>
|
|
28
28
|
{fragment.body}
|
|
29
29
|
</pre>
|
|
30
30
|
|
package/src/web/main.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { App } from "./App.js";
|
|
1
|
+
import { api, subscribeFetchReload } from "./api.js";
|
|
2
|
+
import { mountWebApp } from "./mount.js";
|
|
4
3
|
import "./styles.css";
|
|
5
4
|
|
|
6
5
|
const container = document.getElementById("root");
|
|
@@ -8,8 +7,4 @@ if (container === null) {
|
|
|
8
7
|
throw new Error("missing #root element");
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
<StrictMode>
|
|
13
|
-
<App />
|
|
14
|
-
</StrictMode>,
|
|
15
|
-
);
|
|
10
|
+
mountWebApp({ container, api, subscribeReload: subscribeFetchReload });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { App, type AppProps } from "./App.js";
|
|
4
|
+
|
|
5
|
+
export type { App, AppProps } from "./App.js";
|
|
6
|
+
export type { Api } from "./api.js";
|
|
7
|
+
|
|
8
|
+
/** Arguments to {@link mountWebApp}. */
|
|
9
|
+
export interface MountOptions extends AppProps {
|
|
10
|
+
/** DOM node to mount the React tree into. */
|
|
11
|
+
container: Element | DocumentFragment;
|
|
12
|
+
/** Wrap in `<StrictMode>`. Defaults to true. */
|
|
13
|
+
strict?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Mount the viewer UI into `container` with the supplied API. */
|
|
17
|
+
export function mountWebApp(options: MountOptions): { unmount: () => void; root: Root } {
|
|
18
|
+
const { container, api, subscribeReload, strict = true } = options;
|
|
19
|
+
const root = createRoot(container);
|
|
20
|
+
const appProps: AppProps = subscribeReload !== undefined ? { api, subscribeReload } : { api };
|
|
21
|
+
const tree = <App {...appProps} />;
|
|
22
|
+
root.render(strict ? <StrictMode>{tree}</StrictMode> : tree);
|
|
23
|
+
return {
|
|
24
|
+
root,
|
|
25
|
+
unmount: () => root.unmount(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
Binary file
|
|
Binary file
|