state-scenarios 0.2.0 → 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/dist/adapters/redux.js +2 -2
- package/dist/adapters/tanstack-query.js +1 -1
- package/dist/adapters/xstate.js +2 -2
- package/dist/adapters/zustand.js +1 -1
- package/dist/capture.d.ts +26 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/{chunk-KRCYO64S.js → chunk-3M6SD4TA.js} +5 -1
- package/dist/chunk-3M6SD4TA.js.map +1 -0
- package/dist/{chunk-6L55WKFS.js → chunk-DCURPYW5.js} +4 -2
- package/dist/chunk-DCURPYW5.js.map +1 -0
- package/dist/chunk-FTWYRB6F.js +277 -0
- package/dist/chunk-FTWYRB6F.js.map +1 -0
- package/dist/chunk-HEVJQ47T.js +148 -0
- package/dist/chunk-HEVJQ47T.js.map +1 -0
- package/dist/chunk-KYU7RUWI.js +179 -0
- package/dist/chunk-KYU7RUWI.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +133 -11
- package/dist/index.js.map +1 -1
- package/dist/link.d.ts +4 -0
- package/dist/next.d.ts +21 -0
- package/dist/next.js +30 -0
- package/dist/next.js.map +1 -0
- package/dist/no-msw-browser.d.ts +2 -0
- package/dist/no-msw-browser.js +8 -0
- package/dist/no-msw-browser.js.map +1 -0
- package/dist/panel.d.ts +10 -8
- package/dist/panel.js +284 -40
- package/dist/panel.js.map +1 -1
- package/dist/playwright.js +1 -1
- package/dist/ready.d.ts +39 -0
- package/dist/record.d.ts +49 -0
- package/dist/runtime.d.ts +31 -3
- package/dist/save.d.ts +27 -0
- package/dist/schema.d.ts +9 -0
- package/dist/vite.d.ts +11 -0
- package/dist/vite.js +26 -0
- package/dist/vite.js.map +1 -0
- package/package.json +27 -3
- package/dist/chunk-6L55WKFS.js.map +0 -1
- package/dist/chunk-KDH572NI.js +0 -88
- package/dist/chunk-KDH572NI.js.map +0 -1
- package/dist/chunk-KRCYO64S.js.map +0 -1
package/dist/panel.js
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureScenario,
|
|
3
|
+
checkReadySelector,
|
|
4
|
+
closestReadySelector,
|
|
5
|
+
suggestReadySelectors
|
|
6
|
+
} from "./chunk-FTWYRB6F.js";
|
|
1
7
|
import {
|
|
2
8
|
PARAM,
|
|
3
|
-
|
|
9
|
+
SAVE_ENDPOINT,
|
|
4
10
|
readInline,
|
|
5
11
|
scenarioHref
|
|
6
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3M6SD4TA.js";
|
|
13
|
+
import "./chunk-KYU7RUWI.js";
|
|
7
14
|
import {
|
|
8
15
|
parseScenario
|
|
9
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-DCURPYW5.js";
|
|
10
17
|
|
|
11
18
|
// src/panel.ts
|
|
19
|
+
import { z } from "zod";
|
|
12
20
|
var OPEN_KEY = "scenario-panel:open";
|
|
13
|
-
var
|
|
21
|
+
var FLASH_KEY = "scenario-panel:flash";
|
|
22
|
+
var SaveProbeSchema = z.object({ dir: z.string(), url: z.string().optional() });
|
|
23
|
+
var SaveResultSchema = z.object({ path: z.string().optional(), error: z.string().optional() });
|
|
24
|
+
var FlashSchema = z.object({ message: z.string(), at: z.number() });
|
|
14
25
|
function mountPanel(session, options = {}) {
|
|
15
26
|
if (!customElements.get("scenario-panel")) {
|
|
16
27
|
customElements.define("scenario-panel", ScenarioPanel);
|
|
17
28
|
}
|
|
18
29
|
const el = new ScenarioPanel();
|
|
19
30
|
el.session = session;
|
|
31
|
+
el.schemaPath = options.schemaPath;
|
|
20
32
|
const [v, h] = (options.position ?? "bottom-right").split("-");
|
|
21
33
|
el.dataset.v = v;
|
|
22
34
|
el.dataset.h = h;
|
|
@@ -25,34 +37,18 @@ function mountPanel(session, options = {}) {
|
|
|
25
37
|
document.body.append(el);
|
|
26
38
|
return el;
|
|
27
39
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
new URLSearchParams(location.search).forEach((v, k) => {
|
|
31
|
-
if (!SCENARIO_PARAMS.has(k)) url[k] = v;
|
|
32
|
-
});
|
|
33
|
-
const state = Object.fromEntries(
|
|
34
|
-
session.keys().flatMap((key) => {
|
|
35
|
-
const value = session.read(key);
|
|
36
|
-
return value === void 0 ? [] : [[key, value]];
|
|
37
|
-
})
|
|
38
|
-
);
|
|
39
|
-
const draft = parseScenario({
|
|
40
|
-
name,
|
|
41
|
-
description: description || void 0,
|
|
42
|
-
path: location.pathname,
|
|
43
|
-
url,
|
|
44
|
-
network: session.resolved?.network,
|
|
45
|
-
state
|
|
46
|
-
});
|
|
47
|
-
return session.catalog.resolve(draft);
|
|
48
|
-
}
|
|
49
|
-
var ScenarioPanel = class extends HTMLElement {
|
|
40
|
+
var Base = globalThis.HTMLElement ?? Object;
|
|
41
|
+
var ScenarioPanel = class extends Base {
|
|
50
42
|
session;
|
|
43
|
+
schemaPath;
|
|
51
44
|
render;
|
|
52
45
|
unsubscribe;
|
|
46
|
+
stopPick;
|
|
53
47
|
disconnectedCallback() {
|
|
54
48
|
this.unsubscribe?.();
|
|
55
49
|
this.unsubscribe = void 0;
|
|
50
|
+
this.stopPick?.();
|
|
51
|
+
this.stopPick = void 0;
|
|
56
52
|
}
|
|
57
53
|
connectedCallback() {
|
|
58
54
|
if (this.render) {
|
|
@@ -78,13 +74,26 @@ var ScenarioPanel = class extends HTMLElement {
|
|
|
78
74
|
<section class="live" aria-label="Live state"></section>
|
|
79
75
|
<details class="save">
|
|
80
76
|
<summary>Save as scenario</summary>
|
|
81
|
-
<p class="hint">
|
|
77
|
+
<p class="hint">Saves the current URL, network responses and live state. <b>Copy link</b> shares it as is; <b>Download JSON</b> writes a file for your scenarios folder that keeps only what differs from the base.</p>
|
|
82
78
|
<label>Name <input name="save-name" placeholder="e.g. checkout-card-declined" aria-label="Scenario name"></label>
|
|
83
79
|
<label>Description <input name="save-description" aria-label="Scenario description"></label>
|
|
80
|
+
<label>Ready: what's on screen once this state has rendered <input name="save-ready" placeholder='Use Mark ready, or type text="\u2026"' aria-label="Ready selector"></label>
|
|
81
|
+
<p class="ready-check" role="status"></p>
|
|
84
82
|
<div class="row">
|
|
83
|
+
<button type="button" data-act="mark-ready">Mark ready</button>
|
|
84
|
+
<button type="button" data-act="clear-ready">Clear</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="candidates" role="group" aria-label="Suggested ready selectors"></div>
|
|
87
|
+
<div class="pick-box" hidden><span></span></div>
|
|
88
|
+
<label>Extends
|
|
89
|
+
<select name="save-base" aria-label="Base scenario to extend"></select>
|
|
90
|
+
</label>
|
|
91
|
+
<div class="row">
|
|
92
|
+
<button data-act="save-project" hidden>Save to project</button>
|
|
85
93
|
<button data-act="save-link">Copy link</button>
|
|
86
94
|
<button data-act="save-json">Download JSON</button>
|
|
87
95
|
</div>
|
|
96
|
+
<p class="save-status" role="status"></p>
|
|
88
97
|
<input class="saved" readonly aria-label="Saved scenario link" hidden>
|
|
89
98
|
</details>
|
|
90
99
|
<details class="edit">
|
|
@@ -190,26 +199,239 @@ var ScenarioPanel = class extends HTMLElement {
|
|
|
190
199
|
};
|
|
191
200
|
const saveName = find("input[name=save-name]", HTMLInputElement);
|
|
192
201
|
const saveDescription = find("input[name=save-description]", HTMLInputElement);
|
|
202
|
+
const saveReady = find("input[name=save-ready]", HTMLInputElement);
|
|
203
|
+
const saveBase = find("select[name=save-base]", HTMLSelectElement);
|
|
204
|
+
const readyCheck = find(".ready-check", HTMLParagraphElement);
|
|
205
|
+
const saveProject = find("[data-act=save-project]", HTMLButtonElement);
|
|
206
|
+
const saveStatus = find(".save-status", HTMLParagraphElement);
|
|
207
|
+
const candidates = find(".candidates", HTMLElement);
|
|
193
208
|
const saved = find(".saved", HTMLInputElement);
|
|
194
|
-
const
|
|
209
|
+
const saveDetails = find(".save", HTMLDetailsElement);
|
|
210
|
+
const markReadyBtn = find("[data-act=mark-ready]", HTMLButtonElement);
|
|
211
|
+
const fillBaseOptions = () => {
|
|
212
|
+
const preferred = catalog.get("default")?.name ?? [session.active?.extends ?? []].flat()[0] ?? "";
|
|
213
|
+
const previous = saveBase.value;
|
|
214
|
+
saveBase.innerHTML = `<option value="">(none: save everything)</option>` + catalog.list.map((s) => `<option value="${esc(s.name)}">${esc(s.name)}</option>`).join("");
|
|
215
|
+
if (previous && [...saveBase.options].some((o) => o.value === previous)) {
|
|
216
|
+
saveBase.value = previous;
|
|
217
|
+
} else {
|
|
218
|
+
saveBase.value = preferred;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
fillBaseOptions();
|
|
222
|
+
const renderCandidates = () => {
|
|
223
|
+
const picks = suggestReadySelectors(document.body, { limit: 3 });
|
|
224
|
+
candidates.innerHTML = picks.length ? `<p class="hint">Suggestions</p><div class="row">${picks.map((s) => `<button type="button" data-candidate="${esc(s)}">${esc(s)}</button>`).join("")}</div>` : "";
|
|
225
|
+
candidates.querySelectorAll("[data-candidate]").forEach((btn) => {
|
|
226
|
+
btn.onclick = () => {
|
|
227
|
+
saveReady.value = btn.dataset.candidate ?? "";
|
|
228
|
+
updateReadyCheck();
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
saveDetails.ontoggle = () => {
|
|
233
|
+
if (saveDetails.open) {
|
|
234
|
+
fillBaseOptions();
|
|
235
|
+
renderCandidates();
|
|
236
|
+
updateReadyCheck();
|
|
237
|
+
void probeSaveEndpoint();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const stopPickMode = () => {
|
|
241
|
+
this.stopPick?.();
|
|
242
|
+
this.stopPick = void 0;
|
|
243
|
+
markReadyBtn.setAttribute("aria-pressed", "false");
|
|
244
|
+
markReadyBtn.textContent = "Mark ready";
|
|
245
|
+
};
|
|
246
|
+
const pickBox = find(".pick-box", HTMLDivElement);
|
|
247
|
+
const pickLabel = find(".pick-box span", HTMLSpanElement);
|
|
248
|
+
const outline = (el, label) => {
|
|
249
|
+
if (!el) {
|
|
250
|
+
pickBox.hidden = true;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const r = el.getBoundingClientRect();
|
|
254
|
+
Object.assign(pickBox.style, {
|
|
255
|
+
top: `${r.top}px`,
|
|
256
|
+
left: `${r.left}px`,
|
|
257
|
+
width: `${r.width}px`,
|
|
258
|
+
height: `${r.height}px`
|
|
259
|
+
});
|
|
260
|
+
pickBox.dataset.below = String(r.top < 28);
|
|
261
|
+
pickLabel.textContent = label;
|
|
262
|
+
pickBox.hidden = false;
|
|
263
|
+
};
|
|
264
|
+
const appTarget = (e) => {
|
|
265
|
+
const path = e.composedPath();
|
|
266
|
+
if (path.includes(this)) return void 0;
|
|
267
|
+
return path.find(
|
|
268
|
+
(n) => n instanceof Element && n !== document.documentElement && n !== document.body
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
const READY_MESSAGES = {
|
|
272
|
+
empty: "No ready selector: the link still works, but shoot and tests won't wait for this state to render.",
|
|
273
|
+
none: "No match on this page: tests would time out waiting for it.",
|
|
274
|
+
unchecked: "Can't check this kind of selector here; Playwright still can."
|
|
275
|
+
};
|
|
276
|
+
let readyMatch;
|
|
277
|
+
const updateReadyCheck = () => {
|
|
278
|
+
const value = saveReady.value.trim();
|
|
279
|
+
const check = value ? checkReadySelector(value) : void 0;
|
|
280
|
+
readyMatch = check?.status === "match" ? check.first : void 0;
|
|
281
|
+
let tone = "muted";
|
|
282
|
+
let message;
|
|
283
|
+
if (!check) message = READY_MESSAGES.empty;
|
|
284
|
+
else if (check.status === "invalid") [tone, message] = ["bad", check.error];
|
|
285
|
+
else if (check.status === "none") [tone, message] = ["bad", READY_MESSAGES.none];
|
|
286
|
+
else if (check.status === "unchecked") message = READY_MESSAGES.unchecked;
|
|
287
|
+
else if (!check.visible) {
|
|
288
|
+
message = "Matches, but the first match is hidden: tests wait until it shows.";
|
|
289
|
+
} else if (check.count > 1) {
|
|
290
|
+
message = `Matches ${check.count} elements; tests wait for the first (outlined).`;
|
|
291
|
+
} else [tone, message] = ["ok", "\u2713 Matches 1 element on this page."];
|
|
292
|
+
readyCheck.dataset.tone = tone;
|
|
293
|
+
readyCheck.textContent = message;
|
|
294
|
+
if (this.shadowRoot?.activeElement === saveReady && !this.stopPick) outline(readyMatch, "ready");
|
|
295
|
+
};
|
|
296
|
+
saveReady.oninput = updateReadyCheck;
|
|
297
|
+
saveReady.onfocus = updateReadyCheck;
|
|
298
|
+
saveReady.onblur = () => {
|
|
299
|
+
if (!this.stopPick) outline(void 0, "");
|
|
300
|
+
};
|
|
301
|
+
updateReadyCheck();
|
|
302
|
+
const startPickMode = () => {
|
|
303
|
+
stopPickMode();
|
|
304
|
+
markReadyBtn.setAttribute("aria-pressed", "true");
|
|
305
|
+
markReadyBtn.textContent = "Click the UI\u2026 (Esc cancels)";
|
|
306
|
+
document.documentElement.style.cursor = "crosshair";
|
|
307
|
+
const onMove = (e) => {
|
|
308
|
+
const target = appTarget(e);
|
|
309
|
+
const hit = target && closestReadySelector(target);
|
|
310
|
+
if (hit) outline(hit.element, hit.selector);
|
|
311
|
+
else outline(target, "No selector here: try text, a heading or a button");
|
|
312
|
+
};
|
|
313
|
+
const block = (e) => {
|
|
314
|
+
if (!appTarget(e)) return;
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
e.stopPropagation();
|
|
317
|
+
};
|
|
318
|
+
const onClick = (e) => {
|
|
319
|
+
const target = appTarget(e);
|
|
320
|
+
if (!target) return;
|
|
321
|
+
block(e);
|
|
322
|
+
const hit = closestReadySelector(target);
|
|
323
|
+
if (!hit) return;
|
|
324
|
+
saveReady.value = hit.selector;
|
|
325
|
+
updateReadyCheck();
|
|
326
|
+
stopPickMode();
|
|
327
|
+
saveReady.focus();
|
|
328
|
+
};
|
|
329
|
+
const onKey = (e) => {
|
|
330
|
+
if (e.key !== "Escape") return;
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
stopPickMode();
|
|
333
|
+
};
|
|
334
|
+
const pressEvents = ["pointerdown", "mousedown", "pointerup", "mouseup"];
|
|
335
|
+
document.addEventListener("mousemove", onMove, true);
|
|
336
|
+
document.addEventListener("click", onClick, true);
|
|
337
|
+
document.addEventListener("keydown", onKey, true);
|
|
338
|
+
for (const type of pressEvents) document.addEventListener(type, block, true);
|
|
339
|
+
this.stopPick = () => {
|
|
340
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
341
|
+
document.removeEventListener("click", onClick, true);
|
|
342
|
+
document.removeEventListener("keydown", onKey, true);
|
|
343
|
+
for (const type of pressEvents) document.removeEventListener(type, block, true);
|
|
344
|
+
document.documentElement.style.removeProperty("cursor");
|
|
345
|
+
pickBox.hidden = true;
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
markReadyBtn.onclick = () => {
|
|
349
|
+
if (markReadyBtn.getAttribute("aria-pressed") === "true") stopPickMode();
|
|
350
|
+
else startPickMode();
|
|
351
|
+
};
|
|
352
|
+
find("[data-act=clear-ready]", HTMLButtonElement).onclick = () => {
|
|
353
|
+
saveReady.value = "";
|
|
354
|
+
updateReadyCheck();
|
|
355
|
+
};
|
|
356
|
+
const capture = () => {
|
|
357
|
+
const baseName = saveBase.value;
|
|
358
|
+
const base = baseName ? catalog.get(baseName) : void 0;
|
|
359
|
+
return captureScenario(session, {
|
|
360
|
+
name: saveName.value.trim(),
|
|
361
|
+
description: saveDescription.value.trim(),
|
|
362
|
+
ready: saveReady.value.trim() || void 0,
|
|
363
|
+
base,
|
|
364
|
+
schemaPath: this.schemaPath
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
let probe;
|
|
368
|
+
let saveUrl = SAVE_ENDPOINT;
|
|
369
|
+
const probeSaveEndpoint = () => probe ??= fetch(SAVE_ENDPOINT, { headers: { accept: "application/json" } }).then(async (res) => {
|
|
370
|
+
const isJson = res.ok && res.headers.get("content-type")?.includes("json");
|
|
371
|
+
const probed = isJson ? SaveProbeSchema.safeParse(await res.json()) : void 0;
|
|
372
|
+
if (!probed?.success) return;
|
|
373
|
+
saveUrl = probed.data.url ?? SAVE_ENDPOINT;
|
|
374
|
+
saveProject.textContent = `Save to ${probed.data.dir}`;
|
|
375
|
+
saveProject.hidden = false;
|
|
376
|
+
}).catch(() => {
|
|
377
|
+
});
|
|
378
|
+
const saveToProject = async (overwrite) => {
|
|
379
|
+
const { forCatalog } = capture();
|
|
380
|
+
const res = await fetch(saveUrl, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: { "content-type": "application/json" },
|
|
383
|
+
body: JSON.stringify({ scenario: forCatalog, overwrite })
|
|
384
|
+
});
|
|
385
|
+
const body = SaveResultSchema.catch({}).parse(await res.json().catch(() => ({})));
|
|
386
|
+
if (res.status === 409 && !overwrite) {
|
|
387
|
+
if (confirm(`${body.path} already exists. Replace it?`)) return saveToProject(true);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (!res.ok) throw new Error(body.error ?? `Save failed (HTTP ${res.status})`);
|
|
391
|
+
const message = `Saved ${body.path}. Commit it with your change.`;
|
|
392
|
+
saveStatus.textContent = message;
|
|
393
|
+
try {
|
|
394
|
+
sessionStorage.setItem(FLASH_KEY, JSON.stringify({ message, at: Date.now() }));
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
saveProject.onclick = run(async () => {
|
|
399
|
+
saveStatus.textContent = "";
|
|
400
|
+
await session.settleRecording();
|
|
401
|
+
await saveToProject(false);
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
const flash = FlashSchema.safeParse(JSON.parse(sessionStorage.getItem(FLASH_KEY) ?? "null")).data;
|
|
405
|
+
sessionStorage.removeItem(FLASH_KEY);
|
|
406
|
+
if (flash && Date.now() - flash.at < 1e4) {
|
|
407
|
+
saveStatus.textContent = flash.message;
|
|
408
|
+
saveDetails.open = true;
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
195
412
|
find("[data-act=save-link]", HTMLButtonElement).onclick = run(async () => {
|
|
196
|
-
|
|
413
|
+
await session.settleRecording();
|
|
414
|
+
const { flat } = capture();
|
|
415
|
+
const link = scenarioHref(flat, { inline: true, strict: session.strict });
|
|
197
416
|
saved.hidden = false;
|
|
198
417
|
saved.value = link;
|
|
199
418
|
saved.select();
|
|
200
419
|
await navigator.clipboard?.writeText(link).catch(() => {
|
|
201
420
|
});
|
|
421
|
+
updateReadyCheck();
|
|
202
422
|
});
|
|
203
|
-
find("[data-act=save-json]", HTMLButtonElement).onclick = run(() => {
|
|
204
|
-
|
|
423
|
+
find("[data-act=save-json]", HTMLButtonElement).onclick = run(async () => {
|
|
424
|
+
await session.settleRecording();
|
|
425
|
+
const { forCatalog } = capture();
|
|
205
426
|
const a = document.createElement("a");
|
|
206
427
|
a.href = URL.createObjectURL(
|
|
207
|
-
new Blob([`${JSON.stringify(
|
|
428
|
+
new Blob([`${JSON.stringify(forCatalog, null, 2)}
|
|
208
429
|
`], { type: "application/json" })
|
|
209
430
|
);
|
|
210
|
-
a.download = `${
|
|
431
|
+
a.download = `${forCatalog.name}.json`;
|
|
211
432
|
a.click();
|
|
212
433
|
URL.revokeObjectURL(a.href);
|
|
434
|
+
updateReadyCheck();
|
|
213
435
|
});
|
|
214
436
|
const editor = find(".edit textarea", HTMLTextAreaElement);
|
|
215
437
|
if (readInline(location.hash)) find(".edit", HTMLDetailsElement).open = true;
|
|
@@ -255,21 +477,24 @@ var ScenarioPanel = class extends HTMLElement {
|
|
|
255
477
|
};
|
|
256
478
|
let shownScenario;
|
|
257
479
|
const render = () => {
|
|
258
|
-
const { active, strict, unhandled } = session;
|
|
259
|
-
|
|
480
|
+
const { active, strict, unhandled, recording } = session;
|
|
481
|
+
const mode = recording ? ` (recording ${session.recorded().length})` : strict ? " (strict)" : "";
|
|
482
|
+
toggle.innerHTML = `Scenario: <b>${esc(active?.name ?? "none")}</b>${mode}`;
|
|
260
483
|
const names = (active?.name ?? "").split(",");
|
|
261
484
|
root.querySelectorAll("li[data-name]").forEach((li) => {
|
|
262
485
|
const a = li.querySelector("a");
|
|
263
486
|
if (names.includes(li.dataset.name)) a.setAttribute("aria-current", "true");
|
|
264
487
|
else a.removeAttribute("aria-current");
|
|
265
488
|
});
|
|
266
|
-
find(".unmocked", HTMLElement).innerHTML = unhandled.length ? `<p>${unhandled.length} request(s) not covered by this scenario${strict ? " (answered 501)" : ""}:</p>
|
|
489
|
+
find(".unmocked", HTMLElement).innerHTML = unhandled.length && !recording ? `<p>${unhandled.length} request(s) not covered by this scenario${strict ? " (answered 501)" : ""}:</p>
|
|
267
490
|
<ul>${unhandled.map((u) => `<li><code>${esc(u)}</code></li>`).join("")}</ul>` : "";
|
|
268
491
|
if (active?.name !== shownScenario) {
|
|
269
492
|
shownScenario = active?.name;
|
|
270
493
|
editor.value = JSON.stringify(active ?? { name: "custom", extends: catalog.list[0]?.name }, null, 2);
|
|
494
|
+
fillBaseOptions();
|
|
271
495
|
}
|
|
272
496
|
renderLive();
|
|
497
|
+
if (saveDetails.open) updateReadyCheck();
|
|
273
498
|
};
|
|
274
499
|
this.render = render;
|
|
275
500
|
render();
|
|
@@ -314,15 +539,15 @@ var STYLES = `
|
|
|
314
539
|
}
|
|
315
540
|
#drawer[hidden] { display: none; }
|
|
316
541
|
header, footer, .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
317
|
-
.row { justify-content: flex-start; margin-top: 8px; }
|
|
542
|
+
.row { justify-content: flex-start; margin-top: 8px; flex-wrap: wrap; }
|
|
318
543
|
h2 { margin: 0; font-size: 16px; font-weight: 700; }
|
|
319
544
|
.close { border: 0; background: none; font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 6px; }
|
|
320
|
-
input, textarea {
|
|
545
|
+
input, textarea, select {
|
|
321
546
|
width: 100%; margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line);
|
|
322
547
|
border-radius: 8px; background: var(--bg);
|
|
323
548
|
}
|
|
324
549
|
label { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }
|
|
325
|
-
label input { color: var(--fg); font-size: 14px; }
|
|
550
|
+
label input, label select { color: var(--fg); font-size: 14px; }
|
|
326
551
|
ul { list-style: none; margin: 0; padding: 0; }
|
|
327
552
|
li[hidden] { display: none; }
|
|
328
553
|
.list li { display: flex; align-items: stretch; gap: 4px; }
|
|
@@ -342,7 +567,25 @@ var STYLES = `
|
|
|
342
567
|
textarea { height: 200px; font: 12px/1.45 ui-monospace, monospace; resize: vertical; }
|
|
343
568
|
.hint { margin: 6px 0 0; color: var(--muted); font-size: 12px; }
|
|
344
569
|
.error { color: var(--danger); white-space: pre-wrap; margin: 8px 0; font-size: 12px; }
|
|
345
|
-
.error:empty, .unmocked:empty { display: none; }
|
|
570
|
+
.error:empty, .unmocked:empty, .candidates:empty { display: none; }
|
|
571
|
+
.ready-check, .save-status { color: var(--muted); font-size: 12px; margin: 2px 0 0; }
|
|
572
|
+
.ready-check[data-tone=ok] { color: var(--accent); }
|
|
573
|
+
.ready-check[data-tone=bad] { color: var(--danger); }
|
|
574
|
+
.save-status:empty { display: none; }
|
|
575
|
+
details button[hidden] { display: none; }
|
|
576
|
+
.candidates { margin-top: 6px; }
|
|
577
|
+
.pick-box {
|
|
578
|
+
position: fixed; pointer-events: none; box-sizing: border-box;
|
|
579
|
+
border: 2px solid var(--accent); border-radius: 4px; background: #4747eb14;
|
|
580
|
+
}
|
|
581
|
+
.pick-box[hidden] { display: none; }
|
|
582
|
+
.pick-box span {
|
|
583
|
+
position: absolute; left: -2px; bottom: 100%; margin-bottom: 4px; max-width: 360px;
|
|
584
|
+
padding: 2px 6px; border-radius: 4px; background: var(--accent); color: var(--accent-fg);
|
|
585
|
+
font: 12px/1.4 ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
586
|
+
}
|
|
587
|
+
.pick-box[data-below=true] span { bottom: auto; top: 100%; margin: 4px 0 0; }
|
|
588
|
+
.candidates button { font-size: 12px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
346
589
|
.unmocked { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--hover); color: var(--danger); font-size: 12px; }
|
|
347
590
|
.unmocked p { margin: 0 0 4px; }
|
|
348
591
|
.unmocked ul { padding-left: 16px; list-style: disc; }
|
|
@@ -351,6 +594,7 @@ var STYLES = `
|
|
|
351
594
|
details button, footer button {
|
|
352
595
|
padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--hover);
|
|
353
596
|
}
|
|
597
|
+
button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
|
|
354
598
|
footer { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
|
|
355
599
|
footer a { color: var(--accent); }
|
|
356
600
|
`;
|
package/dist/panel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/panel.ts"],"sourcesContent":["import { PARAM, readInline, scenarioHref, STRICT_PARAM } from './link.js';\nimport type { ScenarioSession } from './runtime.js';\nimport { parseScenario, type Scenario } from './schema.js';\n\nconst OPEN_KEY = 'scenario-panel:open';\n\nconst SCENARIO_PARAMS = new Set([PARAM, STRICT_PARAM]);\n\nexport interface PanelOptions {\n /** Corner to dock in, e.g. to avoid a chat widget. Default \"bottom-right\". */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default: the maximum, so the panel stays above app overlays. */\n zIndex?: number;\n /** \"auto\" follows the OS colour scheme. Default \"auto\". */\n theme?: 'auto' | 'light' | 'dark';\n}\n\n/**\n * Add the floating scenario panel to the page. Framework-agnostic.\n * Remove it with `panel.remove()`; it unsubscribes from the session.\n */\nexport function mountPanel(session: ScenarioSession, options: PanelOptions = {}) {\n if (!customElements.get('scenario-panel')) {\n customElements.define('scenario-panel', ScenarioPanel);\n }\n\n const el = new ScenarioPanel();\n el.session = session;\n const [v, h] = (options.position ?? 'bottom-right').split('-');\n el.dataset.v = v;\n el.dataset.h = h;\n\n if (options.zIndex !== undefined) el.style.zIndex = String(options.zIndex);\n\n if (options.theme && options.theme !== 'auto') el.dataset.theme = options.theme;\n document.body.append(el);\n\n return el;\n}\n\n/**\n * Snapshot what the app shows now as a self-contained scenario: current route and URL params,\n * the active network fixtures, and every connected adapter's live state.\n */\nexport function captureScenario(session: ScenarioSession, name: string, description?: string): Scenario {\n const url: Record<string, string> = {};\n new URLSearchParams(location.search).forEach((v, k) => {\n if (!SCENARIO_PARAMS.has(k)) url[k] = v;\n });\n\n const state = Object.fromEntries(\n session.keys().flatMap((key) => {\n const value = session.read(key);\n\n return value === undefined ? [] : [[key, value] as const];\n }),\n );\n\n const draft = parseScenario({\n name,\n description: description || undefined,\n path: location.pathname,\n url,\n network: session.resolved?.network,\n state,\n });\n\n return session.catalog.resolve(draft); // runs the rules\n}\n\nclass ScenarioPanel extends HTMLElement {\n session!: ScenarioSession;\n private render?: () => void;\n private unsubscribe?: () => void;\n\n disconnectedCallback() {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n }\n\n connectedCallback() {\n // Re-attached (e.g. moved in the DOM): resubscribe and redraw.\n if (this.render) {\n this.unsubscribe = this.session.onChange(this.render);\n this.render();\n\n return;\n }\n\n const session = this.session;\n const { catalog } = session;\n const root = this.attachShadow({ mode: 'open' });\n\n root.innerHTML = `\n <style>${STYLES}</style>\n <button class=\"toggle\" aria-expanded=\"false\" aria-controls=\"drawer\"></button>\n <section id=\"drawer\" aria-label=\"Scenarios\" hidden>\n <header>\n <h2>Scenarios</h2>\n <button class=\"close\" aria-label=\"Close scenarios panel\">×</button>\n </header>\n <p class=\"error\" role=\"alert\"></p>\n <input type=\"search\" placeholder=\"Filter by name, description or tag\" aria-label=\"Filter scenarios\">\n <ul class=\"list\"></ul>\n <div class=\"unmocked\" role=\"status\"></div>\n <section class=\"live\" aria-label=\"Live state\"></section>\n <details class=\"save\">\n <summary>Save as scenario</summary>\n <p class=\"hint\">Current URL, network fixtures and live state, as one shareable scenario.</p>\n <label>Name <input name=\"save-name\" placeholder=\"e.g. checkout-card-declined\" aria-label=\"Scenario name\"></label>\n <label>Description <input name=\"save-description\" aria-label=\"Scenario description\"></label>\n <div class=\"row\">\n <button data-act=\"save-link\">Copy link</button>\n <button data-act=\"save-json\">Download JSON</button>\n </div>\n <input class=\"saved\" readonly aria-label=\"Saved scenario link\" hidden>\n </details>\n <details class=\"edit\">\n <summary>Edit current scenario</summary>\n <textarea spellcheck=\"false\" aria-label=\"Scenario JSON\"></textarea>\n <button data-act=\"apply\">Apply edited scenario</button>\n </details>\n <footer>\n <button data-act=\"copy\">Copy link</button>\n <a class=\"reset\">Reset</a>\n </footer>\n </section>`;\n\n const find = <T extends Element>(selector: string, type: new() => T): T => {\n const el = root.querySelector(selector);\n\n if (!(el instanceof type)) throw new Error(`state-scenarios panel: missing ${selector}`);\n\n return el;\n };\n\n const toggle = find('.toggle', HTMLButtonElement);\n const drawer = find('#drawer', HTMLElement);\n const error = find('.error', HTMLParagraphElement);\n\n const run = (fn: () => void | Promise<void>) => async () => {\n error.textContent = '';\n\n try {\n await fn();\n } catch (err) {\n error.textContent = err instanceof Error ? err.message : String(err);\n }\n };\n\n const href = (s: Scenario) => {\n try {\n return { href: scenarioHref(catalog.resolve(s), { strict: session.strict }), invalid: false };\n } catch {\n return { href: `?${PARAM}=${encodeURIComponent(s.name)}`, invalid: true };\n }\n };\n\n find('.list', HTMLUListElement).innerHTML = catalog.list\n .map((s) => ({ s, ...href(s) }))\n .map(\n ({ s, href, invalid }) => `\n <li data-name=\"${esc(s.name)}\" data-search=\"${\n esc([s.name, s.description, ...(s.tags ?? [])].join(' ').toLowerCase())\n }\">\n <a href=\"${esc(href)}\">\n <strong>${esc(s.name)}${invalid ? ' <em>(invalid, opens with error)</em>' : ''}</strong>\n ${s.description ? `<span>${esc(s.description)}</span>` : ''}\n ${s.tags?.length ? `<small>${s.tags.map(esc).join(' · ')}</small>` : ''}\n </a>\n <button class=\"add\" aria-label=\"Combine ${\n esc(s.name)\n } with the current scenario\" title=\"Combine with the current scenario\">+</button>\n </li>`,\n )\n .join('');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const name = li.dataset.name!;\n li.querySelector('a')!.onclick = (e) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey) return; // let the browser open a new tab\n e.preventDefault();\n void run(() => session.open(name))();\n };\n\n li.querySelector<HTMLButtonElement>('.add')!.onclick = run(() => {\n const names = new Set((session.active?.name ?? '').split(',').filter(Boolean));\n names.add(name);\n\n return session.open([...names].join(','));\n });\n });\n const filter = find('input[type=search]', HTMLInputElement);\n filter.oninput = () => {\n const q = filter.value.toLowerCase().trim();\n root.querySelectorAll<HTMLLIElement>('li[data-search]').forEach((li) => {\n li.hidden = !li.dataset.search!.includes(q);\n });\n };\n\n const editors = new Map<string, { textarea: HTMLTextAreaElement; dirty: boolean; }>();\n\n const renderLive = () => {\n const keys = session.keys();\n\n for (const [key, ed] of editors) {\n if (!keys.includes(key)) {\n ed.textarea.closest('details')!.remove();\n editors.delete(key);\n }\n }\n\n for (const key of keys) {\n let ed = editors.get(key);\n\n if (!ed) {\n const details = document.createElement('details');\n details.innerHTML = `\n <summary>Live state: <code>${esc(key)}</code></summary>\n <textarea spellcheck=\"false\" aria-label=\"Live state ${esc(key)}\"></textarea>\n <div class=\"row\"><button data-act=\"live-apply\">Apply</button><button data-act=\"live-revert\">Revert</button></div>`;\n find('.live', HTMLElement).append(details);\n const entry = { textarea: details.querySelector('textarea')!, dirty: false };\n entry.textarea.oninput = () => (entry.dirty = true);\n details.ontoggle = () => renderLive();\n details.querySelector<HTMLButtonElement>('[data-act=live-apply]')!.onclick = run(async () => {\n await session.applyState(key, JSON.parse(entry.textarea.value));\n entry.dirty = false;\n renderLive();\n });\n details.querySelector<HTMLButtonElement>('[data-act=live-revert]')!.onclick = () => {\n entry.dirty = false;\n renderLive();\n };\n\n editors.set(key, ed = entry);\n }\n\n // Follow the app while open (state can be large), never overwriting what someone is typing.\n if (!ed.dirty && ed.textarea.closest('details')!.open) {\n ed.textarea.value = JSON.stringify(session.read(key) ?? null, null, 2);\n }\n }\n };\n\n const saveName = find('input[name=save-name]', HTMLInputElement);\n const saveDescription = find('input[name=save-description]', HTMLInputElement);\n const saved = find('.saved', HTMLInputElement);\n const capture = () => captureScenario(session, saveName.value.trim(), saveDescription.value.trim());\n find('[data-act=save-link]', HTMLButtonElement).onclick = run(async () => {\n const link = scenarioHref(capture(), { inline: true, strict: session.strict });\n saved.hidden = false;\n saved.value = link;\n saved.select();\n await navigator.clipboard?.writeText(link).catch(() => {});\n });\n find('[data-act=save-json]', HTMLButtonElement).onclick = run(() => {\n const scenario = capture();\n const a = document.createElement('a');\n a.href = URL.createObjectURL(\n new Blob([`${JSON.stringify(scenario, null, 2)}\\n`], { type: 'application/json' }),\n );\n a.download = `${scenario.name}.json`;\n a.click();\n URL.revokeObjectURL(a.href);\n });\n\n const editor = find('.edit textarea', HTMLTextAreaElement);\n\n if (readInline(location.hash)) find('.edit', HTMLDetailsElement).open = true;\n find('[data-act=apply]', HTMLButtonElement).onclick = run(() =>\n session.openInline(parseScenario(JSON.parse(editor.value)))\n );\n\n find('[data-act=copy]', HTMLButtonElement).onclick = run(async () => {\n try {\n await navigator.clipboard.writeText(location.href);\n } catch {\n prompt('Copy this link', location.href);\n }\n });\n // An absolute same-origin URL: a path starting with \"//\" must not read as another host.\n const reset = new URL(location.href);\n reset.search = '';\n reset.hash = '';\n reset.searchParams.set(PARAM, '');\n find('.reset', HTMLAnchorElement).href = reset.href;\n\n const setOpen = (open: boolean) => {\n drawer.hidden = !open;\n toggle.setAttribute('aria-expanded', String(open));\n\n try {\n sessionStorage.setItem(OPEN_KEY, open ? '1' : '');\n } catch {}\n };\n\n let stored: string | null = null;\n\n try {\n stored = sessionStorage.getItem(OPEN_KEY);\n } catch {}\n\n setOpen(!!stored);\n toggle.onclick = () => setOpen(!!drawer.hidden);\n find('.close', HTMLButtonElement).onclick = () => {\n setOpen(false);\n toggle.focus();\n };\n\n drawer.onkeydown = (e) => {\n if (e.key === 'Escape') {\n setOpen(false);\n toggle.focus();\n }\n };\n\n let shownScenario: string | undefined;\n\n const render = () => {\n const { active, strict, unhandled } = session;\n toggle.innerHTML = `Scenario: <b>${esc(active?.name ?? 'none')}</b>${strict ? ' (strict)' : ''}`;\n const names = (active?.name ?? '').split(',');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const a = li.querySelector('a')!;\n\n if (names.includes(li.dataset.name!)) a.setAttribute('aria-current', 'true');\n else a.removeAttribute('aria-current');\n });\n find('.unmocked', HTMLElement).innerHTML = unhandled.length\n ? `<p>${unhandled.length} request(s) not covered by this scenario${\n strict ? ' (answered 501)' : ''\n }:</p>\n <ul>${unhandled.map((u) => `<li><code>${esc(u)}</code></li>`).join('')}</ul>`\n : '';\n\n // Set as a property, not markup, so page text selectors never match the panel's JSON.\n if (active?.name !== shownScenario) {\n shownScenario = active?.name;\n editor.value = JSON.stringify(active ?? { name: 'custom', extends: catalog.list[0]?.name }, null, 2);\n }\n\n renderLive();\n };\n\n this.render = render;\n render();\n this.unsubscribe = session.onChange(render);\n }\n}\n\nconst esc = (s: string) => s.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n\nconst STYLES = `\n :host {\n --bg: #ffffff; --fg: #1b1b1f; --muted: #5d5d66; --line: #dcdce2;\n --accent: #4747eb; --accent-fg: #ffffff; --hover: #f2f2f7; --danger: #b42318;\n all: initial; position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;\n font: 14px/1.4 system-ui, sans-serif; color: var(--fg);\n }\n @media (prefers-color-scheme: dark) {\n :host(:not([data-theme=\"light\"])) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n }\n :host([data-theme=\"dark\"]) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n :host([data-v=\"top\"]) { bottom: auto; top: 16px; }\n :host([data-h=\"left\"]) { right: auto; left: 16px; }\n :host([data-v=\"top\"]) #drawer { bottom: auto; top: 48px; }\n :host([data-h=\"left\"]) #drawer { right: auto; left: 0; }\n :host([data-h=\"left\"]) .toggle { margin-left: 0; }\n * { box-sizing: border-box; font: inherit; color: inherit; }\n button, a { cursor: pointer; }\n :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n .toggle {\n display: block; margin-left: auto; padding: 8px 14px; border: 0; border-radius: 999px;\n background: var(--accent); color: var(--accent-fg); box-shadow: 0 4px 16px #0003;\n }\n .toggle b, strong { font-weight: 700; }\n #drawer {\n position: absolute; right: 0; bottom: 48px; width: min(400px, calc(100vw - 32px));\n max-height: min(680px, calc(100vh - 96px)); overflow: auto; padding: 12px;\n background: var(--bg); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 12px 40px #0004;\n }\n #drawer[hidden] { display: none; }\n header, footer, .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n .row { justify-content: flex-start; margin-top: 8px; }\n h2 { margin: 0; font-size: 16px; font-weight: 700; }\n .close { border: 0; background: none; font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 6px; }\n input, textarea {\n width: 100%; margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line);\n border-radius: 8px; background: var(--bg);\n }\n label { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }\n label input { color: var(--fg); font-size: 14px; }\n ul { list-style: none; margin: 0; padding: 0; }\n li[hidden] { display: none; }\n .list li { display: flex; align-items: stretch; gap: 4px; }\n .list a {\n flex: 1; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px;\n text-decoration: none; border-left: 3px solid transparent;\n }\n .list a:hover { background: var(--hover); }\n .list a[aria-current] { border-left-color: var(--accent); background: var(--hover); }\n .list span { color: var(--muted); }\n .list small { color: var(--muted); font-size: 12px; }\n .add { width: 32px; border: 0; border-radius: 8px; background: none; color: var(--muted); font-size: 18px; }\n .add:hover { background: var(--hover); color: var(--fg); }\n details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n summary { cursor: pointer; font-weight: 600; }\n code { font: 12px ui-monospace, monospace; }\n textarea { height: 200px; font: 12px/1.45 ui-monospace, monospace; resize: vertical; }\n .hint { margin: 6px 0 0; color: var(--muted); font-size: 12px; }\n .error { color: var(--danger); white-space: pre-wrap; margin: 8px 0; font-size: 12px; }\n .error:empty, .unmocked:empty { display: none; }\n .unmocked { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--hover); color: var(--danger); font-size: 12px; }\n .unmocked p { margin: 0 0 4px; }\n .unmocked ul { padding-left: 16px; list-style: disc; }\n .unmocked code { overflow-wrap: anywhere; }\n em { font-style: normal; color: var(--danger); font-weight: 400; }\n details button, footer button {\n padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--hover);\n }\n footer { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n footer a { color: var(--accent); }\n`;\n"],"mappings":"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","names":["href"]}
|
|
1
|
+
{"version":3,"sources":["../src/panel.ts"],"sourcesContent":["import { z } from 'zod';\nimport { captureScenario } from './capture.js';\nimport { PARAM, readInline, RECORD_PARAM, SAVE_ENDPOINT, scenarioHref, STRICT_PARAM } from './link.js';\nimport { checkReadySelector, closestReadySelector, suggestReadySelectors } from './ready.js';\nimport type { ScenarioSession } from './runtime.js';\nimport { parseScenario, type Scenario } from './schema.js';\n\nconst OPEN_KEY = 'scenario-panel:open';\n\n// \"Saved …\" survives the reload Vite does when a new file lands in the scenarios glob.\nconst FLASH_KEY = 'scenario-panel:flash';\n\nconst SaveProbeSchema = z.object({ dir: z.string(), url: z.string().optional() });\n\nconst SaveResultSchema = z.object({ path: z.string().optional(), error: z.string().optional() });\n\nconst FlashSchema = z.object({ message: z.string(), at: z.number() });\n\nexport interface PanelOptions {\n /** Corner to dock in, e.g. to avoid a chat widget. Default \"bottom-right\". */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default: the maximum, so the panel stays above app overlays. */\n zIndex?: number;\n /** \"auto\" follows the OS colour scheme. Default \"auto\". */\n theme?: 'auto' | 'light' | 'dark';\n /**\n * Relative `$schema` path stamped on Download JSON, e.g. `../scenario.schema.json`.\n * Copy link stays schema-free.\n */\n schemaPath?: string;\n}\n\n/**\n * Add the floating scenario panel to the page. Framework-agnostic.\n * Remove it with `panel.remove()`; it unsubscribes from the session.\n */\nexport function mountPanel(session: ScenarioSession, options: PanelOptions = {}) {\n if (!customElements.get('scenario-panel')) {\n customElements.define('scenario-panel', ScenarioPanel);\n }\n\n const el = new ScenarioPanel();\n el.session = session;\n el.schemaPath = options.schemaPath;\n const [v, h] = (options.position ?? 'bottom-right').split('-');\n el.dataset.v = v;\n el.dataset.h = h;\n\n if (options.zIndex !== undefined) el.style.zIndex = String(options.zIndex);\n\n if (options.theme && options.theme !== 'auto') el.dataset.theme = options.theme;\n document.body.append(el);\n\n return el;\n}\n\n// Kept for imports from before captureScenario moved to the main entry.\nexport { type CapturedScenario, type CaptureOptions, captureScenario } from './capture.js';\n\n// Next.js loads client modules on the server too, where HTMLElement is undefined. Only\n// mountPanel creates a panel, and it runs in a browser, so on the server any base will do.\nconst Base: typeof HTMLElement = globalThis.HTMLElement ?? Object;\n\nclass ScenarioPanel extends Base {\n session!: ScenarioSession;\n schemaPath?: string;\n private render?: () => void;\n private unsubscribe?: () => void;\n private stopPick?: () => void;\n\n disconnectedCallback() {\n this.unsubscribe?.();\n this.unsubscribe = undefined;\n this.stopPick?.();\n this.stopPick = undefined;\n }\n\n connectedCallback() {\n // Re-attached (e.g. moved in the DOM): resubscribe and redraw.\n if (this.render) {\n this.unsubscribe = this.session.onChange(this.render);\n this.render();\n\n return;\n }\n\n const session = this.session;\n const { catalog } = session;\n const root = this.attachShadow({ mode: 'open' });\n\n root.innerHTML = `\n <style>${STYLES}</style>\n <button class=\"toggle\" aria-expanded=\"false\" aria-controls=\"drawer\"></button>\n <section id=\"drawer\" aria-label=\"Scenarios\" hidden>\n <header>\n <h2>Scenarios</h2>\n <button class=\"close\" aria-label=\"Close scenarios panel\">×</button>\n </header>\n <p class=\"error\" role=\"alert\"></p>\n <input type=\"search\" placeholder=\"Filter by name, description or tag\" aria-label=\"Filter scenarios\">\n <ul class=\"list\"></ul>\n <div class=\"unmocked\" role=\"status\"></div>\n <section class=\"live\" aria-label=\"Live state\"></section>\n <details class=\"save\">\n <summary>Save as scenario</summary>\n <p class=\"hint\">Saves the current URL, network responses and live state. <b>Copy link</b> shares it as is; <b>Download JSON</b> writes a file for your scenarios folder that keeps only what differs from the base.</p>\n <label>Name <input name=\"save-name\" placeholder=\"e.g. checkout-card-declined\" aria-label=\"Scenario name\"></label>\n <label>Description <input name=\"save-description\" aria-label=\"Scenario description\"></label>\n <label>Ready: what's on screen once this state has rendered <input name=\"save-ready\" placeholder='Use Mark ready, or type text=\"…\"' aria-label=\"Ready selector\"></label>\n <p class=\"ready-check\" role=\"status\"></p>\n <div class=\"row\">\n <button type=\"button\" data-act=\"mark-ready\">Mark ready</button>\n <button type=\"button\" data-act=\"clear-ready\">Clear</button>\n </div>\n <div class=\"candidates\" role=\"group\" aria-label=\"Suggested ready selectors\"></div>\n <div class=\"pick-box\" hidden><span></span></div>\n <label>Extends\n <select name=\"save-base\" aria-label=\"Base scenario to extend\"></select>\n </label>\n <div class=\"row\">\n <button data-act=\"save-project\" hidden>Save to project</button>\n <button data-act=\"save-link\">Copy link</button>\n <button data-act=\"save-json\">Download JSON</button>\n </div>\n <p class=\"save-status\" role=\"status\"></p>\n <input class=\"saved\" readonly aria-label=\"Saved scenario link\" hidden>\n </details>\n <details class=\"edit\">\n <summary>Edit current scenario</summary>\n <textarea spellcheck=\"false\" aria-label=\"Scenario JSON\"></textarea>\n <button data-act=\"apply\">Apply edited scenario</button>\n </details>\n <footer>\n <button data-act=\"copy\">Copy link</button>\n <a class=\"reset\">Reset</a>\n </footer>\n </section>`;\n\n const find = <T extends Element>(selector: string, type: new() => T): T => {\n const el = root.querySelector(selector);\n\n if (!(el instanceof type)) throw new Error(`state-scenarios panel: missing ${selector}`);\n\n return el;\n };\n\n const toggle = find('.toggle', HTMLButtonElement);\n const drawer = find('#drawer', HTMLElement);\n const error = find('.error', HTMLParagraphElement);\n\n const run = (fn: () => void | Promise<void>) => async () => {\n error.textContent = '';\n\n try {\n await fn();\n } catch (err) {\n error.textContent = err instanceof Error ? err.message : String(err);\n }\n };\n\n const href = (s: Scenario) => {\n try {\n return { href: scenarioHref(catalog.resolve(s), { strict: session.strict }), invalid: false };\n } catch {\n return { href: `?${PARAM}=${encodeURIComponent(s.name)}`, invalid: true };\n }\n };\n\n find('.list', HTMLUListElement).innerHTML = catalog.list\n .map((s) => ({ s, ...href(s) }))\n .map(\n ({ s, href, invalid }) => `\n <li data-name=\"${esc(s.name)}\" data-search=\"${\n esc([s.name, s.description, ...(s.tags ?? [])].join(' ').toLowerCase())\n }\">\n <a href=\"${esc(href)}\">\n <strong>${esc(s.name)}${invalid ? ' <em>(invalid, opens with error)</em>' : ''}</strong>\n ${s.description ? `<span>${esc(s.description)}</span>` : ''}\n ${s.tags?.length ? `<small>${s.tags.map(esc).join(' · ')}</small>` : ''}\n </a>\n <button class=\"add\" aria-label=\"Combine ${\n esc(s.name)\n } with the current scenario\" title=\"Combine with the current scenario\">+</button>\n </li>`,\n )\n .join('');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const name = li.dataset.name!;\n li.querySelector('a')!.onclick = (e) => {\n if (e.metaKey || e.ctrlKey || e.shiftKey) return; // let the browser open a new tab\n e.preventDefault();\n void run(() => session.open(name))();\n };\n\n li.querySelector<HTMLButtonElement>('.add')!.onclick = run(() => {\n const names = new Set((session.active?.name ?? '').split(',').filter(Boolean));\n names.add(name);\n\n return session.open([...names].join(','));\n });\n });\n const filter = find('input[type=search]', HTMLInputElement);\n filter.oninput = () => {\n const q = filter.value.toLowerCase().trim();\n root.querySelectorAll<HTMLLIElement>('li[data-search]').forEach((li) => {\n li.hidden = !li.dataset.search!.includes(q);\n });\n };\n\n const editors = new Map<string, { textarea: HTMLTextAreaElement; dirty: boolean; }>();\n\n const renderLive = () => {\n const keys = session.keys();\n\n for (const [key, ed] of editors) {\n if (!keys.includes(key)) {\n ed.textarea.closest('details')!.remove();\n editors.delete(key);\n }\n }\n\n for (const key of keys) {\n let ed = editors.get(key);\n\n if (!ed) {\n const details = document.createElement('details');\n details.innerHTML = `\n <summary>Live state: <code>${esc(key)}</code></summary>\n <textarea spellcheck=\"false\" aria-label=\"Live state ${esc(key)}\"></textarea>\n <div class=\"row\"><button data-act=\"live-apply\">Apply</button><button data-act=\"live-revert\">Revert</button></div>`;\n find('.live', HTMLElement).append(details);\n const entry = { textarea: details.querySelector('textarea')!, dirty: false };\n entry.textarea.oninput = () => (entry.dirty = true);\n details.ontoggle = () => renderLive();\n details.querySelector<HTMLButtonElement>('[data-act=live-apply]')!.onclick = run(async () => {\n await session.applyState(key, JSON.parse(entry.textarea.value));\n entry.dirty = false;\n renderLive();\n });\n details.querySelector<HTMLButtonElement>('[data-act=live-revert]')!.onclick = () => {\n entry.dirty = false;\n renderLive();\n };\n\n editors.set(key, ed = entry);\n }\n\n // Follow the app while open (state can be large), never overwriting what someone is typing.\n if (!ed.dirty && ed.textarea.closest('details')!.open) {\n ed.textarea.value = JSON.stringify(session.read(key) ?? null, null, 2);\n }\n }\n };\n\n const saveName = find('input[name=save-name]', HTMLInputElement);\n const saveDescription = find('input[name=save-description]', HTMLInputElement);\n const saveReady = find('input[name=save-ready]', HTMLInputElement);\n const saveBase = find('select[name=save-base]', HTMLSelectElement);\n const readyCheck = find('.ready-check', HTMLParagraphElement);\n const saveProject = find('[data-act=save-project]', HTMLButtonElement);\n const saveStatus = find('.save-status', HTMLParagraphElement);\n const candidates = find('.candidates', HTMLElement);\n const saved = find('.saved', HTMLInputElement);\n const saveDetails = find('.save', HTMLDetailsElement);\n const markReadyBtn = find('[data-act=mark-ready]', HTMLButtonElement);\n\n const fillBaseOptions = () => {\n const preferred = catalog.get('default')?.name\n ?? [session.active?.extends ?? []].flat()[0]\n ?? '';\n\n const previous = saveBase.value;\n\n saveBase.innerHTML = `<option value=\"\">(none: save everything)</option>`\n + catalog.list.map((s) => `<option value=\"${esc(s.name)}\">${esc(s.name)}</option>`).join('');\n\n if (previous && [...saveBase.options].some((o) => o.value === previous)) {\n saveBase.value = previous;\n } else {\n saveBase.value = preferred;\n }\n };\n\n fillBaseOptions();\n\n const renderCandidates = () => {\n const picks = suggestReadySelectors(document.body, { limit: 3 });\n candidates.innerHTML = picks.length\n ? `<p class=\"hint\">Suggestions</p><div class=\"row\">${\n picks.map((s) => `<button type=\"button\" data-candidate=\"${esc(s)}\">${esc(s)}</button>`).join('')\n }</div>`\n : '';\n candidates.querySelectorAll<HTMLButtonElement>('[data-candidate]').forEach((btn) => {\n btn.onclick = () => {\n saveReady.value = btn.dataset.candidate ?? '';\n updateReadyCheck();\n };\n });\n };\n\n saveDetails.ontoggle = () => {\n if (saveDetails.open) {\n fillBaseOptions();\n // Offer, don't fill: the top suggestion is often chrome present in every state.\n renderCandidates();\n updateReadyCheck();\n void probeSaveEndpoint();\n }\n };\n\n const stopPickMode = () => {\n this.stopPick?.();\n this.stopPick = undefined;\n markReadyBtn.setAttribute('aria-pressed', 'false');\n markReadyBtn.textContent = 'Mark ready';\n };\n\n const pickBox = find('.pick-box', HTMLDivElement);\n const pickLabel = find('.pick-box span', HTMLSpanElement);\n\n const outline = (el: Element | undefined, label: string) => {\n if (!el) {\n pickBox.hidden = true;\n\n return;\n }\n\n const r = el.getBoundingClientRect();\n Object.assign(pickBox.style, {\n top: `${r.top}px`,\n left: `${r.left}px`,\n width: `${r.width}px`,\n height: `${r.height}px`,\n });\n pickBox.dataset.below = String(r.top < 28);\n pickLabel.textContent = label;\n pickBox.hidden = false;\n };\n\n // Elements under the pointer, outside the panel, innermost first.\n const appTarget = (e: Event) => {\n const path = e.composedPath();\n\n if (path.includes(this)) return undefined;\n\n return path.find((n): n is Element =>\n n instanceof Element && n !== document.documentElement && n !== document.body\n );\n };\n\n const READY_MESSAGES = {\n empty:\n \"No ready selector: the link still works, but shoot and tests won't wait for this state to render.\",\n none: 'No match on this page: tests would time out waiting for it.',\n unchecked: \"Can't check this kind of selector here; Playwright still can.\",\n };\n\n let readyMatch: Element | undefined;\n\n const updateReadyCheck = () => {\n const value = saveReady.value.trim();\n const check = value ? checkReadySelector(value) : undefined;\n readyMatch = check?.status === 'match' ? check.first : undefined;\n\n let tone = 'muted';\n let message: string;\n\n if (!check) message = READY_MESSAGES.empty;\n else if (check.status === 'invalid') [tone, message] = ['bad', check.error];\n else if (check.status === 'none') [tone, message] = ['bad', READY_MESSAGES.none];\n else if (check.status === 'unchecked') message = READY_MESSAGES.unchecked;\n else if (!check.visible) {\n message = 'Matches, but the first match is hidden: tests wait until it shows.';\n } else if (check.count > 1) {\n message = `Matches ${check.count} elements; tests wait for the first (outlined).`;\n } else [tone, message] = ['ok', '✓ Matches 1 element on this page.'];\n\n readyCheck.dataset.tone = tone;\n readyCheck.textContent = message;\n\n // While the field has focus, show which element tests will wait on.\n if (this.shadowRoot?.activeElement === saveReady && !this.stopPick) outline(readyMatch, 'ready');\n };\n\n saveReady.oninput = updateReadyCheck;\n saveReady.onfocus = updateReadyCheck;\n\n saveReady.onblur = () => {\n if (!this.stopPick) outline(undefined, '');\n };\n\n updateReadyCheck();\n\n const startPickMode = () => {\n stopPickMode();\n markReadyBtn.setAttribute('aria-pressed', 'true');\n markReadyBtn.textContent = 'Click the UI… (Esc cancels)';\n document.documentElement.style.cursor = 'crosshair';\n\n const onMove = (e: MouseEvent) => {\n const target = appTarget(e);\n const hit = target && closestReadySelector(target);\n\n if (hit) outline(hit.element, hit.selector);\n else outline(target, 'No selector here: try text, a heading or a button');\n };\n\n // Swallow presses so the app doesn't react (open menus, follow links) while picking.\n const block = (e: Event) => {\n if (!appTarget(e)) return;\n e.preventDefault();\n e.stopPropagation();\n };\n\n const onClick = (e: MouseEvent) => {\n const target = appTarget(e);\n\n if (!target) return;\n block(e);\n const hit = closestReadySelector(target);\n\n // Stay in pick mode so the user can try another element; the outline says why.\n if (!hit) return;\n\n saveReady.value = hit.selector;\n updateReadyCheck();\n stopPickMode();\n saveReady.focus();\n };\n\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n // Esc only cancels picking; without this the drawer's Esc handler also closes the panel.\n e.stopPropagation();\n stopPickMode();\n };\n\n const pressEvents = ['pointerdown', 'mousedown', 'pointerup', 'mouseup'] as const;\n\n document.addEventListener('mousemove', onMove, true);\n document.addEventListener('click', onClick, true);\n document.addEventListener('keydown', onKey, true);\n\n for (const type of pressEvents) document.addEventListener(type, block, true);\n\n this.stopPick = () => {\n document.removeEventListener('mousemove', onMove, true);\n document.removeEventListener('click', onClick, true);\n document.removeEventListener('keydown', onKey, true);\n\n for (const type of pressEvents) document.removeEventListener(type, block, true);\n\n document.documentElement.style.removeProperty('cursor');\n pickBox.hidden = true;\n };\n };\n\n markReadyBtn.onclick = () => {\n if (markReadyBtn.getAttribute('aria-pressed') === 'true') stopPickMode();\n else startPickMode();\n };\n\n find('[data-act=clear-ready]', HTMLButtonElement).onclick = () => {\n saveReady.value = '';\n updateReadyCheck();\n };\n\n const capture = () => {\n const baseName = saveBase.value;\n const base = baseName ? catalog.get(baseName) : undefined;\n\n return captureScenario(session, {\n name: saveName.value.trim(),\n description: saveDescription.value.trim(),\n ready: saveReady.value.trim() || undefined,\n base,\n schemaPath: this.schemaPath,\n });\n };\n\n let probe: Promise<void> | undefined;\n // The Next.js route answers with a loopback save server's URL; Vite saves on its own origin.\n let saveUrl = SAVE_ENDPOINT;\n\n // The Vite plugin or the Next.js route answers with JSON; anything else (a build, an app\n // without either) returns HTML or a 404, and the button stays hidden.\n const probeSaveEndpoint = () =>\n probe ??= fetch(SAVE_ENDPOINT, { headers: { accept: 'application/json' } })\n .then(async (res) => {\n const isJson = res.ok && res.headers.get('content-type')?.includes('json');\n const probed = isJson ? SaveProbeSchema.safeParse(await res.json()) : undefined;\n\n if (!probed?.success) return;\n saveUrl = probed.data.url ?? SAVE_ENDPOINT;\n saveProject.textContent = `Save to ${probed.data.dir}`;\n saveProject.hidden = false;\n })\n .catch(() => {});\n\n const saveToProject = async (overwrite: boolean): Promise<void> => {\n const { forCatalog } = capture();\n\n const res = await fetch(saveUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ scenario: forCatalog, overwrite }),\n });\n\n const body = SaveResultSchema.catch({}).parse(await res.json().catch(() => ({})));\n\n if (res.status === 409 && !overwrite) {\n if (confirm(`${body.path} already exists. Replace it?`)) return saveToProject(true);\n\n return;\n }\n\n if (!res.ok) throw new Error(body.error ?? `Save failed (HTTP ${res.status})`);\n\n const message = `Saved ${body.path}. Commit it with your change.`;\n saveStatus.textContent = message;\n\n try {\n sessionStorage.setItem(FLASH_KEY, JSON.stringify({ message, at: Date.now() }));\n } catch {\n // Storage blocked: the message won't survive a reload.\n }\n };\n\n saveProject.onclick = run(async () => {\n saveStatus.textContent = '';\n await session.settleRecording();\n await saveToProject(false);\n });\n\n try {\n const flash = FlashSchema.safeParse(JSON.parse(sessionStorage.getItem(FLASH_KEY) ?? 'null')).data;\n sessionStorage.removeItem(FLASH_KEY);\n\n // Only right after a save; a stale one would describe some other session.\n if (flash && Date.now() - flash.at < 10_000) {\n saveStatus.textContent = flash.message;\n saveDetails.open = true;\n }\n } catch {\n // Storage blocked or corrupt: nothing to show.\n }\n\n find('[data-act=save-link]', HTMLButtonElement).onclick = run(async () => {\n await session.settleRecording();\n const { flat } = capture();\n const link = scenarioHref(flat, { inline: true, strict: session.strict });\n saved.hidden = false;\n saved.value = link;\n saved.select();\n await navigator.clipboard?.writeText(link).catch(() => {});\n updateReadyCheck();\n });\n find('[data-act=save-json]', HTMLButtonElement).onclick = run(async () => {\n await session.settleRecording();\n const { forCatalog } = capture();\n const a = document.createElement('a');\n a.href = URL.createObjectURL(\n new Blob([`${JSON.stringify(forCatalog, null, 2)}\\n`], { type: 'application/json' }),\n );\n a.download = `${forCatalog.name}.json`;\n a.click();\n URL.revokeObjectURL(a.href);\n updateReadyCheck();\n });\n\n const editor = find('.edit textarea', HTMLTextAreaElement);\n\n if (readInline(location.hash)) find('.edit', HTMLDetailsElement).open = true;\n find('[data-act=apply]', HTMLButtonElement).onclick = run(() =>\n session.openInline(parseScenario(JSON.parse(editor.value)))\n );\n\n find('[data-act=copy]', HTMLButtonElement).onclick = run(async () => {\n try {\n await navigator.clipboard.writeText(location.href);\n } catch {\n prompt('Copy this link', location.href);\n }\n });\n // An absolute same-origin URL: a path starting with \"//\" must not read as another host.\n const reset = new URL(location.href);\n reset.search = '';\n reset.hash = '';\n reset.searchParams.set(PARAM, '');\n find('.reset', HTMLAnchorElement).href = reset.href;\n\n const setOpen = (open: boolean) => {\n drawer.hidden = !open;\n toggle.setAttribute('aria-expanded', String(open));\n\n try {\n sessionStorage.setItem(OPEN_KEY, open ? '1' : '');\n } catch {}\n };\n\n let stored: string | null = null;\n\n try {\n stored = sessionStorage.getItem(OPEN_KEY);\n } catch {}\n\n setOpen(!!stored);\n toggle.onclick = () => setOpen(!!drawer.hidden);\n find('.close', HTMLButtonElement).onclick = () => {\n setOpen(false);\n toggle.focus();\n };\n\n drawer.onkeydown = (e) => {\n if (e.key === 'Escape') {\n setOpen(false);\n toggle.focus();\n }\n };\n\n let shownScenario: string | undefined;\n\n const render = () => {\n const { active, strict, unhandled, recording } = session;\n const mode = recording ? ` (recording ${session.recorded().length})` : strict ? ' (strict)' : '';\n toggle.innerHTML = `Scenario: <b>${esc(active?.name ?? 'none')}</b>${mode}`;\n const names = (active?.name ?? '').split(',');\n root.querySelectorAll<HTMLLIElement>('li[data-name]').forEach((li) => {\n const a = li.querySelector('a')!;\n\n if (names.includes(li.dataset.name!)) a.setAttribute('aria-current', 'true');\n else a.removeAttribute('aria-current');\n });\n // While recording, unmocked requests are the point: they're what gets saved.\n find('.unmocked', HTMLElement).innerHTML = unhandled.length && !recording\n ? `<p>${unhandled.length} request(s) not covered by this scenario${\n strict ? ' (answered 501)' : ''\n }:</p>\n <ul>${unhandled.map((u) => `<li><code>${esc(u)}</code></li>`).join('')}</ul>`\n : '';\n\n // Set as a property, not markup, so page text selectors never match the panel's JSON.\n if (active?.name !== shownScenario) {\n shownScenario = active?.name;\n editor.value = JSON.stringify(active ?? { name: 'custom', extends: catalog.list[0]?.name }, null, 2);\n fillBaseOptions();\n }\n\n renderLive();\n\n // The app re-rendered: the ready selector may match now, or no longer.\n if (saveDetails.open) updateReadyCheck();\n };\n\n this.render = render;\n render();\n this.unsubscribe = session.onChange(render);\n }\n}\n\nconst esc = (s: string) => s.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n\nconst STYLES = `\n :host {\n --bg: #ffffff; --fg: #1b1b1f; --muted: #5d5d66; --line: #dcdce2;\n --accent: #4747eb; --accent-fg: #ffffff; --hover: #f2f2f7; --danger: #b42318;\n all: initial; position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;\n font: 14px/1.4 system-ui, sans-serif; color: var(--fg);\n }\n @media (prefers-color-scheme: dark) {\n :host(:not([data-theme=\"light\"])) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n }\n :host([data-theme=\"dark\"]) {\n --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;\n --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;\n }\n :host([data-v=\"top\"]) { bottom: auto; top: 16px; }\n :host([data-h=\"left\"]) { right: auto; left: 16px; }\n :host([data-v=\"top\"]) #drawer { bottom: auto; top: 48px; }\n :host([data-h=\"left\"]) #drawer { right: auto; left: 0; }\n :host([data-h=\"left\"]) .toggle { margin-left: 0; }\n * { box-sizing: border-box; font: inherit; color: inherit; }\n button, a { cursor: pointer; }\n :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n .toggle {\n display: block; margin-left: auto; padding: 8px 14px; border: 0; border-radius: 999px;\n background: var(--accent); color: var(--accent-fg); box-shadow: 0 4px 16px #0003;\n }\n .toggle b, strong { font-weight: 700; }\n #drawer {\n position: absolute; right: 0; bottom: 48px; width: min(400px, calc(100vw - 32px));\n max-height: min(680px, calc(100vh - 96px)); overflow: auto; padding: 12px;\n background: var(--bg); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 12px 40px #0004;\n }\n #drawer[hidden] { display: none; }\n header, footer, .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }\n .row { justify-content: flex-start; margin-top: 8px; flex-wrap: wrap; }\n h2 { margin: 0; font-size: 16px; font-weight: 700; }\n .close { border: 0; background: none; font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 6px; }\n input, textarea, select {\n width: 100%; margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line);\n border-radius: 8px; background: var(--bg);\n }\n label { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }\n label input, label select { color: var(--fg); font-size: 14px; }\n ul { list-style: none; margin: 0; padding: 0; }\n li[hidden] { display: none; }\n .list li { display: flex; align-items: stretch; gap: 4px; }\n .list a {\n flex: 1; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px;\n text-decoration: none; border-left: 3px solid transparent;\n }\n .list a:hover { background: var(--hover); }\n .list a[aria-current] { border-left-color: var(--accent); background: var(--hover); }\n .list span { color: var(--muted); }\n .list small { color: var(--muted); font-size: 12px; }\n .add { width: 32px; border: 0; border-radius: 8px; background: none; color: var(--muted); font-size: 18px; }\n .add:hover { background: var(--hover); color: var(--fg); }\n details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n summary { cursor: pointer; font-weight: 600; }\n code { font: 12px ui-monospace, monospace; }\n textarea { height: 200px; font: 12px/1.45 ui-monospace, monospace; resize: vertical; }\n .hint { margin: 6px 0 0; color: var(--muted); font-size: 12px; }\n .error { color: var(--danger); white-space: pre-wrap; margin: 8px 0; font-size: 12px; }\n .error:empty, .unmocked:empty, .candidates:empty { display: none; }\n .ready-check, .save-status { color: var(--muted); font-size: 12px; margin: 2px 0 0; }\n .ready-check[data-tone=ok] { color: var(--accent); }\n .ready-check[data-tone=bad] { color: var(--danger); }\n .save-status:empty { display: none; }\n details button[hidden] { display: none; }\n .candidates { margin-top: 6px; }\n .pick-box {\n position: fixed; pointer-events: none; box-sizing: border-box;\n border: 2px solid var(--accent); border-radius: 4px; background: #4747eb14;\n }\n .pick-box[hidden] { display: none; }\n .pick-box span {\n position: absolute; left: -2px; bottom: 100%; margin-bottom: 4px; max-width: 360px;\n padding: 2px 6px; border-radius: 4px; background: var(--accent); color: var(--accent-fg);\n font: 12px/1.4 ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .pick-box[data-below=true] span { bottom: auto; top: 100%; margin: 4px 0 0; }\n .candidates button { font-size: 12px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }\n .unmocked { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--hover); color: var(--danger); font-size: 12px; }\n .unmocked p { margin: 0 0 4px; }\n .unmocked ul { padding-left: 16px; list-style: disc; }\n .unmocked code { overflow-wrap: anywhere; }\n em { font-style: normal; color: var(--danger); font-weight: 400; }\n details button, footer button {\n padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--hover);\n }\n button[aria-pressed=\"true\"] { border-color: var(--accent); color: var(--accent); }\n footer { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }\n footer a { color: var(--accent); }\n`;\n"],"mappings":"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","names":["href"]}
|