@melra/browser-runtime 0.3.0-alpha.3

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 (58) hide show
  1. package/LICENSE +202 -0
  2. package/dist/browser-connection.d.ts +17 -0
  3. package/dist/browser-connection.d.ts.map +1 -0
  4. package/dist/browser-connection.js +95 -0
  5. package/dist/browser-connection.js.map +1 -0
  6. package/dist/browser-connection.test.d.ts +2 -0
  7. package/dist/browser-connection.test.d.ts.map +1 -0
  8. package/dist/browser-connection.test.js +123 -0
  9. package/dist/browser-connection.test.js.map +1 -0
  10. package/dist/dialogs.test.d.ts +2 -0
  11. package/dist/dialogs.test.d.ts.map +1 -0
  12. package/dist/dialogs.test.js +165 -0
  13. package/dist/dialogs.test.js.map +1 -0
  14. package/dist/frames.test.d.ts +2 -0
  15. package/dist/frames.test.d.ts.map +1 -0
  16. package/dist/frames.test.js +157 -0
  17. package/dist/frames.test.js.map +1 -0
  18. package/dist/history-tabs.test.d.ts +2 -0
  19. package/dist/history-tabs.test.d.ts.map +1 -0
  20. package/dist/history-tabs.test.js +171 -0
  21. package/dist/history-tabs.test.js.map +1 -0
  22. package/dist/index.d.ts +124 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +822 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/input-fidelity.test.d.ts +2 -0
  27. package/dist/input-fidelity.test.d.ts.map +1 -0
  28. package/dist/input-fidelity.test.js +260 -0
  29. package/dist/input-fidelity.test.js.map +1 -0
  30. package/dist/network-policy.d.ts +7 -0
  31. package/dist/network-policy.d.ts.map +1 -0
  32. package/dist/network-policy.js +112 -0
  33. package/dist/network-policy.js.map +1 -0
  34. package/dist/network-policy.test.d.ts +2 -0
  35. package/dist/network-policy.test.d.ts.map +1 -0
  36. package/dist/network-policy.test.js +21 -0
  37. package/dist/network-policy.test.js.map +1 -0
  38. package/dist/see-act.test.d.ts +2 -0
  39. package/dist/see-act.test.d.ts.map +1 -0
  40. package/dist/see-act.test.js +152 -0
  41. package/dist/see-act.test.js.map +1 -0
  42. package/dist/selector.d.ts +30 -0
  43. package/dist/selector.d.ts.map +1 -0
  44. package/dist/selector.js +39 -0
  45. package/dist/selector.js.map +1 -0
  46. package/dist/selector.test.d.ts +2 -0
  47. package/dist/selector.test.d.ts.map +1 -0
  48. package/dist/selector.test.js +34 -0
  49. package/dist/selector.test.js.map +1 -0
  50. package/dist/stable-dom.d.ts +21 -0
  51. package/dist/stable-dom.d.ts.map +1 -0
  52. package/dist/stable-dom.js +76 -0
  53. package/dist/stable-dom.js.map +1 -0
  54. package/dist/stable-dom.test.d.ts +2 -0
  55. package/dist/stable-dom.test.d.ts.map +1 -0
  56. package/dist/stable-dom.test.js +22 -0
  57. package/dist/stable-dom.test.js.map +1 -0
  58. package/package.json +33 -0
