executable-stories-showcase 0.0.0-stage → 0.1.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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +17 -2
  3. package/dist/adapters/cookies.d.ts +11 -0
  4. package/dist/adapters/cookies.js +27 -0
  5. package/dist/adapters/cookies.js.map +1 -0
  6. package/dist/adapters/redux.d.ts +23 -0
  7. package/dist/adapters/redux.js +28 -0
  8. package/dist/adapters/redux.js.map +1 -0
  9. package/dist/adapters/tanstack-query.d.ts +17 -0
  10. package/dist/adapters/tanstack-query.js +43 -0
  11. package/dist/adapters/tanstack-query.js.map +1 -0
  12. package/dist/adapters/xstate.d.ts +33 -0
  13. package/dist/adapters/xstate.js +36 -0
  14. package/dist/adapters/xstate.js.map +1 -0
  15. package/dist/adapters/zustand.d.ts +17 -0
  16. package/dist/adapters/zustand.js +16 -0
  17. package/dist/adapters/zustand.js.map +1 -0
  18. package/dist/capture-CIKR46QE.js +10 -0
  19. package/dist/capture-CIKR46QE.js.map +1 -0
  20. package/dist/capture.d.ts +26 -0
  21. package/dist/catalog.d.ts +27 -0
  22. package/dist/chunk-BK4EEQXV.js +98 -0
  23. package/dist/chunk-BK4EEQXV.js.map +1 -0
  24. package/dist/chunk-C755RCRL.js +125 -0
  25. package/dist/chunk-C755RCRL.js.map +1 -0
  26. package/dist/chunk-COLHEOSE.js +148 -0
  27. package/dist/chunk-COLHEOSE.js.map +1 -0
  28. package/dist/chunk-DETES767.js +43 -0
  29. package/dist/chunk-DETES767.js.map +1 -0
  30. package/dist/chunk-ESIS3D6R.js +47 -0
  31. package/dist/chunk-ESIS3D6R.js.map +1 -0
  32. package/dist/chunk-MCC3CAWQ.js +208 -0
  33. package/dist/chunk-MCC3CAWQ.js.map +1 -0
  34. package/dist/chunk-XLNYICOI.js +234 -0
  35. package/dist/chunk-XLNYICOI.js.map +1 -0
  36. package/dist/index.d.ts +9 -0
  37. package/dist/index.js +531 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/jwt.d.ts +7 -0
  40. package/dist/link.d.ts +33 -0
  41. package/dist/network-I5SB4F6T.js +7 -0
  42. package/dist/network-I5SB4F6T.js.map +1 -0
  43. package/dist/network.d.ts +3 -0
  44. package/dist/next.d.ts +21 -0
  45. package/dist/next.js +30 -0
  46. package/dist/next.js.map +1 -0
  47. package/dist/no-msw-browser.d.ts +2 -0
  48. package/dist/no-msw-browser.js +8 -0
  49. package/dist/no-msw-browser.js.map +1 -0
  50. package/dist/panel.d.ts +30 -0
  51. package/dist/panel.js +607 -0
  52. package/dist/panel.js.map +1 -0
  53. package/dist/playwright.d.ts +31 -0
  54. package/dist/playwright.js +92 -0
  55. package/dist/playwright.js.map +1 -0
  56. package/dist/ready-4MENAMNI.js +13 -0
  57. package/dist/ready-4MENAMNI.js.map +1 -0
  58. package/dist/ready.d.ts +39 -0
  59. package/dist/record.d.ts +49 -0
  60. package/dist/redact.d.ts +18 -0
  61. package/dist/runtime.d.ts +191 -0
  62. package/dist/save.d.ts +27 -0
  63. package/dist/schema.d.ts +161 -0
  64. package/dist/vite.d.ts +11 -0
  65. package/dist/vite.js +26 -0
  66. package/dist/vite.js.map +1 -0
  67. package/package.json +118 -3
