@flow-like/widget-bundler 0.1.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/README.md +388 -0
- package/package.json +40 -0
- package/src/add.ts +109 -0
- package/src/bundle-format.ts +104 -0
- package/src/cli.ts +156 -0
- package/src/contract-types.ts +759 -0
- package/src/csp-reason.ts +307 -0
- package/src/csp-source.ts +361 -0
- package/src/csp.ts +54 -0
- package/src/dev/contract-endpoint.ts +103 -0
- package/src/dev/form-model.ts +63 -0
- package/src/dev/harness-html.ts +864 -0
- package/src/dev/server.ts +297 -0
- package/src/extract.ts +1620 -0
- package/src/generated/widget-source-data.ts +982 -0
- package/src/html.ts +60 -0
- package/src/index.ts +17 -0
- package/src/inline.ts +247 -0
- package/src/pack.ts +502 -0
- package/src/psl.ts +145 -0
- package/src/validate-cmd.ts +268 -0
- package/src/vite.ts +109 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DARK_TOKENS,
|
|
3
|
+
DEFAULT_LIGHT_TOKENS,
|
|
4
|
+
FLW_PROTOCOL,
|
|
5
|
+
} from "@flow-like/widget-sdk";
|
|
6
|
+
import { escapeHtmlAttr } from "../html";
|
|
7
|
+
|
|
8
|
+
export interface HarnessWidget {
|
|
9
|
+
group: string;
|
|
10
|
+
id: string;
|
|
11
|
+
/** Full child dev-server document URL for the sandboxed iframe */
|
|
12
|
+
entryUrl: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface HarnessData {
|
|
16
|
+
protocol: string;
|
|
17
|
+
widgets: HarnessWidget[];
|
|
18
|
+
themes: {
|
|
19
|
+
light: Record<string, string>;
|
|
20
|
+
dark: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function embedJson(data: HarnessData): string {
|
|
25
|
+
return JSON.stringify(data).replace(/</g, "\\u003c");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sidebarMarkup(widgets: HarnessWidget[]): string {
|
|
29
|
+
const byGroup = new Map<string, HarnessWidget[]>();
|
|
30
|
+
for (const widget of widgets) {
|
|
31
|
+
const list = byGroup.get(widget.group) ?? [];
|
|
32
|
+
list.push(widget);
|
|
33
|
+
byGroup.set(widget.group, list);
|
|
34
|
+
}
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
for (const group of [...byGroup.keys()].sort()) {
|
|
37
|
+
parts.push(`<div class="group-name">${escapeHtmlAttr(group)}</div>`);
|
|
38
|
+
for (const widget of byGroup.get(group) ?? []) {
|
|
39
|
+
parts.push(
|
|
40
|
+
`<button type="button" class="widget-item" data-key="${escapeHtmlAttr(
|
|
41
|
+
`${widget.group}/${widget.id}`,
|
|
42
|
+
)}">${escapeHtmlAttr(widget.id)}</button>`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return parts.join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const HARNESS_CSS = `
|
|
50
|
+
:root {
|
|
51
|
+
--h-bg: #f6f6f7; --h-fg: #1c1c1f; --h-panel: #ffffff; --h-border: #d9d9de;
|
|
52
|
+
--h-muted: #6b6b74; --h-accent: #e05d38; --h-ok: #1a7f37; --h-err: #c62828;
|
|
53
|
+
--h-warn: #9a6700; --h-mono: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
54
|
+
}
|
|
55
|
+
body.dark {
|
|
56
|
+
--h-bg: #17181c; --h-fg: #e8e8ea; --h-panel: #1f2026; --h-border: #34353d;
|
|
57
|
+
--h-muted: #9a9aa4; --h-ok: #4caf72; --h-err: #ef6a6a; --h-warn: #d4a72c;
|
|
58
|
+
}
|
|
59
|
+
* { box-sizing: border-box; }
|
|
60
|
+
html, body { height: 100%; }
|
|
61
|
+
body {
|
|
62
|
+
margin: 0; font: 13px/1.45 system-ui, sans-serif;
|
|
63
|
+
background: var(--h-bg); color: var(--h-fg);
|
|
64
|
+
}
|
|
65
|
+
button, select, input, textarea {
|
|
66
|
+
font: inherit; color: inherit; background: var(--h-panel);
|
|
67
|
+
border: 1px solid var(--h-border); border-radius: 4px; padding: 3px 8px;
|
|
68
|
+
}
|
|
69
|
+
button { cursor: pointer; }
|
|
70
|
+
button:hover { border-color: var(--h-accent); }
|
|
71
|
+
#app { display: flex; height: 100vh; }
|
|
72
|
+
#sidebar {
|
|
73
|
+
width: 220px; flex: none; border-right: 1px solid var(--h-border);
|
|
74
|
+
background: var(--h-panel); padding: 12px; overflow-y: auto;
|
|
75
|
+
}
|
|
76
|
+
#sidebar h1 { font-size: 14px; margin: 0 0 12px; }
|
|
77
|
+
#sidebar h1 span { color: var(--h-muted); font-weight: normal; }
|
|
78
|
+
.group-name {
|
|
79
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
|
|
80
|
+
color: var(--h-muted); margin: 10px 0 4px;
|
|
81
|
+
}
|
|
82
|
+
.widget-item {
|
|
83
|
+
display: block; width: 100%; text-align: left; border: none;
|
|
84
|
+
background: none; padding: 5px 8px; border-radius: 4px; margin: 1px 0;
|
|
85
|
+
}
|
|
86
|
+
.widget-item:hover { background: var(--h-bg); }
|
|
87
|
+
.widget-item.active { background: var(--h-accent); color: #fff; }
|
|
88
|
+
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
89
|
+
#toolbar {
|
|
90
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
91
|
+
padding: 8px 12px; border-bottom: 1px solid var(--h-border);
|
|
92
|
+
background: var(--h-panel);
|
|
93
|
+
}
|
|
94
|
+
#widget-title { font-weight: 600; }
|
|
95
|
+
#status { color: var(--h-muted); }
|
|
96
|
+
#status.ok { color: var(--h-ok); }
|
|
97
|
+
#status.err { color: var(--h-err); }
|
|
98
|
+
.spacer { flex: 1; }
|
|
99
|
+
.control { display: inline-flex; align-items: center; gap: 5px; color: var(--h-muted); }
|
|
100
|
+
#height-display { font-family: var(--h-mono); font-size: 12px; }
|
|
101
|
+
#content { flex: 1; display: flex; min-height: 0; }
|
|
102
|
+
#stage-wrap { flex: 1; overflow: auto; padding: 16px; }
|
|
103
|
+
#stage { width: 100%; margin: 0 auto; transition: width 0.15s ease; }
|
|
104
|
+
#stage iframe {
|
|
105
|
+
width: 100%; border: 1px dashed var(--h-border); border-radius: 6px;
|
|
106
|
+
background: #fff; display: block;
|
|
107
|
+
}
|
|
108
|
+
body.dark #stage iframe { background: #17181c; }
|
|
109
|
+
#panels {
|
|
110
|
+
width: 380px; flex: none; overflow-y: auto; padding: 10px;
|
|
111
|
+
border-left: 1px solid var(--h-border); background: var(--h-panel);
|
|
112
|
+
}
|
|
113
|
+
#panels details { border-bottom: 1px solid var(--h-border); padding: 6px 0; }
|
|
114
|
+
#panels summary {
|
|
115
|
+
cursor: pointer; font-weight: 600; padding: 4px 0;
|
|
116
|
+
display: flex; align-items: center; gap: 8px;
|
|
117
|
+
}
|
|
118
|
+
#panels summary button { font-size: 11px; padding: 1px 6px; margin-left: auto; }
|
|
119
|
+
.empty { color: var(--h-muted); padding: 4px 0; }
|
|
120
|
+
.field { margin: 8px 0; }
|
|
121
|
+
.field-label { display: block; font-weight: 600; margin-bottom: 2px; }
|
|
122
|
+
.field-desc { color: var(--h-muted); font-size: 12px; margin-bottom: 3px; }
|
|
123
|
+
.field input[type="text"], .field input[type="number"], .field select,
|
|
124
|
+
.field textarea { width: 100%; }
|
|
125
|
+
.field textarea { font-family: var(--h-mono); font-size: 12px; }
|
|
126
|
+
.field textarea.bad { border-color: var(--h-err); }
|
|
127
|
+
.field-error { color: var(--h-err); font-size: 12px; min-height: 0; }
|
|
128
|
+
.fixture-btn { margin: 2px 4px 2px 0; }
|
|
129
|
+
#event-log, #trace {
|
|
130
|
+
max-height: 220px; overflow-y: auto; font-family: var(--h-mono);
|
|
131
|
+
font-size: 11px; background: var(--h-bg); border-radius: 4px; padding: 6px;
|
|
132
|
+
}
|
|
133
|
+
.log-entry { border-bottom: 1px solid var(--h-border); padding: 3px 0; }
|
|
134
|
+
.log-entry.invalid .log-name { color: var(--h-err); }
|
|
135
|
+
.log-entry.meta .log-name { color: var(--h-muted); }
|
|
136
|
+
.log-head { display: flex; gap: 8px; align-items: baseline; }
|
|
137
|
+
.log-time { color: var(--h-muted); }
|
|
138
|
+
.log-name { font-weight: 600; }
|
|
139
|
+
.log-flag {
|
|
140
|
+
color: #fff; background: var(--h-err); border-radius: 3px;
|
|
141
|
+
padding: 0 4px; font-size: 10px;
|
|
142
|
+
}
|
|
143
|
+
.log-payload { margin: 2px 0; white-space: pre-wrap; word-break: break-all; }
|
|
144
|
+
.log-error { color: var(--h-err); }
|
|
145
|
+
.trace-line { white-space: pre-wrap; word-break: break-all; }
|
|
146
|
+
.trace-in { color: var(--h-ok); }
|
|
147
|
+
.trace-out { color: var(--h-accent); }
|
|
148
|
+
.trace-note { color: var(--h-warn); }
|
|
149
|
+
#query-panel { display: flex; flex-direction: column; gap: 6px; padding: 6px 0; }
|
|
150
|
+
#query-args { font-family: var(--h-mono); font-size: 12px; }
|
|
151
|
+
#query-result {
|
|
152
|
+
font-family: var(--h-mono); font-size: 12px; white-space: pre-wrap;
|
|
153
|
+
word-break: break-all; margin: 0; min-height: 18px;
|
|
154
|
+
}
|
|
155
|
+
#query-result.ok { color: var(--h-ok); }
|
|
156
|
+
#query-result.err { color: var(--h-err); }
|
|
157
|
+
#query-result.warn { color: var(--h-warn); }
|
|
158
|
+
.token-row { display: flex; gap: 6px; align-items: center; margin: 3px 0; }
|
|
159
|
+
.token-name { font-family: var(--h-mono); font-size: 11px; flex: none; width: 150px; }
|
|
160
|
+
.token-row input { flex: 1; font-family: var(--h-mono); font-size: 11px; }
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
// The page script is plain browser JS (no framework, no external assets). It
|
|
164
|
+
// is the real flw/1 host: nonce handshake, init on hello/load, props:update,
|
|
165
|
+
// theme:change, query round-trips, event/resize/value:changed handling.
|
|
166
|
+
// No template literals inside — the file itself is a TS template literal.
|
|
167
|
+
const HARNESS_JS = `
|
|
168
|
+
"use strict";
|
|
169
|
+
(function () {
|
|
170
|
+
var H = window.__HARNESS__;
|
|
171
|
+
var TRACE_CAP = 300;
|
|
172
|
+
var LOG_CAP = 200;
|
|
173
|
+
|
|
174
|
+
var state = {
|
|
175
|
+
widgets: H.widgets.slice(),
|
|
176
|
+
selected: null,
|
|
177
|
+
contract: null,
|
|
178
|
+
formModel: [],
|
|
179
|
+
fixtures: {},
|
|
180
|
+
props: {},
|
|
181
|
+
session: null,
|
|
182
|
+
mode: "light",
|
|
183
|
+
overrides: { light: {}, dark: {} },
|
|
184
|
+
preview: false,
|
|
185
|
+
pending: {}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function $(id) { return document.getElementById(id); }
|
|
189
|
+
|
|
190
|
+
function make(tag, className, text) {
|
|
191
|
+
var node = document.createElement(tag);
|
|
192
|
+
if (className) node.className = className;
|
|
193
|
+
if (text !== undefined) node.textContent = text;
|
|
194
|
+
return node;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function uid() {
|
|
198
|
+
return window.crypto && crypto.randomUUID
|
|
199
|
+
? crypto.randomUUID()
|
|
200
|
+
: "id-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function timestamp() {
|
|
204
|
+
var d = new Date();
|
|
205
|
+
function pad(n, w) { return String(n).padStart(w || 2, "0"); }
|
|
206
|
+
return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" +
|
|
207
|
+
pad(d.getSeconds()) + "." + pad(d.getMilliseconds(), 3);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function shortJson(value) {
|
|
211
|
+
var text;
|
|
212
|
+
try { text = JSON.stringify(value); } catch (e) { text = String(value); }
|
|
213
|
+
if (text === undefined) text = "undefined";
|
|
214
|
+
return text.length > 600 ? text.slice(0, 600) + "\\u2026" : text;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function appendCapped(container, node, cap) {
|
|
218
|
+
container.appendChild(node);
|
|
219
|
+
while (container.childNodes.length > cap) container.removeChild(container.firstChild);
|
|
220
|
+
container.scrollTop = container.scrollHeight;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function trace(dir, type, payload) {
|
|
224
|
+
var line = make("div", "trace-line " + (dir === "in" ? "trace-in" : "trace-out"),
|
|
225
|
+
timestamp() + " " + (dir === "in" ? "\\u2190" : "\\u2192") + " " + type + " " + shortJson(payload));
|
|
226
|
+
appendCapped($("trace"), line, TRACE_CAP);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function traceNote(text) {
|
|
230
|
+
appendCapped($("trace"), make("div", "trace-line trace-note", timestamp() + " \\u00b7 " + text), TRACE_CAP);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------- minimal contract schema validation (mirrors the SDK subset) ----------
|
|
234
|
+
function typeOk(t, v) {
|
|
235
|
+
switch (t) {
|
|
236
|
+
case "string": return typeof v === "string";
|
|
237
|
+
case "number": return typeof v === "number";
|
|
238
|
+
case "integer": return typeof v === "number" && v % 1 === 0;
|
|
239
|
+
case "boolean": return typeof v === "boolean";
|
|
240
|
+
case "null": return v === null;
|
|
241
|
+
case "array": return Array.isArray(v);
|
|
242
|
+
case "object": return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
243
|
+
default: return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function checkSchema(schema, value, path, errors) {
|
|
248
|
+
if (!schema || typeof schema !== "object") return errors;
|
|
249
|
+
var t = schema.type;
|
|
250
|
+
if (typeof t === "string" && !typeOk(t, value)) {
|
|
251
|
+
errors.push(path + ": expected " + t + ", got " + (value === null ? "null" : Array.isArray(value) ? "array" : typeof value));
|
|
252
|
+
return errors;
|
|
253
|
+
}
|
|
254
|
+
if (Array.isArray(t) && !t.some(function (x) { return typeOk(x, value); })) {
|
|
255
|
+
errors.push(path + ": expected one of [" + t.join(", ") + "]");
|
|
256
|
+
return errors;
|
|
257
|
+
}
|
|
258
|
+
if (Array.isArray(schema.enum) && !schema.enum.some(function (x) { return JSON.stringify(x) === JSON.stringify(value); })) {
|
|
259
|
+
errors.push(path + ": not one of the allowed enum values");
|
|
260
|
+
}
|
|
261
|
+
if ("const" in schema && JSON.stringify(schema.const) !== JSON.stringify(value)) {
|
|
262
|
+
errors.push(path + ": value does not equal const");
|
|
263
|
+
}
|
|
264
|
+
if (typeof value === "number") {
|
|
265
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push(path + ": " + value + " < minimum " + schema.minimum);
|
|
266
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push(path + ": " + value + " > maximum " + schema.maximum);
|
|
267
|
+
}
|
|
268
|
+
if (Array.isArray(value) && schema.items && !Array.isArray(schema.items)) {
|
|
269
|
+
value.forEach(function (item, i) { checkSchema(schema.items, item, path + "[" + i + "]", errors); });
|
|
270
|
+
}
|
|
271
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
272
|
+
(Array.isArray(schema.required) ? schema.required : []).forEach(function (key) {
|
|
273
|
+
if (!(key in value)) errors.push(path + ": missing required property '" + key + "'");
|
|
274
|
+
});
|
|
275
|
+
var props = schema.properties || {};
|
|
276
|
+
Object.keys(props).forEach(function (key) {
|
|
277
|
+
if (key in value) checkSchema(props[key], value[key], path + "." + key, errors);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (Array.isArray(schema.anyOf) && !schema.anyOf.some(function (sub) { return checkSchema(sub, value, path, []).length === 0; })) {
|
|
281
|
+
errors.push(path + ": value matches no schema in anyOf");
|
|
282
|
+
}
|
|
283
|
+
return errors;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------- theme ----------
|
|
287
|
+
function themeTokens() {
|
|
288
|
+
var tokens = {};
|
|
289
|
+
var base = state.mode === "dark" ? H.themes.dark : H.themes.light;
|
|
290
|
+
Object.keys(base).forEach(function (k) { tokens[k] = base[k]; });
|
|
291
|
+
var overrides = state.overrides[state.mode];
|
|
292
|
+
Object.keys(overrides).forEach(function (k) { tokens[k] = overrides[k]; });
|
|
293
|
+
return tokens;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function currentTheme() { return { mode: state.mode, tokens: themeTokens() }; }
|
|
297
|
+
|
|
298
|
+
function sendTheme() { send("theme:change", currentTheme()); }
|
|
299
|
+
|
|
300
|
+
function setMode(mode) {
|
|
301
|
+
state.mode = mode;
|
|
302
|
+
document.body.classList.toggle("dark", mode === "dark");
|
|
303
|
+
$("theme-toggle").textContent = mode;
|
|
304
|
+
renderTokenEditor();
|
|
305
|
+
if (state.session) sendTheme();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderTokenEditor() {
|
|
309
|
+
var box = $("token-editor");
|
|
310
|
+
box.textContent = "";
|
|
311
|
+
var tokens = themeTokens();
|
|
312
|
+
Object.keys(tokens).forEach(function (name) {
|
|
313
|
+
var row = make("div", "token-row");
|
|
314
|
+
row.appendChild(make("span", "token-name", name));
|
|
315
|
+
var input = document.createElement("input");
|
|
316
|
+
input.type = "text";
|
|
317
|
+
input.value = tokens[name];
|
|
318
|
+
input.addEventListener("change", function () {
|
|
319
|
+
state.overrides[state.mode][name] = input.value;
|
|
320
|
+
sendTheme();
|
|
321
|
+
});
|
|
322
|
+
row.appendChild(input);
|
|
323
|
+
box.appendChild(row);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------- flw/1 host ----------
|
|
328
|
+
function send(type, payload) {
|
|
329
|
+
var s = state.session;
|
|
330
|
+
if (!s || !s.iframe.contentWindow) return;
|
|
331
|
+
trace("out", type, payload);
|
|
332
|
+
s.iframe.contentWindow.postMessage({
|
|
333
|
+
protocol: H.protocol,
|
|
334
|
+
nonce: s.nonce,
|
|
335
|
+
instanceId: s.instanceId,
|
|
336
|
+
type: type,
|
|
337
|
+
payload: payload
|
|
338
|
+
}, "*");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function currentProps() {
|
|
342
|
+
var props = {};
|
|
343
|
+
Object.keys(state.props).forEach(function (key) {
|
|
344
|
+
if (state.props[key] !== undefined) props[key] = state.props[key];
|
|
345
|
+
});
|
|
346
|
+
return props;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function sendInit() {
|
|
350
|
+
send("init", {
|
|
351
|
+
props: currentProps(),
|
|
352
|
+
theme: currentTheme(),
|
|
353
|
+
locale: navigator.language || "en",
|
|
354
|
+
instanceId: state.session.instanceId,
|
|
355
|
+
capabilities: state.preview ? { preview: true } : {}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function setStatus(text, cls) {
|
|
360
|
+
var status = $("status");
|
|
361
|
+
status.textContent = text;
|
|
362
|
+
status.className = cls || "";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function unmount() {
|
|
366
|
+
if (state.session) {
|
|
367
|
+
state.session.iframe.remove();
|
|
368
|
+
state.session = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function mount() {
|
|
373
|
+
unmount();
|
|
374
|
+
var widget = state.selected;
|
|
375
|
+
if (!widget) return;
|
|
376
|
+
var iframe = document.createElement("iframe");
|
|
377
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
378
|
+
iframe.src = widget.entryUrl;
|
|
379
|
+
var defaultHeight = (state.contract && state.contract.sizing && state.contract.sizing.defaultHeight) || 320;
|
|
380
|
+
iframe.style.height = defaultHeight + "px";
|
|
381
|
+
state.session = {
|
|
382
|
+
nonce: uid(),
|
|
383
|
+
instanceId: "harness-" + widget.id + "-" + Date.now().toString(36),
|
|
384
|
+
iframe: iframe,
|
|
385
|
+
ready: false
|
|
386
|
+
};
|
|
387
|
+
$("height-display").textContent = "height " + defaultHeight + "px (default)";
|
|
388
|
+
$("stage").appendChild(iframe);
|
|
389
|
+
iframe.addEventListener("load", function () {
|
|
390
|
+
setTimeout(function () {
|
|
391
|
+
if (state.session && state.session.iframe === iframe && !state.session.ready) sendInit();
|
|
392
|
+
}, 50);
|
|
393
|
+
});
|
|
394
|
+
setStatus("connecting to " + widget.entryUrl + " \\u2026");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
window.addEventListener("message", function (event) {
|
|
398
|
+
var s = state.session;
|
|
399
|
+
if (!s || event.source !== s.iframe.contentWindow) return;
|
|
400
|
+
var data = event.data;
|
|
401
|
+
if (!data || data.protocol !== H.protocol || typeof data.type !== "string") return;
|
|
402
|
+
trace("in", data.type, data.payload);
|
|
403
|
+
if (data.type === "hello") { sendInit(); return; }
|
|
404
|
+
if (data.nonce !== s.nonce) { traceNote("dropped '" + data.type + "': nonce mismatch"); return; }
|
|
405
|
+
if (data.type === "ready") {
|
|
406
|
+
s.ready = true;
|
|
407
|
+
setStatus("ready \\u00b7 contract v" + (data.payload ? data.payload.contractVersion : "?") +
|
|
408
|
+
(state.preview ? " \\u00b7 preview" : ""), "ok");
|
|
409
|
+
} else if (data.type === "event") {
|
|
410
|
+
onEvent(data.payload || {});
|
|
411
|
+
} else if (data.type === "query:result") {
|
|
412
|
+
onQueryResult(data.payload || {});
|
|
413
|
+
} else if (data.type === "resize") {
|
|
414
|
+
onResize(data.payload || {});
|
|
415
|
+
} else if (data.type === "value:changed") {
|
|
416
|
+
logEntry("value:changed", (data.payload || {}).values, [], true);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
function onEvent(payload) {
|
|
421
|
+
var name = typeof payload.name === "string" ? payload.name : "(unnamed)";
|
|
422
|
+
var errors = [];
|
|
423
|
+
var events = (state.contract && state.contract.events) || {};
|
|
424
|
+
if (!Object.prototype.hasOwnProperty.call(events, name)) {
|
|
425
|
+
errors.push("not declared in the contract");
|
|
426
|
+
} else if (events[name].payloadSchema) {
|
|
427
|
+
checkSchema(events[name].payloadSchema, payload.payload, "$", errors);
|
|
428
|
+
} else if (payload.payload !== undefined && payload.payload !== null) {
|
|
429
|
+
errors.push("contract declares no payload, but one was sent");
|
|
430
|
+
}
|
|
431
|
+
logEntry(name, payload.payload, errors, false);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function logEntry(name, payload, errors, meta) {
|
|
435
|
+
var entry = make("div", "log-entry" + (errors.length ? " invalid" : "") + (meta ? " meta" : ""));
|
|
436
|
+
var head = make("div", "log-head");
|
|
437
|
+
head.appendChild(make("span", "log-time", timestamp()));
|
|
438
|
+
head.appendChild(make("span", "log-name", name));
|
|
439
|
+
if (errors.length) head.appendChild(make("span", "log-flag", "invalid"));
|
|
440
|
+
entry.appendChild(head);
|
|
441
|
+
entry.appendChild(make("pre", "log-payload", shortJson(payload)));
|
|
442
|
+
errors.forEach(function (err) { entry.appendChild(make("div", "log-error", err)); });
|
|
443
|
+
appendCapped($("event-log"), entry, LOG_CAP);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function onResize(payload) {
|
|
447
|
+
var s = state.session;
|
|
448
|
+
if (!s) return;
|
|
449
|
+
var reported = typeof payload.height === "number" ? payload.height : 0;
|
|
450
|
+
var maxHeight = (state.contract && state.contract.sizing && state.contract.sizing.maxHeight) || 4000;
|
|
451
|
+
var applied = Math.max(40, Math.min(Math.ceil(reported), maxHeight));
|
|
452
|
+
s.iframe.style.height = applied + "px";
|
|
453
|
+
$("height-display").textContent = "height " + Math.round(reported) + "px" +
|
|
454
|
+
(applied !== Math.ceil(reported) ? " (clamped to " + applied + "px)" : "");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------- props panel ----------
|
|
458
|
+
function seedProps() {
|
|
459
|
+
var props = {};
|
|
460
|
+
state.formModel.forEach(function (field) {
|
|
461
|
+
if (field.default !== undefined) props[field.key] = field.default;
|
|
462
|
+
});
|
|
463
|
+
state.props = props;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function sendProp(key, value) {
|
|
467
|
+
state.props[key] = value;
|
|
468
|
+
var patch = {};
|
|
469
|
+
patch[key] = value;
|
|
470
|
+
send("props:update", { props: patch });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function renderPropsPanel() {
|
|
474
|
+
var panel = $("props-panel");
|
|
475
|
+
panel.textContent = "";
|
|
476
|
+
if (!state.formModel.length) {
|
|
477
|
+
panel.appendChild(make("div", "empty", "No inputs declared in the contract."));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
state.formModel.forEach(function (field) { panel.appendChild(fieldRow(field)); });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function fieldRow(field) {
|
|
484
|
+
var row = make("div", "field");
|
|
485
|
+
row.appendChild(make("label", "field-label", field.key + (field.optional ? " (optional)" : "")));
|
|
486
|
+
if (field.description) row.appendChild(make("div", "field-desc", field.description));
|
|
487
|
+
var control = field.control;
|
|
488
|
+
var value = state.props[field.key];
|
|
489
|
+
if (control.kind === "checkbox") {
|
|
490
|
+
var checkbox = document.createElement("input");
|
|
491
|
+
checkbox.type = "checkbox";
|
|
492
|
+
checkbox.checked = value === true;
|
|
493
|
+
checkbox.addEventListener("change", function () { sendProp(field.key, checkbox.checked); });
|
|
494
|
+
row.appendChild(checkbox);
|
|
495
|
+
} else if (control.kind === "select") {
|
|
496
|
+
var select = document.createElement("select");
|
|
497
|
+
control.choices.forEach(function (choice) {
|
|
498
|
+
var option = document.createElement("option");
|
|
499
|
+
option.value = choice;
|
|
500
|
+
option.textContent = choice;
|
|
501
|
+
select.appendChild(option);
|
|
502
|
+
});
|
|
503
|
+
if (typeof value === "string") select.value = value;
|
|
504
|
+
select.addEventListener("change", function () { sendProp(field.key, select.value); });
|
|
505
|
+
row.appendChild(select);
|
|
506
|
+
} else if (control.kind === "number") {
|
|
507
|
+
var num = document.createElement("input");
|
|
508
|
+
num.type = "number";
|
|
509
|
+
if (control.min !== undefined) num.min = String(control.min);
|
|
510
|
+
if (control.max !== undefined) num.max = String(control.max);
|
|
511
|
+
num.step = control.integer ? "1" : "any";
|
|
512
|
+
if (typeof value === "number") num.value = String(value);
|
|
513
|
+
var numError = make("div", "field-error");
|
|
514
|
+
num.addEventListener("change", function () {
|
|
515
|
+
var parsed = control.integer ? parseInt(num.value, 10) : Number(num.value);
|
|
516
|
+
if (num.value === "" || isNaN(parsed)) { numError.textContent = "not a number"; return; }
|
|
517
|
+
if (control.min !== undefined && parsed < control.min) { numError.textContent = "minimum is " + control.min; return; }
|
|
518
|
+
if (control.max !== undefined && parsed > control.max) { numError.textContent = "maximum is " + control.max; return; }
|
|
519
|
+
numError.textContent = "";
|
|
520
|
+
sendProp(field.key, parsed);
|
|
521
|
+
});
|
|
522
|
+
row.appendChild(num);
|
|
523
|
+
row.appendChild(numError);
|
|
524
|
+
} else if (control.kind === "json") {
|
|
525
|
+
var area = document.createElement("textarea");
|
|
526
|
+
area.rows = 4;
|
|
527
|
+
area.spellcheck = false;
|
|
528
|
+
area.value = value === undefined ? "" : JSON.stringify(value, null, 2);
|
|
529
|
+
var jsonError = make("div", "field-error");
|
|
530
|
+
area.addEventListener("input", function () {
|
|
531
|
+
if (area.value.trim() === "") { area.classList.remove("bad"); jsonError.textContent = ""; return; }
|
|
532
|
+
var parsed;
|
|
533
|
+
try { parsed = JSON.parse(area.value); } catch (e) {
|
|
534
|
+
area.classList.add("bad");
|
|
535
|
+
jsonError.textContent = "invalid JSON: " + e.message;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
area.classList.remove("bad");
|
|
539
|
+
var errors = control.schema ? checkSchema(control.schema, parsed, "$", []) : [];
|
|
540
|
+
jsonError.textContent = errors.join("; ");
|
|
541
|
+
sendProp(field.key, parsed);
|
|
542
|
+
});
|
|
543
|
+
row.appendChild(area);
|
|
544
|
+
row.appendChild(jsonError);
|
|
545
|
+
} else {
|
|
546
|
+
var text = document.createElement("input");
|
|
547
|
+
text.type = "text";
|
|
548
|
+
if (typeof value === "string") text.value = value;
|
|
549
|
+
text.addEventListener("input", function () { sendProp(field.key, text.value); });
|
|
550
|
+
row.appendChild(text);
|
|
551
|
+
}
|
|
552
|
+
return row;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------- fixtures ----------
|
|
556
|
+
function renderFixtures() {
|
|
557
|
+
var box = $("fixtures-panel");
|
|
558
|
+
box.textContent = "";
|
|
559
|
+
var names = Object.keys(state.fixtures);
|
|
560
|
+
if (!names.length) {
|
|
561
|
+
box.appendChild(make("div", "empty", "No dev.fixtures declared in widget.config.ts."));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
names.forEach(function (name) {
|
|
565
|
+
var button = make("button", "fixture-btn", name);
|
|
566
|
+
button.type = "button";
|
|
567
|
+
button.addEventListener("click", function () {
|
|
568
|
+
var patch = state.fixtures[name];
|
|
569
|
+
Object.keys(patch).forEach(function (key) { state.props[key] = patch[key]; });
|
|
570
|
+
renderPropsPanel();
|
|
571
|
+
send("props:update", { props: patch });
|
|
572
|
+
});
|
|
573
|
+
box.appendChild(button);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---------- query invoker ----------
|
|
578
|
+
function renderQueryPanel() {
|
|
579
|
+
var select = $("query-select");
|
|
580
|
+
select.textContent = "";
|
|
581
|
+
var queries = (state.contract && state.contract.queries) || {};
|
|
582
|
+
var names = Object.keys(queries);
|
|
583
|
+
$("query-invoke").disabled = names.length === 0;
|
|
584
|
+
if (!names.length) {
|
|
585
|
+
var placeholder = document.createElement("option");
|
|
586
|
+
placeholder.textContent = "(no queries declared)";
|
|
587
|
+
select.appendChild(placeholder);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
names.forEach(function (name) {
|
|
591
|
+
var option = document.createElement("option");
|
|
592
|
+
option.value = name;
|
|
593
|
+
option.textContent = name;
|
|
594
|
+
select.appendChild(option);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function invokeQuery() {
|
|
599
|
+
var name = $("query-select").value;
|
|
600
|
+
var queries = (state.contract && state.contract.queries) || {};
|
|
601
|
+
if (!Object.prototype.hasOwnProperty.call(queries, name)) return;
|
|
602
|
+
var result = $("query-result");
|
|
603
|
+
var argsText = $("query-args").value.trim();
|
|
604
|
+
var args = null;
|
|
605
|
+
if (argsText !== "") {
|
|
606
|
+
try { args = JSON.parse(argsText); } catch (e) {
|
|
607
|
+
result.textContent = "invalid args JSON: " + e.message;
|
|
608
|
+
result.className = "err";
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
var queryId = uid();
|
|
613
|
+
state.pending[queryId] = {
|
|
614
|
+
name: name,
|
|
615
|
+
timer: setTimeout(function () {
|
|
616
|
+
if (state.pending[queryId]) {
|
|
617
|
+
delete state.pending[queryId];
|
|
618
|
+
result.textContent = "query '" + name + "' timed out after 10s";
|
|
619
|
+
result.className = "err";
|
|
620
|
+
}
|
|
621
|
+
}, 10000)
|
|
622
|
+
};
|
|
623
|
+
result.textContent = "waiting \\u2026";
|
|
624
|
+
result.className = "";
|
|
625
|
+
send("query", { queryId: queryId, name: name, args: args });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function onQueryResult(payload) {
|
|
629
|
+
var pending = state.pending[payload.queryId];
|
|
630
|
+
if (!pending) { traceNote("query:result for unknown queryId " + shortJson(payload.queryId)); return; }
|
|
631
|
+
clearTimeout(pending.timer);
|
|
632
|
+
delete state.pending[payload.queryId];
|
|
633
|
+
var box = $("query-result");
|
|
634
|
+
if (!payload.ok) {
|
|
635
|
+
box.textContent = pending.name + " failed: " + payload.error;
|
|
636
|
+
box.className = "err";
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
var text;
|
|
640
|
+
try { text = JSON.stringify(payload.value, null, 2); } catch (e) { text = String(payload.value); }
|
|
641
|
+
box.textContent = pending.name + " \\u2192 " + (text === undefined ? "undefined" : text);
|
|
642
|
+
box.className = "ok";
|
|
643
|
+
var spec = ((state.contract && state.contract.queries) || {})[pending.name];
|
|
644
|
+
if (spec && spec.resultSchema) {
|
|
645
|
+
var errors = checkSchema(spec.resultSchema, payload.value, "$", []);
|
|
646
|
+
if (errors.length) {
|
|
647
|
+
box.className = "warn";
|
|
648
|
+
box.textContent += "\\n(schema mismatch: " + errors.join("; ") + ")";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ---------- selection & boot ----------
|
|
654
|
+
function renderSidebar() {
|
|
655
|
+
var nav = $("widget-list");
|
|
656
|
+
nav.textContent = "";
|
|
657
|
+
var byGroup = {};
|
|
658
|
+
state.widgets.forEach(function (widget) {
|
|
659
|
+
(byGroup[widget.group] = byGroup[widget.group] || []).push(widget);
|
|
660
|
+
});
|
|
661
|
+
Object.keys(byGroup).sort().forEach(function (group) {
|
|
662
|
+
nav.appendChild(make("div", "group-name", group));
|
|
663
|
+
byGroup[group].forEach(function (widget) {
|
|
664
|
+
var button = make("button", "widget-item", widget.id);
|
|
665
|
+
button.type = "button";
|
|
666
|
+
button.dataset.key = widget.group + "/" + widget.id;
|
|
667
|
+
if (state.selected && state.selected.group === widget.group && state.selected.id === widget.id) {
|
|
668
|
+
button.classList.add("active");
|
|
669
|
+
}
|
|
670
|
+
button.addEventListener("click", function () { selectWidget(widget); });
|
|
671
|
+
nav.appendChild(button);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function selectWidget(widget) {
|
|
677
|
+
state.selected = widget;
|
|
678
|
+
renderSidebar();
|
|
679
|
+
$("widget-title").textContent = widget.group + "/" + widget.id;
|
|
680
|
+
$("event-log").textContent = "";
|
|
681
|
+
$("query-result").textContent = "";
|
|
682
|
+
$("query-result").className = "";
|
|
683
|
+
loadContract();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function loadContract() {
|
|
687
|
+
var widget = state.selected;
|
|
688
|
+
if (!widget) return;
|
|
689
|
+
setStatus("loading contract \\u2026");
|
|
690
|
+
fetch("/api/contract/" + encodeURIComponent(widget.group) + "/" + encodeURIComponent(widget.id))
|
|
691
|
+
.then(function (response) {
|
|
692
|
+
return response.json().then(function (body) { return { ok: response.ok, body: body }; });
|
|
693
|
+
})
|
|
694
|
+
.then(function (result) {
|
|
695
|
+
if (state.selected !== widget) return;
|
|
696
|
+
if (!result.ok) {
|
|
697
|
+
state.contract = null;
|
|
698
|
+
state.formModel = [];
|
|
699
|
+
state.fixtures = {};
|
|
700
|
+
setStatus("contract error: " + (result.body && result.body.error), "err");
|
|
701
|
+
} else {
|
|
702
|
+
state.contract = result.body.contract;
|
|
703
|
+
state.formModel = result.body.formModel || [];
|
|
704
|
+
state.fixtures = result.body.fixtures || {};
|
|
705
|
+
(result.body.warnings || []).forEach(function (warning) {
|
|
706
|
+
traceNote("contract warning: " + warning);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
seedProps();
|
|
710
|
+
renderPropsPanel();
|
|
711
|
+
renderFixtures();
|
|
712
|
+
renderQueryPanel();
|
|
713
|
+
mount();
|
|
714
|
+
})
|
|
715
|
+
.catch(function (error) {
|
|
716
|
+
if (state.selected !== widget) return;
|
|
717
|
+
setStatus("contract fetch failed: " + error, "err");
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function refreshWidgets(autoselect) {
|
|
722
|
+
return fetch("/api/widgets")
|
|
723
|
+
.then(function (response) { return response.json(); })
|
|
724
|
+
.then(function (body) {
|
|
725
|
+
if (body && Array.isArray(body.widgets) && body.widgets.length) {
|
|
726
|
+
state.widgets = body.widgets;
|
|
727
|
+
if (state.selected) {
|
|
728
|
+
var again = state.widgets.filter(function (widget) {
|
|
729
|
+
return widget.group === state.selected.group && widget.id === state.selected.id;
|
|
730
|
+
})[0];
|
|
731
|
+
if (again) state.selected = again;
|
|
732
|
+
}
|
|
733
|
+
renderSidebar();
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
.catch(function () {})
|
|
737
|
+
.then(function () {
|
|
738
|
+
if (autoselect && !state.selected && state.widgets.length) selectWidget(state.widgets[0]);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function boot() {
|
|
743
|
+
renderSidebar();
|
|
744
|
+
renderTokenEditor();
|
|
745
|
+
$("width-select").addEventListener("change", function () {
|
|
746
|
+
var value = $("width-select").value;
|
|
747
|
+
$("stage").style.width = value === "full" ? "100%" : value + "px";
|
|
748
|
+
});
|
|
749
|
+
$("theme-toggle").addEventListener("click", function () {
|
|
750
|
+
setMode(state.mode === "dark" ? "light" : "dark");
|
|
751
|
+
});
|
|
752
|
+
$("preview-toggle").addEventListener("change", function (event) {
|
|
753
|
+
state.preview = event.target.checked;
|
|
754
|
+
mount();
|
|
755
|
+
});
|
|
756
|
+
$("reload-btn").addEventListener("click", function () { loadContract(); });
|
|
757
|
+
$("query-invoke").addEventListener("click", invokeQuery);
|
|
758
|
+
$("clear-log").addEventListener("click", function (event) {
|
|
759
|
+
event.preventDefault();
|
|
760
|
+
event.stopPropagation();
|
|
761
|
+
$("event-log").textContent = "";
|
|
762
|
+
});
|
|
763
|
+
$("clear-trace").addEventListener("click", function (event) {
|
|
764
|
+
event.preventDefault();
|
|
765
|
+
event.stopPropagation();
|
|
766
|
+
$("trace").textContent = "";
|
|
767
|
+
});
|
|
768
|
+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
769
|
+
setMode("dark");
|
|
770
|
+
}
|
|
771
|
+
refreshWidgets(true);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
boot();
|
|
775
|
+
})();
|
|
776
|
+
`;
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Generate the self-contained mock-host harness page (inline HTML/CSS/JS,
|
|
780
|
+
* no frameworks, no external assets). Pure function of the widget list.
|
|
781
|
+
*/
|
|
782
|
+
export function harnessHtml(widgets: HarnessWidget[]): string {
|
|
783
|
+
const data: HarnessData = {
|
|
784
|
+
protocol: FLW_PROTOCOL,
|
|
785
|
+
widgets,
|
|
786
|
+
themes: { light: DEFAULT_LIGHT_TOKENS, dark: DEFAULT_DARK_TOKENS },
|
|
787
|
+
};
|
|
788
|
+
return `<!doctype html>
|
|
789
|
+
<html lang="en">
|
|
790
|
+
<head>
|
|
791
|
+
<meta charset="utf-8" />
|
|
792
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
793
|
+
<title>Flow-Like Widget Harness</title>
|
|
794
|
+
<style>${HARNESS_CSS}</style>
|
|
795
|
+
<script>window.__HARNESS__ = ${embedJson(data)};</script>
|
|
796
|
+
</head>
|
|
797
|
+
<body>
|
|
798
|
+
<div id="app">
|
|
799
|
+
<aside id="sidebar">
|
|
800
|
+
<h1>Flow-Like <span>widget harness</span></h1>
|
|
801
|
+
<nav id="widget-list">
|
|
802
|
+
${sidebarMarkup(widgets)}
|
|
803
|
+
</nav>
|
|
804
|
+
</aside>
|
|
805
|
+
<main id="main">
|
|
806
|
+
<div id="toolbar">
|
|
807
|
+
<span id="widget-title">select a widget</span>
|
|
808
|
+
<span id="status"></span>
|
|
809
|
+
<span class="spacer"></span>
|
|
810
|
+
<label class="control">width
|
|
811
|
+
<select id="width-select">
|
|
812
|
+
<option value="full" selected>full</option>
|
|
813
|
+
<option value="360">360</option>
|
|
814
|
+
<option value="480">480</option>
|
|
815
|
+
<option value="768">768</option>
|
|
816
|
+
<option value="1024">1024</option>
|
|
817
|
+
</select>
|
|
818
|
+
</label>
|
|
819
|
+
<span id="height-display" class="control"></span>
|
|
820
|
+
<label class="control"><input type="checkbox" id="preview-toggle" /> preview</label>
|
|
821
|
+
<button id="theme-toggle" type="button" title="Toggle light/dark theme">light</button>
|
|
822
|
+
<button id="reload-btn" type="button" title="Re-extract the contract and remount">reload</button>
|
|
823
|
+
</div>
|
|
824
|
+
<div id="content">
|
|
825
|
+
<div id="stage-wrap"><div id="stage"></div></div>
|
|
826
|
+
<div id="panels">
|
|
827
|
+
<details open>
|
|
828
|
+
<summary>Props</summary>
|
|
829
|
+
<div id="props-panel"></div>
|
|
830
|
+
</details>
|
|
831
|
+
<details open>
|
|
832
|
+
<summary>Fixtures</summary>
|
|
833
|
+
<div id="fixtures-panel"></div>
|
|
834
|
+
</details>
|
|
835
|
+
<details open>
|
|
836
|
+
<summary>Events <button id="clear-log" type="button">clear</button></summary>
|
|
837
|
+
<div id="event-log"></div>
|
|
838
|
+
</details>
|
|
839
|
+
<details open>
|
|
840
|
+
<summary>Query</summary>
|
|
841
|
+
<div id="query-panel">
|
|
842
|
+
<select id="query-select"></select>
|
|
843
|
+
<textarea id="query-args" rows="3" placeholder="args (JSON, empty = null)"></textarea>
|
|
844
|
+
<button id="query-invoke" type="button">invoke</button>
|
|
845
|
+
<pre id="query-result"></pre>
|
|
846
|
+
</div>
|
|
847
|
+
</details>
|
|
848
|
+
<details>
|
|
849
|
+
<summary>Theme tokens</summary>
|
|
850
|
+
<div id="token-editor"></div>
|
|
851
|
+
</details>
|
|
852
|
+
<details>
|
|
853
|
+
<summary>Protocol trace <button id="clear-trace" type="button">clear</button></summary>
|
|
854
|
+
<div id="trace"></div>
|
|
855
|
+
</details>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
</main>
|
|
859
|
+
</div>
|
|
860
|
+
<script>${HARNESS_JS}</script>
|
|
861
|
+
</body>
|
|
862
|
+
</html>
|
|
863
|
+
`;
|
|
864
|
+
}
|