@@ -0,0 +1,157 @@
1
+ // Copyright 2026 XAGI Labs Private Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { createServer } from "node:http";
4
+ import { mkdtemp, rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { BrowserOperationSchema } from "@melra/protocol";
8
+ import { afterEach, describe, expect, it } from "vitest";
9
+ import { BrowserRuntime, captchaReport, detectBrowserExecutable } from "./index.js";
10
+ /**
11
+ * Consent banners, cookie walls, and captcha widgets live in iframes, and every
12
+ * locator used to be built from the page — which only searches the main
13
+ * document. An agent could see the banner in a screenshot and had no way to
14
+ * click it. These tests drive real frames, so they fail if that regresses.
15
+ */
16
+ const BANNER = `<!doctype html><html><body>
17
+ <button id="accept">Accept all cookies</button>
18
+ </body></html>`;
19
+ /** Attaches itself after a delay, so a wait started before it exists must find it. */
20
+ const LATE = `<!doctype html><html><body><p>loading</p><script>
21
+ setTimeout(() => {
22
+ document.body.innerHTML = '<button id="late">Continue</button>';
23
+ }, 400);
24
+ </script></body></html>`;
25
+ const PAGE = `<!doctype html><html><head><title>Framed</title></head><body>
26
+ <h1>Article</h1>
27
+ <iframe src="/banner" title="consent"></iframe>
28
+ <iframe src="/late" title="late"></iframe>
29
+ </body></html>`;
30
+ const roots = [];
31
+ const servers = [];
32
+ afterEach(async () => {
33
+ await Promise.all(servers.splice(0).map((server) => new Promise((done) => {
34
+ server.close(() => done());
35
+ })));
36
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
37
+ });
38
+ async function fixture() {
39
+ const executablePath = await detectBrowserExecutable();
40
+ if (executablePath === undefined)
41
+ return undefined;
42
+ const server = createServer((request, response) => {
43
+ const body = request.url === "/banner" ? BANNER : request.url === "/late" ? LATE : PAGE;
44
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
45
+ response.end(body);
46
+ });
47
+ servers.push(server);
48
+ await new Promise((done) => {
49
+ server.listen(0, "127.0.0.1", done);
50
+ });
51
+ const address = server.address();
52
+ if (address === null || typeof address === "string") {
53
+ throw new Error("fixture_server_address_unavailable");
54
+ }
55
+ const root = await mkdtemp(join(tmpdir(), "melra-frames-"));
56
+ roots.push(root);
57
+ return {
58
+ runtime: new BrowserRuntime({
59
+ workspaceRoot: root,
60
+ artifactDirectory: join(root, "artifacts"),
61
+ executablePath,
62
+ allowedDomains: ["127.0.0.1"],
63
+ allowLocalhost: true,
64
+ }),
65
+ url: `http://127.0.0.1:${address.port}`,
66
+ };
67
+ }
68
+ describe("frame-aware targeting", () => {
69
+ it("lists and clicks an element inside an iframe", async () => {
70
+ const context = await fixture();
71
+ if (context === undefined)
72
+ return;
73
+ const { runtime, url } = context;
74
+ try {
75
+ const observed = await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
76
+ const elements = observed.elements;
77
+ const accept = elements.find((item) => item.name === "Accept all cookies");
78
+ // The banner is in a child document, so it is absent from the page text
79
+ // but must still be listed, addressable, and attributed to its frame.
80
+ expect(String(observed.text)).not.toContain("Accept all cookies");
81
+ expect(accept).toBeDefined();
82
+ expect(accept?.frame).toContain("/banner");
83
+ expect(elements.every((item) => typeof item.index === "number")).toBe(true);
84
+ expect(new Set(elements.map((item) => item.index)).size).toBe(elements.length);
85
+ // The whole point: a target resolved without naming the frame.
86
+ const clicked = await runtime.execute(BrowserOperationSchema.parse({
87
+ kind: "browser",
88
+ action: "click",
89
+ target: { text: "Accept all cookies" },
90
+ }));
91
+ expect(clicked.success).toBe(true);
92
+ }
93
+ finally {
94
+ await runtime.close();
95
+ }
96
+ });
97
+ it("waits for a target that appears in a frame after the wait starts", async () => {
98
+ const context = await fixture();
99
+ if (context === undefined)
100
+ return;
101
+ const { runtime, url } = context;
102
+ try {
103
+ await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
104
+ const waited = await runtime.execute(BrowserOperationSchema.parse({
105
+ kind: "browser",
106
+ action: "wait",
107
+ target: { text: "Continue" },
108
+ state: "visible",
109
+ timeoutMs: 5_000,
110
+ }));
111
+ expect(waited.waited).toBe(true);
112
+ }
113
+ finally {
114
+ await runtime.close();
115
+ }
116
+ });
117
+ it("does not report a target as hidden merely because other frames lack it", async () => {
118
+ const context = await fixture();
119
+ if (context === undefined)
120
+ return;
121
+ const { runtime, url } = context;
122
+ try {
123
+ await runtime.execute(BrowserOperationSchema.parse({ kind: "browser", action: "navigate", url }));
124
+ // The banner is visible in exactly one of three frames. A per-candidate
125
+ // "some frame is missing it" test would call that hidden immediately.
126
+ await expect(runtime.execute(BrowserOperationSchema.parse({
127
+ kind: "browser",
128
+ action: "wait",
129
+ target: { text: "Accept all cookies" },
130
+ state: "hidden",
131
+ timeoutMs: 700,
132
+ }))).rejects.toThrow(/browser_wait_timeout:hidden/);
133
+ }
134
+ finally {
135
+ await runtime.close();
136
+ }
137
+ });
138
+ });
139
+ describe("captchaReport", () => {
140
+ it("names the vendor of a human-verification widget", () => {
141
+ const report = captchaReport([
142
+ "http://127.0.0.1:8080/",
143
+ "https://www.google.com/recaptcha/api2/anchor?k=abc",
144
+ "https://challenges.cloudflare.com/cdn-cgi/challenge-platform/turnstile",
145
+ ]);
146
+ const captcha = report.captcha;
147
+ expect(captcha.present).toBe(true);
148
+ expect(captcha.vendors).toEqual(["recaptcha", "turnstile"]);
149
+ });
150
+ it("stays silent on a page with no challenge", () => {
151
+ expect(captchaReport(["https://example.com/", "https://cdn.example.com/ad"])).toEqual({});
152
+ });
153
+ it("does not match a lookalike host outside the vendor domain", () => {
154
+ expect(captchaReport(["https://hcaptcha.com.evil.test/x"])).toEqual({});
155
+ });
156
+ });
157
+ //# sourceMappingURL=frames.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.test.js","sourceRoot":"","sources":["../src/frames.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=history-tabs.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-tabs.test.d.ts","sourceRoot":"","sources":["../src/history-tabs.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,171 @@
1
+ // Copyright 2026 XAGI Labs Private Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { createServer } from "node:http";
4
+ import { mkdtemp, rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { BrowserOperationSchema } from "@melra/protocol";
8
+ import { afterEach, describe, expect, it } from "vitest";
9
+ import { BrowserRuntime, detectBrowserExecutable } from "./index.js";
10
+ /**
11
+ * A session could only ever move forward, in one tab. There was no `back`, so a
12
+ * wrong click was unrecoverable without re-navigating from scratch, and `tabs`
13
+ * reported which page was `active` while nothing could change it — the
14
+ * `tabIndex` schema field existed but was read only by `close`.
15
+ */
16
+ const roots = [];
17
+ const servers = [];
18
+ afterEach(async () => {
19
+ await Promise.all(servers.splice(0).map((server) => new Promise((done) => {
20
+ server.close(() => done());
21
+ })));
22
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
23
+ });
24
+ /** Serves /one, /two, /three, each naming itself in the title and body. */
25
+ async function fixture() {
26
+ const executablePath = await detectBrowserExecutable();
27
+ if (executablePath === undefined)
28
+ return undefined;
29
+ const server = createServer((request, response) => {
30
+ const name = (request.url ?? "/").slice(1) || "one";
31
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
32
+ response.end(`<!doctype html><html><head><title>${name}</title></head><body><h1>${name}</h1></body></html>`);
33
+ });
34
+ servers.push(server);
35
+ await new Promise((done) => {
36
+ server.listen(0, "127.0.0.1", done);
37
+ });
38
+ const address = server.address();
39
+ if (address === null || typeof address === "string") {
40
+ throw new Error("fixture_server_address_unavailable");
41
+ }
42
+ const root = await mkdtemp(join(tmpdir(), "melra-history-tabs-"));
43
+ roots.push(root);
44
+ return {
45
+ runtime: new BrowserRuntime({
46
+ workspaceRoot: root,
47
+ artifactDirectory: join(root, "artifacts"),
48
+ executablePath,
49
+ allowedDomains: ["127.0.0.1"],
50
+ allowLocalhost: true,
51
+ }),
52
+ url: `http://127.0.0.1:${address.port}`,
53
+ };
54
+ }
55
+ const op = (fields) => BrowserOperationSchema.parse({ kind: "browser", ...fields });
56
+ describe("browser history", () => {
57
+ it("moves back and forward through the history stack", async () => {
58
+ const context = await fixture();
59
+ if (context === undefined)
60
+ return;
61
+ const { runtime, url } = context;
62
+ try {
63
+ await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
64
+ await runtime.execute(op({ action: "navigate", url: `${url}/two` }));
65
+ const back = await runtime.execute(op({ action: "back" }));
66
+ expect(back.moved).toBe(true);
67
+ expect(back.title).toBe("one");
68
+ const forward = await runtime.execute(op({ action: "forward" }));
69
+ expect(forward.moved).toBe(true);
70
+ expect(forward.title).toBe("two");
71
+ }
72
+ finally {
73
+ await runtime.close();
74
+ }
75
+ }, 120_000);
76
+ it("reports a move with no HTTP response, and the end of the stack, differently", async () => {
77
+ const context = await fixture();
78
+ if (context === undefined)
79
+ return;
80
+ const { runtime, url } = context;
81
+ try {
82
+ await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
83
+ // A page starts on `about:blank`, which is a real history entry but
84
+ // produces no HTTP response, so Playwright reports the same `null` it
85
+ // reports for "nowhere to go". This moved.
86
+ const blank = await runtime.execute(op({ action: "back" }));
87
+ expect(blank.moved).toBe(true);
88
+ expect(blank.status).toBe(null);
89
+ expect(blank.url).toBe("about:blank");
90
+ // Now there genuinely is nothing before it. "Go back unless already at
91
+ // the start" is a normal thing to want, so this reports rather than
92
+ // throws, and it must not look like the case above.
93
+ const end = await runtime.execute(op({ action: "back" }));
94
+ expect(end.moved).toBe(false);
95
+ expect(end.url).toBe("about:blank");
96
+ }
97
+ finally {
98
+ await runtime.close();
99
+ }
100
+ }, 120_000);
101
+ it("reloads the current page in place", async () => {
102
+ const context = await fixture();
103
+ if (context === undefined)
104
+ return;
105
+ const { runtime, url } = context;
106
+ try {
107
+ await runtime.execute(op({ action: "navigate", url: `${url}/three` }));
108
+ const reloaded = await runtime.execute(op({ action: "reload" }));
109
+ expect(reloaded.status).toBe(200);
110
+ expect(reloaded.url).toBe(`${url}/three`);
111
+ }
112
+ finally {
113
+ await runtime.close();
114
+ }
115
+ }, 120_000);
116
+ });
117
+ describe("browser tabs", () => {
118
+ it("opens a tab, switches between tabs, and closes one", async () => {
119
+ const context = await fixture();
120
+ if (context === undefined)
121
+ return;
122
+ const { runtime, url } = context;
123
+ try {
124
+ await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
125
+ const opened = await runtime.execute(op({ action: "tab_new", url: `${url}/two` }));
126
+ expect(opened.opened).toBe(true);
127
+ expect(opened.index).toBe(1);
128
+ // A new tab becomes the active one, so the next action lands on it.
129
+ expect((await runtime.execute(op({ action: "inspect" }))).title).toBe("two");
130
+ const switched = await runtime.execute(op({ action: "tab_switch", tabIndex: 0 }));
131
+ expect(switched.switched).toBe(true);
132
+ expect(switched.tabs.find((tab) => tab.active)?.index).toBe(0);
133
+ expect((await runtime.execute(op({ action: "inspect" }))).title).toBe("one");
134
+ const closed = await runtime.execute(op({ action: "close", tabIndex: 1 }));
135
+ expect(closed.tabs.length).toBe(1);
136
+ }
137
+ finally {
138
+ await runtime.close();
139
+ }
140
+ }, 120_000);
141
+ it("refuses a tab index that does not exist", async () => {
142
+ const context = await fixture();
143
+ if (context === undefined)
144
+ return;
145
+ const { runtime, url } = context;
146
+ try {
147
+ await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
148
+ await expect(runtime.execute(op({ action: "tab_switch", tabIndex: 7 }))).rejects.toThrow("browser_tab_not_found");
149
+ await expect(runtime.execute(op({ action: "tab_switch" }))).rejects.toThrow("browser_tab_switch_requires_tab_index");
150
+ }
151
+ finally {
152
+ await runtime.close();
153
+ }
154
+ }, 120_000);
155
+ it("blocks a disallowed domain when opening a tab", async () => {
156
+ const context = await fixture();
157
+ if (context === undefined)
158
+ return;
159
+ const { runtime, url } = context;
160
+ try {
161
+ await runtime.execute(op({ action: "navigate", url: `${url}/one` }));
162
+ // `tab_new` takes a url, so it has to run the same guard `navigate` does
163
+ // rather than becoming a way around the allowlist.
164
+ await expect(runtime.execute(op({ action: "tab_new", url: "http://169.254.169.254/latest" }))).rejects.toThrow();
165
+ }
166
+ finally {
167
+ await runtime.close();
168
+ }
169
+ }, 120_000);
170
+ });
171
+ //# sourceMappingURL=history-tabs.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history-tabs.test.js","sourceRoot":"","sources":["../src/history-tabs.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import type { BrowserOperation } from "@melra/protocol";
2
+ import { type NetworkPolicy } from "./network-policy.js";
3
+ export interface BrowserRuntimeOptions extends NetworkPolicy {
4
+ artifactDirectory: string;
5
+ workspaceRoot: string;
6
+ executablePath?: string;
7
+ headless?: boolean;
8
+ cdpEndpoint?: string;
9
+ cdpContextIndex?: number;
10
+ recordHarPath?: string;
11
+ }
12
+ /**
13
+ * Name any human-verification widget embedded in the page.
14
+ *
15
+ * Returns nothing when the page is clear, so a normal snapshot is unchanged.
16
+ */
17
+ export declare function captchaReport(frameUrls: string[]): Record<string, unknown>;
18
+ export declare class BrowserRuntime {
19
+ private readonly options;
20
+ private browser;
21
+ private context;
22
+ private activePage;
23
+ private connection;
24
+ private routeHandler;
25
+ /**
26
+ * Dialogs raised by the action currently running, cleared by `execute`.
27
+ *
28
+ * One buffer on the runtime rather than one per page because the handler is
29
+ * registered on the context, and because operations execute one at a time —
30
+ * a task owns its runtime for the length of one action.
31
+ */
32
+ private dialogs;
33
+ constructor(options: BrowserRuntimeOptions);
34
+ private uploadPaths;
35
+ private ensureContext;
36
+ private page;
37
+ /**
38
+ * The tab list, with the index each entry can be addressed by.
39
+ *
40
+ * Returned by every tab action rather than only by `tabs`, because opening,
41
+ * switching, and closing all renumber the list — a caller that had to issue a
42
+ * separate `tabs` call after each one would be acting on a stale index.
43
+ */
44
+ private describeTabs;
45
+ /**
46
+ * The frames a target may live in, main frame first.
47
+ *
48
+ * Consent banners, cookie walls, payment fields, and captcha widgets are
49
+ * almost always in an iframe, and `page.locator` only ever searched the main
50
+ * document — so the element an agent could plainly see in a screenshot was
51
+ * unaddressable, and the run died on an action timeout. Searching every frame
52
+ * with the main one first keeps precise callers unaffected and makes the
53
+ * embedded case work without a new field to pass.
54
+ *
55
+ * Ad and analytics pages can attach dozens of frames, so the list is bounded.
56
+ */
57
+ private frames;
58
+ /**
59
+ * Every way the caller gave us to find the element, in the order we trust
60
+ * them. Split out from `locator` because waiting and acting need the same
61
+ * list but disagree about what to do when nothing matches yet.
62
+ */
63
+ private candidates;
64
+ /**
65
+ * Resolve a target to a locator that actually matches something.
66
+ *
67
+ * Text matching used to be `exact: true` only, which fails on the whitespace,
68
+ * casing, and nested-markup differences that real pages are full of — a button
69
+ * rendered as `<button> Sign in </button>` never matched `"Sign in"`. Exact is
70
+ * still tried first so a precise caller keeps precise behaviour; the substring
71
+ * form is only consulted when exact found nothing.
72
+ *
73
+ * A target that matches nothing is reported as such instead of being handed to
74
+ * Playwright to fail as an opaque action timeout thirty seconds later.
75
+ */
76
+ private locator;
77
+ /**
78
+ * Wait for a target to reach a state, re-resolving it on every poll.
79
+ *
80
+ * Playwright's own `waitFor` is bound to one frame decided up front, which is
81
+ * exactly wrong here: the consent iframe or captcha widget being waited for
82
+ * usually does not exist yet when the wait starts. Re-reading `page.frames()`
83
+ * each round costs a poll interval of latency and covers the frame that
84
+ * appears halfway through.
85
+ *
86
+ * `visible` and `attached` are satisfied by any one candidate; `hidden` and
87
+ * `detached` must hold for all of them. Fanning out across frames makes that
88
+ * distinction load-bearing — most frames never contain the target, so "some
89
+ * candidate is absent" is true from the first poll and would report a banner
90
+ * as dismissed while it is still on screen.
91
+ */
92
+ private waitForTarget;
93
+ /**
94
+ * Type by pressing keys rather than by assigning the value.
95
+ *
96
+ * `.fill()` sets the field and fires a single `input` event. Anything that
97
+ * listens for keystrokes never sees the text: React inputs with key handlers,
98
+ * autocomplete dropdowns, comboboxes that filter as you type, and fields that
99
+ * enable a submit button only after a `keyup`. The old server used
100
+ * `page.type`, and losing that is most of why typing "stopped working".
101
+ * `.fill("")` is still how the field gets cleared, because it empties one
102
+ * reliably no matter what is in it.
103
+ */
104
+ private typeInto;
105
+ private snapshot;
106
+ /**
107
+ * Read one element rather than the whole page.
108
+ *
109
+ * The old server had `extract_text` and `extract_html`; both were dropped, so
110
+ * a caller wanting one table out of a large document had to take the entire
111
+ * page text and cut it up itself. `inspect` already accepts a target, so
112
+ * scoping it needs no new action.
113
+ */
114
+ private extract;
115
+ private settledSnapshot;
116
+ execute(operation: BrowserOperation): Promise<Record<string, unknown>>;
117
+ private run;
118
+ close(): Promise<void>;
119
+ }
120
+ export * from "./network-policy.js";
121
+ export * from "./selector.js";
122
+ export * from "./stable-dom.js";
123
+ export * from "./browser-connection.js";
124
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,gBAAgB,EAAiB,MAAM,iBAAiB,CAAC;AAKvE,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAI7B,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAsCD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiB1E;AAgGD,qBAAa,cAAc;IAeb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAdpC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAAgD;IACpE;;;;;;OAMG;IACH,OAAO,CAAC,OAAO,CAAiC;gBAEnB,OAAO,EAAE,qBAAqB;YAE7C,WAAW;YA8BX,aAAa;YAqFb,IAAI;IASlB;;;;;;OAMG;YACW,YAAY;IAa1B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM;IAId;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAwBlB;;;;;;;;;;;OAWG;YACW,OAAO;IAarB;;;;;;;;;;;;;;OAcG;YACW,aAAa;IAsC3B;;;;;;;;;;OAUG;YACW,QAAQ;YAeR,QAAQ;IAwCtB;;;;;;;OAOG;YACW,OAAO;YAqBP,eAAe;IAcvB,OAAO,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;YAe9D,GAAG;IAsTX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAuB7B;AAED,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,yBAAyB,CAAC"}