package/dist/panel.js ADDED
@@ -0,0 +1,607 @@
1
+ import {
2
+ captureScenario
3
+ } from "./chunk-C755RCRL.js";
4
+ import {
5
+ checkReadySelector,
6
+ closestReadySelector,
7
+ suggestReadySelectors
8
+ } from "./chunk-XLNYICOI.js";
9
+ import {
10
+ PARAM,
11
+ SAVE_ENDPOINT,
12
+ isLoopbackSaveUrl,
13
+ readInline,
14
+ scenarioHref
15
+ } from "./chunk-ESIS3D6R.js";
16
+ import "./chunk-MCC3CAWQ.js";
17
+ import {
18
+ parseScenario
19
+ } from "./chunk-BK4EEQXV.js";
20
+
21
+ // src/panel.ts
22
+ import { z } from "zod";
23
+ var OPEN_KEY = "scenario-panel:open";
24
+ var FLASH_KEY = "scenario-panel:flash";
25
+ var SaveProbeSchema = z.object({ dir: z.string(), url: z.string().refine(isLoopbackSaveUrl).optional() });
26
+ var SaveResultSchema = z.object({ path: z.string().optional(), error: z.string().optional() });
27
+ var FlashSchema = z.object({ message: z.string(), at: z.number() });
28
+ function mountPanel(session, options = {}) {
29
+ if (!customElements.get("scenario-panel")) {
30
+ customElements.define("scenario-panel", ScenarioPanel);
31
+ }
32
+ const el = new ScenarioPanel();
33
+ el.session = session;
34
+ el.schemaPath = options.schemaPath;
35
+ const [v, h] = (options.position ?? "bottom-right").split("-");
36
+ el.dataset.v = v;
37
+ el.dataset.h = h;
38
+ if (options.zIndex !== void 0) el.style.zIndex = String(options.zIndex);
39
+ if (options.theme && options.theme !== "auto") el.dataset.theme = options.theme;
40
+ document.body.append(el);
41
+ return el;
42
+ }
43
+ var Base = globalThis.HTMLElement ?? Object;
44
+ var ScenarioPanel = class extends Base {
45
+ session;
46
+ schemaPath;
47
+ render;
48
+ unsubscribe;
49
+ stopPick;
50
+ disconnectedCallback() {
51
+ this.unsubscribe?.();
52
+ this.unsubscribe = void 0;
53
+ this.stopPick?.();
54
+ this.stopPick = void 0;
55
+ }
56
+ connectedCallback() {
57
+ if (this.render) {
58
+ this.unsubscribe = this.session.onChange(this.render);
59
+ this.render();
60
+ return;
61
+ }
62
+ const session = this.session;
63
+ const { catalog } = session;
64
+ const root = this.attachShadow({ mode: "open" });
65
+ root.innerHTML = `
66
+ <style>${STYLES}</style>
67
+ <button class="toggle" aria-expanded="false" aria-controls="drawer"></button>
68
+ <section id="drawer" aria-label="Scenarios" hidden>
69
+ <header>
70
+ <h2>Scenarios</h2>
71
+ <button class="close" aria-label="Close scenarios panel">\xD7</button>
72
+ </header>
73
+ <p class="error" role="alert"></p>
74
+ <input type="search" placeholder="Filter by name, description or tag" aria-label="Filter scenarios">
75
+ <ul class="list"></ul>
76
+ <div class="unmocked" role="status"></div>
77
+ <section class="live" aria-label="Live state"></section>
78
+ <details class="save">
79
+ <summary>Save as scenario</summary>
80
+ <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>
81
+ <label>Name <input name="save-name" placeholder="e.g. checkout-card-declined" aria-label="Scenario name"></label>
82
+ <label>Description <input name="save-description" aria-label="Scenario description"></label>
83
+ <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>
84
+ <p class="ready-check" role="status"></p>
85
+ <div class="row">
86
+ <button type="button" data-act="mark-ready">Mark ready</button>
87
+ <button type="button" data-act="clear-ready">Clear</button>
88
+ </div>
89
+ <div class="candidates" role="group" aria-label="Suggested ready selectors"></div>
90
+ <div class="pick-box" hidden><span></span></div>
91
+ <label>Extends
92
+ <select name="save-base" aria-label="Base scenario to extend"></select>
93
+ </label>
94
+ <div class="row">
95
+ <button data-act="save-project" hidden>Save to project</button>
96
+ <button data-act="save-link">Copy link</button>
97
+ <button data-act="save-json">Download JSON</button>
98
+ </div>
99
+ <p class="save-status" role="status"></p>
100
+ <input class="saved" readonly aria-label="Saved scenario link" hidden>
101
+ </details>
102
+ <details class="edit">
103
+ <summary>Edit current scenario</summary>
104
+ <textarea spellcheck="false" aria-label="Scenario JSON"></textarea>
105
+ <button data-act="apply">Apply edited scenario</button>
106
+ </details>
107
+ <footer>
108
+ <button data-act="copy">Copy link</button>
109
+ <a class="reset">Reset</a>
110
+ </footer>
111
+ </section>`;
112
+ const find = (selector, type) => {
113
+ const el = root.querySelector(selector);
114
+ if (!(el instanceof type)) throw new Error(`executable-stories-showcase panel: missing ${selector}`);
115
+ return el;
116
+ };
117
+ const toggle = find(".toggle", HTMLButtonElement);
118
+ const drawer = find("#drawer", HTMLElement);
119
+ const error = find(".error", HTMLParagraphElement);
120
+ const run = (fn) => async () => {
121
+ error.textContent = "";
122
+ try {
123
+ await fn();
124
+ } catch (err) {
125
+ error.textContent = err instanceof Error ? err.message : String(err);
126
+ }
127
+ };
128
+ const href = (s) => {
129
+ try {
130
+ return { href: scenarioHref(catalog.resolve(s), { strict: session.strict, record: session.recording }), invalid: false };
131
+ } catch {
132
+ return { href: `?${PARAM}=${encodeURIComponent(s.name)}`, invalid: true };
133
+ }
134
+ };
135
+ find(".list", HTMLUListElement).innerHTML = catalog.list.map((s) => ({ s, ...href(s) })).map(
136
+ ({ s, href: href2, invalid }) => `
137
+ <li data-name="${esc(s.name)}" data-search="${esc([s.name, s.description, ...s.tags ?? []].join(" ").toLowerCase())}">
138
+ <a href="${esc(href2)}">
139
+ <strong>${esc(s.name)}${invalid ? " <em>(invalid, opens with error)</em>" : ""}</strong>
140
+ ${s.description ? `<span>${esc(s.description)}</span>` : ""}
141
+ ${s.tags?.length ? `<small>${s.tags.map(esc).join(" \xB7 ")}</small>` : ""}
142
+ </a>
143
+ <button class="add" aria-label="Combine ${esc(s.name)} with the current scenario" title="Combine with the current scenario">+</button>
144
+ </li>`
145
+ ).join("");
146
+ root.querySelectorAll("li[data-name]").forEach((li) => {
147
+ const name = li.dataset.name;
148
+ li.querySelector("a").onclick = (e) => {
149
+ if (e.metaKey || e.ctrlKey || e.shiftKey) return;
150
+ e.preventDefault();
151
+ void run(() => session.open(name))();
152
+ };
153
+ li.querySelector(".add").onclick = run(() => {
154
+ const names = new Set((session.active?.name ?? "").split(",").filter(Boolean));
155
+ names.add(name);
156
+ return session.open([...names].join(","));
157
+ });
158
+ });
159
+ const filter = find("input[type=search]", HTMLInputElement);
160
+ filter.oninput = () => {
161
+ const q = filter.value.toLowerCase().trim();
162
+ root.querySelectorAll("li[data-search]").forEach((li) => {
163
+ li.hidden = !li.dataset.search.includes(q);
164
+ });
165
+ };
166
+ const editors = /* @__PURE__ */ new Map();
167
+ const renderLive = () => {
168
+ const keys = session.keys();
169
+ for (const [key, ed] of editors) {
170
+ if (!keys.includes(key)) {
171
+ ed.textarea.closest("details").remove();
172
+ editors.delete(key);
173
+ }
174
+ }
175
+ for (const key of keys) {
176
+ let ed = editors.get(key);
177
+ if (!ed) {
178
+ const details = document.createElement("details");
179
+ details.innerHTML = `
180
+ <summary>Live state: <code>${esc(key)}</code></summary>
181
+ <textarea spellcheck="false" aria-label="Live state ${esc(key)}"></textarea>
182
+ <div class="row"><button data-act="live-apply">Apply</button><button data-act="live-revert">Revert</button></div>`;
183
+ find(".live", HTMLElement).append(details);
184
+ const entry = { textarea: details.querySelector("textarea"), dirty: false };
185
+ entry.textarea.oninput = () => entry.dirty = true;
186
+ details.ontoggle = () => renderLive();
187
+ details.querySelector("[data-act=live-apply]").onclick = run(async () => {
188
+ await session.applyState(key, JSON.parse(entry.textarea.value));
189
+ entry.dirty = false;
190
+ renderLive();
191
+ });
192
+ details.querySelector("[data-act=live-revert]").onclick = () => {
193
+ entry.dirty = false;
194
+ renderLive();
195
+ };
196
+ editors.set(key, ed = entry);
197
+ }
198
+ if (!ed.dirty && ed.textarea.closest("details").open) {
199
+ ed.textarea.value = JSON.stringify(session.read(key) ?? null, null, 2);
200
+ }
201
+ }
202
+ };
203
+ const saveName = find("input[name=save-name]", HTMLInputElement);
204
+ const saveDescription = find("input[name=save-description]", HTMLInputElement);
205
+ const saveReady = find("input[name=save-ready]", HTMLInputElement);
206
+ const saveBase = find("select[name=save-base]", HTMLSelectElement);
207
+ const readyCheck = find(".ready-check", HTMLParagraphElement);
208
+ const saveProject = find("[data-act=save-project]", HTMLButtonElement);
209
+ const saveStatus = find(".save-status", HTMLParagraphElement);
210
+ const candidates = find(".candidates", HTMLElement);
211
+ const saved = find(".saved", HTMLInputElement);
212
+ const saveDetails = find(".save", HTMLDetailsElement);
213
+ const markReadyBtn = find("[data-act=mark-ready]", HTMLButtonElement);
214
+ const fillBaseOptions = () => {
215
+ const preferred = catalog.get("default")?.name ?? [session.active?.extends ?? []].flat()[0] ?? "";
216
+ const previous = saveBase.value;
217
+ saveBase.innerHTML = `<option value="">(none: save everything)</option>` + catalog.list.map((s) => `<option value="${esc(s.name)}">${esc(s.name)}</option>`).join("");
218
+ if (previous && [...saveBase.options].some((o) => o.value === previous)) {
219
+ saveBase.value = previous;
220
+ } else {
221
+ saveBase.value = preferred;
222
+ }
223
+ };
224
+ fillBaseOptions();
225
+ const renderCandidates = () => {
226
+ const picks = suggestReadySelectors(document.body, { limit: 3 });
227
+ 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>` : "";
228
+ candidates.querySelectorAll("[data-candidate]").forEach((btn) => {
229
+ btn.onclick = () => {
230
+ saveReady.value = btn.dataset.candidate ?? "";
231
+ updateReadyCheck();
232
+ };
233
+ });
234
+ };
235
+ saveDetails.ontoggle = () => {
236
+ if (saveDetails.open) {
237
+ fillBaseOptions();
238
+ renderCandidates();
239
+ updateReadyCheck();
240
+ void probeSaveEndpoint();
241
+ }
242
+ };
243
+ const stopPickMode = () => {
244
+ this.stopPick?.();
245
+ this.stopPick = void 0;
246
+ markReadyBtn.setAttribute("aria-pressed", "false");
247
+ markReadyBtn.textContent = "Mark ready";
248
+ };
249
+ const pickBox = find(".pick-box", HTMLDivElement);
250
+ const pickLabel = find(".pick-box span", HTMLSpanElement);
251
+ const outline = (el, label) => {
252
+ if (!el) {
253
+ pickBox.hidden = true;
254
+ return;
255
+ }
256
+ const r = el.getBoundingClientRect();
257
+ Object.assign(pickBox.style, {
258
+ top: `${r.top}px`,
259
+ left: `${r.left}px`,
260
+ width: `${r.width}px`,
261
+ height: `${r.height}px`
262
+ });
263
+ pickBox.dataset.below = String(r.top < 28);
264
+ pickLabel.textContent = label;
265
+ pickBox.hidden = false;
266
+ };
267
+ const appTarget = (e) => {
268
+ const path = e.composedPath();
269
+ if (path.includes(this)) return void 0;
270
+ return path.find(
271
+ (n) => n instanceof Element && n !== document.documentElement && n !== document.body
272
+ );
273
+ };
274
+ const READY_MESSAGES = {
275
+ empty: "No ready selector: the link still works, but shoot and tests won't wait for this state to render.",
276
+ none: "No match on this page: tests would time out waiting for it.",
277
+ unchecked: "Can't check this kind of selector here; Playwright still can."
278
+ };
279
+ let readyMatch;
280
+ const updateReadyCheck = () => {
281
+ const value = saveReady.value.trim();
282
+ const check = value ? checkReadySelector(value) : void 0;
283
+ readyMatch = check?.status === "match" ? check.first : void 0;
284
+ let tone = "muted";
285
+ let message;
286
+ if (!check) message = READY_MESSAGES.empty;
287
+ else if (check.status === "invalid") [tone, message] = ["bad", check.error];
288
+ else if (check.status === "none") [tone, message] = ["bad", READY_MESSAGES.none];
289
+ else if (check.status === "unchecked") message = READY_MESSAGES.unchecked;
290
+ else if (!check.visible) {
291
+ message = "Matches, but the first match is hidden: tests wait until it shows.";
292
+ } else if (check.count > 1) {
293
+ message = `Matches ${check.count} elements; tests wait for the first (outlined).`;
294
+ } else [tone, message] = ["ok", "\u2713 Matches 1 element on this page."];
295
+ readyCheck.dataset.tone = tone;
296
+ readyCheck.textContent = message;
297
+ if (this.shadowRoot?.activeElement === saveReady && !this.stopPick) outline(readyMatch, "ready");
298
+ };
299
+ saveReady.oninput = updateReadyCheck;
300
+ saveReady.onfocus = updateReadyCheck;
301
+ saveReady.onblur = () => {
302
+ if (!this.stopPick) outline(void 0, "");
303
+ };
304
+ updateReadyCheck();
305
+ const startPickMode = () => {
306
+ stopPickMode();
307
+ markReadyBtn.setAttribute("aria-pressed", "true");
308
+ markReadyBtn.textContent = "Click the UI\u2026 (Esc cancels)";
309
+ document.documentElement.style.cursor = "crosshair";
310
+ const onMove = (e) => {
311
+ const target = appTarget(e);
312
+ const hit = target && closestReadySelector(target);
313
+ if (hit) outline(hit.element, hit.selector);
314
+ else outline(target, "No selector here: try text, a heading or a button");
315
+ };
316
+ const block = (e) => {
317
+ if (!appTarget(e)) return;
318
+ e.preventDefault();
319
+ e.stopPropagation();
320
+ };
321
+ const onClick = (e) => {
322
+ const target = appTarget(e);
323
+ if (!target) return;
324
+ block(e);
325
+ const hit = closestReadySelector(target);
326
+ if (!hit) return;
327
+ saveReady.value = hit.selector;
328
+ updateReadyCheck();
329
+ stopPickMode();
330
+ saveReady.focus();
331
+ };
332
+ const onKey = (e) => {
333
+ if (e.key !== "Escape") return;
334
+ e.stopPropagation();
335
+ stopPickMode();
336
+ };
337
+ const pressEvents = ["pointerdown", "mousedown", "pointerup", "mouseup"];
338
+ document.addEventListener("mousemove", onMove, true);
339
+ document.addEventListener("click", onClick, true);
340
+ document.addEventListener("keydown", onKey, true);
341
+ for (const type of pressEvents) document.addEventListener(type, block, true);
342
+ this.stopPick = () => {
343
+ document.removeEventListener("mousemove", onMove, true);
344
+ document.removeEventListener("click", onClick, true);
345
+ document.removeEventListener("keydown", onKey, true);
346
+ for (const type of pressEvents) document.removeEventListener(type, block, true);
347
+ document.documentElement.style.removeProperty("cursor");
348
+ pickBox.hidden = true;
349
+ };
350
+ };
351
+ markReadyBtn.onclick = () => {
352
+ if (markReadyBtn.getAttribute("aria-pressed") === "true") stopPickMode();
353
+ else startPickMode();
354
+ };
355
+ find("[data-act=clear-ready]", HTMLButtonElement).onclick = () => {
356
+ saveReady.value = "";
357
+ updateReadyCheck();
358
+ };
359
+ const capture = () => {
360
+ const baseName = saveBase.value;
361
+ const base = baseName ? catalog.get(baseName) : void 0;
362
+ return captureScenario(session, {
363
+ name: saveName.value.trim(),
364
+ description: saveDescription.value.trim(),
365
+ ready: saveReady.value.trim() || void 0,
366
+ base,
367
+ schemaPath: this.schemaPath
368
+ });
369
+ };
370
+ let probe;
371
+ let saveUrl = SAVE_ENDPOINT;
372
+ const probeSaveEndpoint = () => probe ??= fetch(SAVE_ENDPOINT, { headers: { accept: "application/json" } }).then(async (res) => {
373
+ const isJson = res.ok && res.headers.get("content-type")?.includes("json");
374
+ const probed = isJson ? SaveProbeSchema.safeParse(await res.json()) : void 0;
375
+ if (!probed?.success) return;
376
+ saveUrl = probed.data.url ?? SAVE_ENDPOINT;
377
+ saveProject.textContent = `Save to ${probed.data.dir}`;
378
+ saveProject.hidden = false;
379
+ }).catch(() => {
380
+ });
381
+ const saveToProject = async (overwrite) => {
382
+ const { forCatalog } = capture();
383
+ const res = await fetch(saveUrl, {
384
+ method: "POST",
385
+ headers: { "content-type": "application/json" },
386
+ body: JSON.stringify({ scenario: forCatalog, overwrite })
387
+ });
388
+ const body = SaveResultSchema.catch({}).parse(await res.json().catch(() => ({})));
389
+ if (res.status === 409 && !overwrite) {
390
+ if (confirm(`${body.path} already exists. Replace it?`)) return saveToProject(true);
391
+ return;
392
+ }
393
+ if (!res.ok) throw new Error(body.error ?? `Save failed (HTTP ${res.status})`);
394
+ const message = `Saved ${body.path}. Commit it with your change.`;
395
+ saveStatus.textContent = message;
396
+ try {
397
+ sessionStorage.setItem(FLASH_KEY, JSON.stringify({ message, at: Date.now() }));
398
+ } catch {
399
+ }
400
+ };
401
+ saveProject.onclick = run(async () => {
402
+ saveStatus.textContent = "";
403
+ await session.settleRecording();
404
+ await saveToProject(false);
405
+ });
406
+ try {
407
+ const flash = FlashSchema.safeParse(JSON.parse(sessionStorage.getItem(FLASH_KEY) ?? "null")).data;
408
+ sessionStorage.removeItem(FLASH_KEY);
409
+ if (flash && Date.now() - flash.at < 1e4) {
410
+ saveStatus.textContent = flash.message;
411
+ saveDetails.open = true;
412
+ }
413
+ } catch {
414
+ }
415
+ find("[data-act=save-link]", HTMLButtonElement).onclick = run(async () => {
416
+ await session.settleRecording();
417
+ const { flat } = capture();
418
+ const link = scenarioHref(flat, { inline: true, strict: session.strict });
419
+ saved.hidden = false;
420
+ saved.value = link;
421
+ saved.select();
422
+ await navigator.clipboard?.writeText(link).catch(() => {
423
+ });
424
+ updateReadyCheck();
425
+ });
426
+ find("[data-act=save-json]", HTMLButtonElement).onclick = run(async () => {
427
+ await session.settleRecording();
428
+ const { forCatalog } = capture();
429
+ const a = document.createElement("a");
430
+ a.href = URL.createObjectURL(
431
+ new Blob([`${JSON.stringify(forCatalog, null, 2)}
432
+ `], { type: "application/json" })
433
+ );
434
+ a.download = `${forCatalog.name}.json`;
435
+ a.click();
436
+ URL.revokeObjectURL(a.href);
437
+ updateReadyCheck();
438
+ });
439
+ const editor = find(".edit textarea", HTMLTextAreaElement);
440
+ if (readInline(location.hash)) find(".edit", HTMLDetailsElement).open = true;
441
+ find("[data-act=apply]", HTMLButtonElement).onclick = run(
442
+ () => session.openInline(parseScenario(JSON.parse(editor.value)))
443
+ );
444
+ find("[data-act=copy]", HTMLButtonElement).onclick = run(async () => {
445
+ try {
446
+ await navigator.clipboard.writeText(location.href);
447
+ } catch {
448
+ prompt("Copy this link", location.href);
449
+ }
450
+ });
451
+ const reset = new URL(location.href);
452
+ reset.search = "";
453
+ reset.hash = "";
454
+ reset.searchParams.set(PARAM, "");
455
+ find(".reset", HTMLAnchorElement).href = reset.href;
456
+ const setOpen = (open) => {
457
+ drawer.hidden = !open;
458
+ toggle.setAttribute("aria-expanded", String(open));
459
+ try {
460
+ sessionStorage.setItem(OPEN_KEY, open ? "1" : "");
461
+ } catch {
462
+ }
463
+ };
464
+ let stored = null;
465
+ try {
466
+ stored = sessionStorage.getItem(OPEN_KEY);
467
+ } catch {
468
+ }
469
+ setOpen(!!stored);
470
+ toggle.onclick = () => setOpen(!!drawer.hidden);
471
+ find(".close", HTMLButtonElement).onclick = () => {
472
+ setOpen(false);
473
+ toggle.focus();
474
+ };
475
+ drawer.onkeydown = (e) => {
476
+ if (e.key === "Escape") {
477
+ setOpen(false);
478
+ toggle.focus();
479
+ }
480
+ };
481
+ let shownScenario;
482
+ const render = () => {
483
+ const { active, strict, unhandled, recording } = session;
484
+ const mode = recording ? ` (recording ${session.recorded().length})` : strict ? " (strict)" : "";
485
+ toggle.innerHTML = `Scenario: <b>${esc(active?.name ?? "none")}</b>${mode}`;
486
+ const names = (active?.name ?? "").split(",");
487
+ root.querySelectorAll("li[data-name]").forEach((li) => {
488
+ const a = li.querySelector("a");
489
+ if (names.includes(li.dataset.name)) a.setAttribute("aria-current", "true");
490
+ else a.removeAttribute("aria-current");
491
+ });
492
+ find(".unmocked", HTMLElement).innerHTML = unhandled.length && !recording ? `<p>${unhandled.length} request(s) not covered by this scenario${strict ? " (answered 501)" : ""}:</p>
493
+ <ul>${unhandled.map((u) => `<li><code>${esc(u)}</code></li>`).join("")}</ul>` : "";
494
+ if (active?.name !== shownScenario) {
495
+ shownScenario = active?.name;
496
+ editor.value = JSON.stringify(active ?? { name: "custom", extends: catalog.list[0]?.name }, null, 2);
497
+ fillBaseOptions();
498
+ }
499
+ renderLive();
500
+ if (saveDetails.open) updateReadyCheck();
501
+ };
502
+ this.render = render;
503
+ render();
504
+ this.unsubscribe = session.onChange(render);
505
+ }
506
+ };
507
+ var esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
508
+ var STYLES = `
509
+ :host {
510
+ --bg: #ffffff; --fg: #1b1b1f; --muted: #5d5d66; --line: #dcdce2;
511
+ --accent: #4747eb; --accent-fg: #ffffff; --hover: #f2f2f7; --danger: #b42318;
512
+ all: initial; position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
513
+ font: 14px/1.4 system-ui, sans-serif; color: var(--fg);
514
+ }
515
+ @media (prefers-color-scheme: dark) {
516
+ :host(:not([data-theme="light"])) {
517
+ --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;
518
+ --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;
519
+ }
520
+ }
521
+ :host([data-theme="dark"]) {
522
+ --bg: #1c1c21; --fg: #ececf1; --muted: #a3a3ad; --line: #34343c;
523
+ --accent: #8c8cff; --accent-fg: #101014; --hover: #26262d; --danger: #ff8a80;
524
+ }
525
+ :host([data-v="top"]) { bottom: auto; top: 16px; }
526
+ :host([data-h="left"]) { right: auto; left: 16px; }
527
+ :host([data-v="top"]) #drawer { bottom: auto; top: 48px; }
528
+ :host([data-h="left"]) #drawer { right: auto; left: 0; }
529
+ :host([data-h="left"]) .toggle { margin-left: 0; }
530
+ * { box-sizing: border-box; font: inherit; color: inherit; }
531
+ button, a { cursor: pointer; }
532
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
533
+ .toggle {
534
+ display: block; margin-left: auto; padding: 8px 14px; border: 0; border-radius: 999px;
535
+ background: var(--accent); color: var(--accent-fg); box-shadow: 0 4px 16px #0003;
536
+ }
537
+ .toggle b, strong { font-weight: 700; }
538
+ #drawer {
539
+ position: absolute; right: 0; bottom: 48px; width: min(400px, calc(100vw - 32px));
540
+ max-height: min(680px, calc(100vh - 96px)); overflow: auto; padding: 12px;
541
+ background: var(--bg); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 12px 40px #0004;
542
+ }
543
+ #drawer[hidden] { display: none; }
544
+ header, footer, .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
545
+ .row { justify-content: flex-start; margin-top: 8px; flex-wrap: wrap; }
546
+ h2 { margin: 0; font-size: 16px; font-weight: 700; }
547
+ .close { border: 0; background: none; font-size: 22px; line-height: 1; padding: 4px 8px; border-radius: 6px; }
548
+ input, textarea, select {
549
+ width: 100%; margin: 6px 0; padding: 8px 10px; border: 1px solid var(--line);
550
+ border-radius: 8px; background: var(--bg);
551
+ }
552
+ label { display: block; margin-top: 6px; color: var(--muted); font-size: 12px; }
553
+ label input, label select { color: var(--fg); font-size: 14px; }
554
+ ul { list-style: none; margin: 0; padding: 0; }
555
+ li[hidden] { display: none; }
556
+ .list li { display: flex; align-items: stretch; gap: 4px; }
557
+ .list a {
558
+ flex: 1; min-width: 0; overflow-wrap: anywhere; display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-radius: 8px;
559
+ text-decoration: none; border-left: 3px solid transparent;
560
+ }
561
+ .list a:hover { background: var(--hover); }
562
+ .list a[aria-current] { border-left-color: var(--accent); background: var(--hover); }
563
+ .list span { color: var(--muted); }
564
+ .list small { color: var(--muted); font-size: 12px; }
565
+ .add { width: 32px; border: 0; border-radius: 8px; background: none; color: var(--muted); font-size: 18px; }
566
+ .add:hover { background: var(--hover); color: var(--fg); }
567
+ details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
568
+ summary { cursor: pointer; font-weight: 600; }
569
+ code { font: 12px ui-monospace, monospace; }
570
+ textarea { height: 200px; font: 12px/1.45 ui-monospace, monospace; resize: vertical; }
571
+ .hint { margin: 6px 0 0; color: var(--muted); font-size: 12px; }
572
+ .error { color: var(--danger); white-space: pre-wrap; margin: 8px 0; font-size: 12px; }
573
+ .error:empty, .unmocked:empty, .candidates:empty { display: none; }
574
+ .ready-check, .save-status { color: var(--muted); font-size: 12px; margin: 2px 0 0; }
575
+ .ready-check[data-tone=ok] { color: var(--accent); }
576
+ .ready-check[data-tone=bad] { color: var(--danger); }
577
+ .save-status:empty { display: none; }
578
+ details button[hidden] { display: none; }
579
+ .candidates { margin-top: 6px; }
580
+ .pick-box {
581
+ position: fixed; pointer-events: none; box-sizing: border-box;
582
+ border: 2px solid var(--accent); border-radius: 4px; background: #4747eb14;
583
+ }
584
+ .pick-box[hidden] { display: none; }
585
+ .pick-box span {
586
+ position: absolute; left: -2px; bottom: 100%; margin-bottom: 4px; max-width: 360px;
587
+ padding: 2px 6px; border-radius: 4px; background: var(--accent); color: var(--accent-fg);
588
+ font: 12px/1.4 ui-monospace, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
589
+ }
590
+ .pick-box[data-below=true] span { bottom: auto; top: 100%; margin: 4px 0 0; }
591
+ .candidates button { font-size: 12px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
592
+ .unmocked { margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--hover); color: var(--danger); font-size: 12px; }
593
+ .unmocked p { margin: 0 0 4px; }
594
+ .unmocked ul { padding-left: 16px; list-style: disc; }
595
+ .unmocked code { overflow-wrap: anywhere; }
596
+ em { font-style: normal; color: var(--danger); font-weight: 400; }
597
+ details button, footer button {
598
+ padding: 6px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--hover);
599
+ }
600
+ button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
601
+ footer { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
602
+ footer a { color: var(--accent); }
603
+ `;
604
+ export {
605
+ mountPanel
606
+ };
607
+ //# sourceMappingURL=panel.js.map