lib-e2e-cypress-for-dummys-ts 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.
package/README.md ADDED
@@ -0,0 +1,459 @@
1
+ # lib-e2e-cypress-for-dummys
2
+
3
+ > A floating widget that records real browser interactions and generates ready-to-run **Cypress E2E test code** — no setup required, no Cypress knowledge needed.
4
+
5
+ ---
6
+
7
+ ## What it does
8
+
9
+ Drop the widget into any web app. Press record. Click around. The widget watches every click, input, select, route change, HTTP call, and navigation event, and translates each one into the matching Cypress command in real time. Stop recording, give the test a name, and the code is saved to IndexedDB — ready to copy into your test suite.
10
+
11
+ **Zero framework coupling.** It is a plain Custom Element (`<lib-e2e-recorder>`). Angular, React, Vue, plain HTML — anything works.
12
+
13
+ ---
14
+
15
+ ## Why use it
16
+
17
+ | Without this library | With this library |
18
+ |---|---|
19
+ | Write `cy.get('[data-cy="..."]').click()` by hand | Click in the browser, get the command automatically |
20
+ | Manually craft `cy.intercept()` for every API call | Every fetch/XHR is captured and the interceptor is generated |
21
+ | Guess which selector attribute to use | Choose the strategy once in settings; the recorder handles the rest |
22
+ | Copy-paste commands into `.cy.ts` files | The advanced editor inserts the `it()` block directly into the file |
23
+ | Maintain tests in one language | Interface in ES / EN / FR / IT / DE, auto-detected from the browser |
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install lib-e2e-cypress-for-dummys-ts idb sweetalert2
31
+ ```
32
+
33
+ The package ships three formats:
34
+
35
+ | File | Format | Use |
36
+ |---|---|---|
37
+ | `dist/index.js` | ESM | Modern bundlers (Vite, Angular, etc.) |
38
+ | `dist/index.cjs` | CJS | CommonJS environments |
39
+ | `dist/index.d.ts` | TypeScript declarations | Type-safe imports |
40
+
41
+ `idb` and `sweetalert2` are peer dependencies and must be installed alongside the library.
42
+
43
+ ---
44
+
45
+ ## Quick start — plain HTML
46
+
47
+ ```html
48
+ <!DOCTYPE html>
49
+ <html>
50
+ <head>
51
+ <!-- SweetAlert2 CSS (required for modals) -->
52
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2/dist/sweetalert2.min.css" />
53
+ </head>
54
+ <body>
55
+ <!-- Your app content here -->
56
+
57
+ <lib-e2e-recorder></lib-e2e-recorder>
58
+
59
+ <script type="module">
60
+ import 'path/to/dist/index.js';
61
+ </script>
62
+ </body>
63
+ </html>
64
+ ```
65
+
66
+ The widget appears as a small floating panel. Press **⏺** to start recording.
67
+
68
+ ---
69
+
70
+ ## Quick start — Angular
71
+
72
+ **1. Register the custom element schema** (in `app.config.ts` or the module):
73
+
74
+ ```typescript
75
+ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
76
+
77
+ // In a standalone component or NgModule:
78
+ schemas: [CUSTOM_ELEMENTS_SCHEMA]
79
+ ```
80
+
81
+ **2. Import the library** in `main.ts` (or any file that is loaded before the widget is used):
82
+
83
+ ```typescript
84
+ import 'lib-e2e-cypress-for-dummys-ts/dist/index.js';
85
+ ```
86
+
87
+ **3. Add the element** to any template (typically `AppComponent`):
88
+
89
+ ```html
90
+ <!-- Only render in non-production environments -->
91
+ @if (!isProduction) {
92
+ <lib-e2e-recorder></lib-e2e-recorder>
93
+ }
94
+ ```
95
+
96
+ **4. Control it programmatically** (optional):
97
+
98
+ ```typescript
99
+ import type { LibE2eRecorderElement } from 'lib-e2e-cypress-for-dummys-ts';
100
+
101
+ const recorder = document.querySelector('lib-e2e-recorder') as LibE2eRecorderElement;
102
+
103
+ // Start / stop recording
104
+ recorder.toggle();
105
+
106
+ // Set the language explicitly
107
+ recorder.setLanguage('en');
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Features
113
+
114
+ ### Recording interactions
115
+
116
+ The recorder automatically captures:
117
+
118
+ | Interaction | Generated Cypress command |
119
+ |---|---|
120
+ | Click on a button or element | `cy.get('[data-cy="submit"]').click()` |
121
+ | Type into a text field | `cy.get('[data-cy="email"]').clear().type('user@example.com')` |
122
+ | Select a `<select>` value | `cy.get('[data-cy="country"]').select('ES')` |
123
+ | SPA route change (push/replace/popstate) | `cy.url().should('include', '/dashboard')` |
124
+ | Page load | `cy.visit('/current-path')` |
125
+
126
+ Input events are **debounced by 1 second** so only the final value is captured, not every keystroke.
127
+
128
+ ---
129
+
130
+ ### HTTP monitoring
131
+
132
+ Every `fetch` and `XMLHttpRequest` call made while recording is captured automatically.
133
+
134
+ For **GET, POST and PUT** requests the recorder generates a pair of commands:
135
+
136
+ ```javascript
137
+ // Interceptor (placed at the top of the test, in beforeEach)
138
+ cy.intercept('GET', '**/api/users').as('get-api-users')
139
+
140
+ // Wait command (placed inline with the test actions)
141
+ cy.wait('@get-api-users').then((interception) => { })
142
+ ```
143
+
144
+ **DELETE** requests are intentionally ignored (read-only guard).
145
+
146
+ #### Advanced HTTP mode
147
+
148
+ Enable **Validaciones de body** in the settings panel to get auto-generated body assertions:
149
+
150
+ ```javascript
151
+ // GET — validates response body fields
152
+ cy.wait('@get-api-users').then((interception) => {
153
+ if (interception.response) {
154
+ expect(interception.response.body.name).to.equal("Alice");
155
+ expect(interception.response.body.role).to.equal("admin");
156
+ }
157
+ })
158
+
159
+ // POST / PUT — validates request body fields
160
+ cy.wait('@post-api-users').then((interception) => {
161
+ expect(interception.request.body.name).to.equal("Alice");
162
+ })
163
+ ```
164
+
165
+ Fields named `id` or `uid` are always skipped (they change between environments). Array responses fall back to the default empty `.then()` block.
166
+
167
+ ---
168
+
169
+ ### Selector strategy
170
+
171
+ The recorder needs to choose an attribute to generate `cy.get()` selectors. Four strategies are available:
172
+
173
+ | Strategy | Generated selector |
174
+ |---|---|
175
+ | `data-cy` (default) | `[data-cy="login-btn"]` |
176
+ | `data-testid` | `[data-testid="login-btn"]` |
177
+ | `aria-label` | `[aria-label="Login"]` |
178
+ | `id` | `#login-btn` |
179
+
180
+ Change the strategy at any time in **⚙️ Config → Estrategia de selector**. The recorder always falls back through the chain (`data-cy` → `id`, etc.) if the preferred attribute is missing on the target element.
181
+
182
+ Forbidden id prefixes (`cdk-`, `mat-`, `ng-`, `mdc-`, `p-`, and others) are automatically excluded to avoid Angular Material / PrimeNG generated ids.
183
+
184
+ ---
185
+
186
+ ### Smart selector picker
187
+
188
+ When the recorder cannot generate a reliable selector for a clicked element (no `data-cy`, `data-testid`, `aria-label`, or clean `id`), the **Smart Selector Picker** appears as an overlay.
189
+
190
+ The picker walks the DOM ancestor chain of the clicked element (up to 10 levels) and colour-codes each ancestor by selector quality:
191
+
192
+ | Badge | Quality | Criteria |
193
+ |---|---|---|
194
+ | 🟢 Excellent | Best | Has `data-cy`, `data-testid`, or `aria-label` |
195
+ | 🔵 Good | Reliable | Has a valid `id` (no framework-generated prefix) |
196
+ | 🟡 Acceptable | Fragile | Has CSS classes — no testing attribute |
197
+ | 🔴 Not recommended | Avoid | Only tag name or inline `style` |
198
+
199
+ **How to use:**
200
+ 1. The picker auto-selects the best available ancestor
201
+ 2. Use **↑ / ↓** to navigate the ancestor list
202
+ 3. Press **Enter** (or click a row) to record `cy.get('<selector>').click()`
203
+ 4. Press **Escape** or click outside to dismiss without recording
204
+
205
+ The picker also closes automatically when recording is stopped or paused.
206
+
207
+ **Toggle in settings:** ⚙️ Config → Smart selector. When disabled, unresolvable clicks are silently dropped (the previous behaviour).
208
+
209
+ ---
210
+
211
+ ### Saving and managing tests
212
+
213
+ When recording stops, a dialog prompts you to:
214
+
215
+ - Give the test a **description** (becomes the `it('…')` label)
216
+ - Optionally add **tags** for filtering later (e.g. `smoke`, `login`, `regression`)
217
+ - Choose **Save** (keeps it in IndexedDB) or **Save and edit** (opens the advanced editor immediately)
218
+
219
+ The **📋 Tests** panel lets you:
220
+
221
+ - Browse all saved tests filtered by tag
222
+ - Expand a test to see its commands and interceptors
223
+ - Copy the full `describe()` block to the clipboard with a custom suite name
224
+ - Copy just the commands or just the interceptors separately
225
+ - Multi-select tests and generate a combined `describe()` block
226
+ - Delete individual tests
227
+
228
+ ---
229
+
230
+ ### Command previewer
231
+
232
+ The **⌨️ Commands** panel shows the commands captured in the current (unsaved) recording in real time. While the panel is open you can:
233
+
234
+ - Reorder commands with the up/down arrows
235
+ - Delete individual commands
236
+ - Add a custom assertion with the **assertion builder**
237
+
238
+ #### Assertion builder
239
+
240
+ The assertion builder at the bottom of the command panel lets you add `cy.get().should()` commands without typing:
241
+
242
+ 1. Enter the CSS selector (e.g. `[data-cy="error-msg"]`)
243
+ 2. Choose the assertion type from the dropdown:
244
+ - `be.visible`, `not.exist`, `be.disabled`, `be.checked` — no value required
245
+ - `contain.text`, `have.value`, `have.length`, `have.class`, `have.attr` — enter the expected value
246
+ 3. Click **➕ Añadir**
247
+
248
+ ---
249
+
250
+ ### Advanced editor (direct file insertion)
251
+
252
+ The **📁 Files** panel uses the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API) to browse your local Cypress folder and insert tests directly into `.cy.ts` files — no copy-paste needed.
253
+
254
+ **Setup (one time):**
255
+
256
+ 1. Open **⚙️ Config → Carpeta Cypress**
257
+ 2. Click **📁 Seleccionar carpeta** and pick the folder that contains the `cypress/` directory
258
+ 3. The browser stores the permission — you won't be asked again
259
+
260
+ **Expected folder structure:**
261
+
262
+ ```
263
+ cypress/ ← select this folder
264
+ └── e2e/ ← the recorder reads .cy.ts files from here
265
+ └── login.cy.ts
266
+ ```
267
+
268
+ **Using the editor:**
269
+
270
+ 1. Open **📁 Files** → select a `.cy.ts` file from the tree
271
+ 2. The `it()` block for the selected test appears in the panel
272
+ 3. Click **💾 Insertar en archivo** — the block is appended inside the last `})` of the describe
273
+ 4. Interceptors are automatically wrapped in a `beforeEach()`
274
+ 5. Alternatively, click **✏️ Editar manualmente** to open a full diff editor with save support
275
+
276
+ > The File System Access API is supported in Chromium-based browsers (Chrome, Edge, Opera). Firefox and Safari do not support it.
277
+
278
+ ---
279
+
280
+ ### Recording history
281
+
282
+ The last **5 recordings** are automatically saved to `localStorage` so you never lose work if you accidentally close the dialog without saving. Use `recorder.recoverLastRecording()` to restore the most recent recording programmatically.
283
+
284
+ ---
285
+
286
+ ### Keyboard shortcuts
287
+
288
+ | Shortcut | Action |
289
+ |---|---|
290
+ | `Ctrl + R` | Start / stop recording |
291
+ | `Ctrl + P` | Pause / resume recording |
292
+ | `Ctrl + 1` | Open saved tests panel |
293
+ | `Ctrl + 2` | Open command previewer |
294
+ | `Ctrl + 3` | Open settings |
295
+
296
+ ---
297
+
298
+ ### Import / Export
299
+
300
+ Back up all tests to a JSON file or restore from a previous backup via **⚙️ Config → Datos**:
301
+
302
+ - **⬆️ Export tests** — downloads `e2e-cypress-export.json`
303
+ - **⬇️ Import tests** — uploads a JSON file and merges it into the current database
304
+
305
+ JSON format:
306
+
307
+ ```json
308
+ {
309
+ "tests": [ { "name": "Login correcto", "createdAt": 1234567890 } ],
310
+ "interceptors": []
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ### Internationalisation
317
+
318
+ The interface auto-detects the browser language. Supported languages:
319
+
320
+ | Code | Language |
321
+ |---|---|
322
+ | `es` | Spanish (default fallback) |
323
+ | `en` | English |
324
+ | `fr` | French |
325
+ | `it` | Italian |
326
+ | `de` | German |
327
+
328
+ Override the language programmatically:
329
+
330
+ ```typescript
331
+ recorder.setLanguage('en'); // accepts: 'es' | 'en' | 'fr' | 'it' | 'de'
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Public API
337
+
338
+ ### `<lib-e2e-recorder>` element
339
+
340
+ ```typescript
341
+ class LibE2eRecorderElement extends HTMLElement {
342
+ // Injected services (auto-created if not set)
343
+ recording: RecordingService;
344
+ persistence: PersistenceService;
345
+ translation: TranslationService;
346
+
347
+ // State (read-only in normal use)
348
+ isRecording: boolean;
349
+ isPaused: boolean;
350
+ cypressCommands: string[];
351
+ interceptors: string[];
352
+
353
+ // Dialog flags
354
+ isCommandsDialogOpen: boolean;
355
+ isSavedTestsDialogOpen: boolean;
356
+ isSaveTestDialogOpen: boolean;
357
+ isSettingsDialogOpen: boolean;
358
+ isAdvancedEditorDialogOpen: boolean;
359
+
360
+ // Methods
361
+ toggle(): void; // start / stop recording
362
+ togglePause(): void; // pause / resume
363
+ setLanguage(lang?: string): void; // set UI language
364
+
365
+ showCommandsDialog(): void;
366
+ showSavedTestsDialog(): void;
367
+ showSaveTestDialog(): void;
368
+ showSettingsDialog(): void;
369
+ showAdvancedEditorDialog(testId?: number): void;
370
+
371
+ getRecordingHistory(): Array<{ commands: string[]; interceptors: string[]; savedAt: number }>;
372
+ recoverLastRecording(): void;
373
+ clearRecordingHistory(): void;
374
+ }
375
+ ```
376
+
377
+ ### `RecordingService`
378
+
379
+ ```typescript
380
+ class RecordingService {
381
+ selectorStrategy: 'data-cy' | 'data-testid' | 'aria-label' | 'id';
382
+
383
+ startRecording(): void;
384
+ stopRecording(): void;
385
+ pauseRecording(): void;
386
+ resumeRecording(): void;
387
+ toggleRecording(): void;
388
+ togglePause(): void;
389
+
390
+ addCommand(cmd: string): void; // respects recording/paused state
391
+ appendCommand(cmd: string): void; // always appends (for manual assertions)
392
+ removeCommand(index: number): void;
393
+ moveCommand(from: number, to: number): void;
394
+
395
+ registerInterceptor(method: string, url: string, alias: string): void;
396
+ removeInterceptor(index: number): void;
397
+
398
+ clearCommands(): void;
399
+
400
+ getCommandsSnapshot(): string[];
401
+ getInterceptorsSnapshot(): string[];
402
+
403
+ onCommandsChange(fn: (cmds: string[]) => void): () => void;
404
+ onInterceptorsChange(fn: (ints: string[]) => void): () => void;
405
+ onRecordingChange(fn: (isRecording: boolean) => void): () => void;
406
+ onPauseChange(fn: (isPaused: boolean) => void): () => void;
407
+
408
+ destroy(): void;
409
+ }
410
+ ```
411
+
412
+ ### `PersistenceService`
413
+
414
+ ```typescript
415
+ class PersistenceService {
416
+ insertTest(name: string, commands?: string[], interceptors?: string[], tags?: string[]): Promise<number>;
417
+ getAllTests(): Promise<TestWithDetails[]>;
418
+ getTestById(testId: number): Promise<TestDetail | null>;
419
+ deleteTest(id: number): Promise<void>;
420
+
421
+ setConfig(config: Record<string, unknown>): Promise<void>;
422
+ setConfigKey(key: string, value: unknown): Promise<void>;
423
+ getConfig(key: string): Promise<Record<string, unknown> | null>;
424
+ getGeneralConfig(): Promise<ConfigRecord | null>;
425
+
426
+ clearAllData(): Promise<void>;
427
+ requestDirectoryPermissions(): Promise<void>;
428
+ }
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Technical notes
434
+
435
+ - **Storage:** IndexedDB via [`idb`](https://github.com/jakearchibald/idb). Data survives page reloads and browser restarts.
436
+ - **Modals:** [`SweetAlert2`](https://sweetalert2.github.io/) with a custom dark theme injected as a `<style>` tag.
437
+ - **Shadow DOM:** All components use `mode: 'open'` shadow roots, so widget styles never bleed into the host application.
438
+ - **Bundle:** ESM + CJS, compiled to ES2022. No framework runtime included.
439
+ - **Browser support:** Chromium 105+ for full feature set (File System Access API). Recording and saving work in all modern browsers.
440
+
441
+ ---
442
+
443
+ ## Development
444
+
445
+ ```bash
446
+ npm install
447
+ npm run build # compile with tsup
448
+ npm test # run vitest unit tests
449
+ npm run test:coverage # coverage report (≥80% gate)
450
+ npm run lint # ESLint
451
+ ```
452
+
453
+ See [`CLAUDE.md`](./CLAUDE.md) for the full development workflow (Spec-Driven Development, TDD, commit conventions).
454
+
455
+ ---
456
+
457
+ ## Licence
458
+
459
+ MIT
@@ -0,0 +1,60 @@
1
+ // src/utils/selector-quality.utils.ts
2
+ var FORBIDDEN_ID_PREFIXES = [
3
+ "cdk-",
4
+ "mat-",
5
+ "p-",
6
+ "ng-",
7
+ "mdc-",
8
+ "primeng-",
9
+ "auto-",
10
+ "field-",
11
+ "input-",
12
+ "select-"
13
+ ];
14
+ function getSelectorQuality(el) {
15
+ if (el.hasAttribute("data-cy") || el.hasAttribute("data-testid") || el.hasAttribute("aria-label")) {
16
+ return "excellent";
17
+ }
18
+ const rawId = el.id;
19
+ if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
20
+ return "good";
21
+ }
22
+ if (typeof el.className === "string" && el.className.trim().length > 0) {
23
+ return "acceptable";
24
+ }
25
+ return "poor";
26
+ }
27
+ function buildPickerSelector(el) {
28
+ const dataCy = el.getAttribute("data-cy");
29
+ if (dataCy) return `[data-cy="${dataCy}"]`;
30
+ const dataTestid = el.getAttribute("data-testid");
31
+ if (dataTestid) return `[data-testid="${dataTestid}"]`;
32
+ const ariaLabel = el.getAttribute("aria-label");
33
+ if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
34
+ const rawId = el.id;
35
+ if (rawId && rawId.length < 25 && /^[a-zA-Z][\w-]*$/.test(rawId) && !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) && !/^\d+$/.test(rawId)) {
36
+ return `#${rawId}`;
37
+ }
38
+ if (typeof el.className === "string" && el.className.trim().length > 0) {
39
+ const classes = el.className.trim().split(/\s+/);
40
+ return classes.map((c) => `.${c}`).join("");
41
+ }
42
+ return el.tagName.toLowerCase();
43
+ }
44
+
45
+ // src/utils/html.utils.ts
46
+ function escHtml(s) {
47
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
48
+ }
49
+ function escAttr(s) {
50
+ return s.replace(/"/g, "&quot;").replace(/</g, "&lt;");
51
+ }
52
+
53
+ export {
54
+ FORBIDDEN_ID_PREFIXES,
55
+ getSelectorQuality,
56
+ buildPickerSelector,
57
+ escHtml,
58
+ escAttr
59
+ };
60
+ //# sourceMappingURL=chunk-DRNRKHXN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/selector-quality.utils.ts","../src/utils/html.utils.ts"],"sourcesContent":["export type SelectorQuality = 'excellent' | 'good' | 'acceptable' | 'poor';\n\nexport const FORBIDDEN_ID_PREFIXES = [\n 'cdk-', 'mat-', 'p-', 'ng-', 'mdc-',\n 'primeng-', 'auto-', 'field-', 'input-', 'select-',\n];\n\nexport function getSelectorQuality(el: HTMLElement): SelectorQuality {\n if (\n el.hasAttribute('data-cy') ||\n el.hasAttribute('data-testid') ||\n el.hasAttribute('aria-label')\n ) {\n return 'excellent';\n }\n\n const rawId = el.id;\n if (\n rawId &&\n rawId.length < 25 &&\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\n !/^\\d+$/.test(rawId)\n ) {\n return 'good';\n }\n\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\n return 'acceptable';\n }\n\n return 'poor';\n}\n\n/** Returns the best cy.get() selector string for a given element. */\nexport function buildPickerSelector(el: HTMLElement): string {\n const dataCy = el.getAttribute('data-cy');\n if (dataCy) return `[data-cy=\"${dataCy}\"]`;\n\n const dataTestid = el.getAttribute('data-testid');\n if (dataTestid) return `[data-testid=\"${dataTestid}\"]`;\n\n const ariaLabel = el.getAttribute('aria-label');\n if (ariaLabel) return `[aria-label=\"${ariaLabel}\"]`;\n\n const rawId = el.id;\n if (\n rawId &&\n rawId.length < 25 &&\n /^[a-zA-Z][\\w-]*$/.test(rawId) &&\n !FORBIDDEN_ID_PREFIXES.some((p) => rawId.startsWith(p)) &&\n !/^\\d+$/.test(rawId)\n ) {\n return `#${rawId}`;\n }\n\n if (typeof el.className === 'string' && el.className.trim().length > 0) {\n const classes = el.className.trim().split(/\\s+/);\n return classes.map((c) => `.${c}`).join('');\n }\n\n return el.tagName.toLowerCase();\n}\n","export function escHtml(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nexport function escAttr(s: string): string {\n return s.replace(/\"/g, '&quot;').replace(/</g, '&lt;');\n}\n"],"mappings":";AAEO,IAAM,wBAAwB;AAAA,EACnC;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAC7B;AAAA,EAAY;AAAA,EAAS;AAAA,EAAU;AAAA,EAAU;AAC3C;AAEO,SAAS,mBAAmB,IAAkC;AACnE,MACE,GAAG,aAAa,SAAS,KACzB,GAAG,aAAa,aAAa,KAC7B,GAAG,aAAa,YAAY,GAC5B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,GAAG;AACjB,MACE,SACA,MAAM,SAAS,MACf,mBAAmB,KAAK,KAAK,KAC7B,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC,KACtD,CAAC,QAAQ,KAAK,KAAK,GACnB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,GAAG,cAAc,YAAY,GAAG,UAAU,KAAK,EAAE,SAAS,GAAG;AACtE,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,IAAyB;AAC3D,QAAM,SAAS,GAAG,aAAa,SAAS;AACxC,MAAI,OAAQ,QAAO,aAAa,MAAM;AAEtC,QAAM,aAAa,GAAG,aAAa,aAAa;AAChD,MAAI,WAAY,QAAO,iBAAiB,UAAU;AAElD,QAAM,YAAY,GAAG,aAAa,YAAY;AAC9C,MAAI,UAAW,QAAO,gBAAgB,SAAS;AAE/C,QAAM,QAAQ,GAAG;AACjB,MACE,SACA,MAAM,SAAS,MACf,mBAAmB,KAAK,KAAK,KAC7B,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,WAAW,CAAC,CAAC,KACtD,CAAC,QAAQ,KAAK,KAAK,GACnB;AACA,WAAO,IAAI,KAAK;AAAA,EAClB;AAEA,MAAI,OAAO,GAAG,cAAc,YAAY,GAAG,UAAU,KAAK,EAAE,SAAS,GAAG;AACtE,UAAM,UAAU,GAAG,UAAU,KAAK,EAAE,MAAM,KAAK;AAC/C,WAAO,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE;AAAA,EAC5C;AAEA,SAAO,GAAG,QAAQ,YAAY;AAChC;;;AC9DO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC5E;AAEO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,QAAQ,EAAE,QAAQ,MAAM,MAAM;AACvD;","names":[]}