@happyvertical/smrt-svelte 0.38.1 → 0.38.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.
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Unit tests for the AdminShell activity feed adapter's reconciliation core
3
+ * (#1779).
4
+ *
5
+ * These exercise {@link ActivityFeedReconciler} — the pure diff between a set of
6
+ * live rows and the shell activity registry — against a REAL {@link ShellState}.
7
+ * Per repo policy nothing here is mocked: the shell, its activity events, and
8
+ * the mapping are all real; the reconciler is fed plain row arrays that stand in
9
+ * for successive live-collection ticks (`liveCollection`'s engine is exercised
10
+ * separately in `activity-feed.integration.svelte.test.ts`). This isolates the
11
+ * mapping / create-update-remove / ownership behavior the adapter guarantees.
12
+ */
13
+ import { afterEach, describe, expect, it } from 'vitest';
14
+ import { createShellState } from '../../components/workspace/admin-shell/state.svelte.js';
15
+ import { ActivityFeedReconciler, } from '../activity-feed.svelte.js';
16
+ /** Editorial map: a job row → focus-scope activity, or `null` for a draft. */
17
+ const mapJob = (row) => {
18
+ if (row.draft)
19
+ return null;
20
+ return {
21
+ kind: 'render-job',
22
+ scope: 'focus',
23
+ label: row.title,
24
+ status: row.state,
25
+ progress: row.progress ?? null,
26
+ };
27
+ };
28
+ /** Record every activity event the shell emits, for sequence assertions. */
29
+ function recordEvents(shell) {
30
+ const events = [];
31
+ const stop = shell.watchActivities((event) => events.push(event));
32
+ return { events, stop };
33
+ }
34
+ describe('ActivityFeedReconciler', () => {
35
+ const teardown = [];
36
+ afterEach(() => {
37
+ for (const fn of teardown.splice(0))
38
+ fn();
39
+ });
40
+ function setup() {
41
+ const shell = createShellState();
42
+ const recorded = recordEvents(shell);
43
+ teardown.push(recorded.stop);
44
+ const reconciler = new ActivityFeedReconciler(shell, mapJob);
45
+ return { shell, reconciler, events: recorded.events };
46
+ }
47
+ it('upserts a shell activity when a row first appears', () => {
48
+ const { shell, reconciler, events } = setup();
49
+ reconciler.reconcile([
50
+ { id: 'job-1', title: 'Render intro', state: 'running', progress: 20 },
51
+ ]);
52
+ const activities = shell.listActivities();
53
+ expect(activities).toHaveLength(1);
54
+ expect(activities[0]).toMatchObject({
55
+ id: 'job-1',
56
+ kind: 'render-job',
57
+ scope: 'focus',
58
+ label: 'Render intro',
59
+ status: 'running',
60
+ progress: 20,
61
+ });
62
+ // The activity id defaults to the row id, and the shell stamped createdAt.
63
+ expect(activities[0].createdAt).toBeTruthy();
64
+ // A single create event (upsert), no transition/remove.
65
+ expect(events.map((e) => e.type)).toEqual(['upsert']);
66
+ });
67
+ it('updates the activity in place when a row changes (status transition)', () => {
68
+ const { shell, reconciler, events } = setup();
69
+ reconciler.reconcile([
70
+ { id: 'job-1', title: 'Render intro', state: 'running', progress: 20 },
71
+ ]);
72
+ const createdAt = shell.listActivities()[0].createdAt;
73
+ // Same row id, advanced progress + status flip → an UPDATE, not a second
74
+ // create, and the shell reports it as a `transition` (status changed).
75
+ reconciler.reconcile([
76
+ { id: 'job-1', title: 'Render intro', state: 'completed', progress: 100 },
77
+ ]);
78
+ const activities = shell.listActivities();
79
+ expect(activities).toHaveLength(1);
80
+ expect(activities[0]).toMatchObject({
81
+ id: 'job-1',
82
+ status: 'completed',
83
+ progress: 100,
84
+ });
85
+ // createdAt preserved across the update (update went through updateActivity,
86
+ // not a fresh upsert that would re-stamp it).
87
+ expect(activities[0].createdAt).toBe(createdAt);
88
+ expect(events.map((e) => e.type)).toEqual(['upsert', 'transition']);
89
+ });
90
+ it('performs no shell mutation when a reconcile tick is unchanged', () => {
91
+ const { reconciler, events } = setup();
92
+ const rows = [
93
+ { id: 'job-1', title: 'Render intro', state: 'running', progress: 20 },
94
+ ];
95
+ reconciler.reconcile(rows);
96
+ // Re-feed identical content (a common no-op reactive tick): a new array with
97
+ // an equal-but-distinct row object. Must not emit a second event.
98
+ reconciler.reconcile([{ ...rows[0] }]);
99
+ expect(events.map((e) => e.type)).toEqual(['upsert']);
100
+ });
101
+ it('removes the activity when its row vanishes from the collection', () => {
102
+ const { shell, reconciler, events } = setup();
103
+ reconciler.reconcile([
104
+ { id: 'job-1', title: 'Render intro', state: 'running' },
105
+ { id: 'job-2', title: 'Render outro', state: 'queued' },
106
+ ]);
107
+ expect(shell.listActivities()).toHaveLength(2);
108
+ // job-1 gone from the next tick → removed; job-2 unchanged → left alone.
109
+ reconciler.reconcile([
110
+ { id: 'job-2', title: 'Render outro', state: 'queued' },
111
+ ]);
112
+ const remaining = shell.listActivities();
113
+ expect(remaining).toHaveLength(1);
114
+ expect(remaining[0].id).toBe('job-2');
115
+ expect(events.map((e) => e.type)).toEqual([
116
+ 'upsert', // job-1 create
117
+ 'upsert', // job-2 create
118
+ 'remove', // job-1 removed
119
+ ]);
120
+ });
121
+ it('treats a row that maps to null as absent (excluded, then removed when it flips)', () => {
122
+ const { shell, reconciler } = setup();
123
+ // A draft row maps to null → never becomes an activity.
124
+ reconciler.reconcile([
125
+ { id: 'job-1', title: 'Draft', state: 'queued', draft: true },
126
+ ]);
127
+ expect(shell.listActivities()).toHaveLength(0);
128
+ // It leaves draft → now an activity appears.
129
+ reconciler.reconcile([
130
+ { id: 'job-1', title: 'Published', state: 'running' },
131
+ ]);
132
+ expect(shell.listActivities()).toHaveLength(1);
133
+ // It goes back to draft (maps to null again) → activity is removed.
134
+ reconciler.reconcile([
135
+ { id: 'job-1', title: 'Published', state: 'running', draft: true },
136
+ ]);
137
+ expect(shell.listActivities()).toHaveLength(0);
138
+ });
139
+ it('lets the map decouple the activity id from the row id', () => {
140
+ const shell = createShellState();
141
+ const reconciler = new ActivityFeedReconciler(shell, (row) => ({
142
+ id: `activity:${row.id}`,
143
+ kind: 'render-job',
144
+ scope: 'system',
145
+ label: row.title,
146
+ status: row.state,
147
+ }));
148
+ reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
149
+ const activities = shell.listActivities();
150
+ expect(activities).toHaveLength(1);
151
+ expect(activities[0].id).toBe('activity:job-1');
152
+ });
153
+ it('routes an activity to the shell edge for its scope', () => {
154
+ const { shell, reconciler } = setup();
155
+ reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
156
+ // scope 'focus' homes to the right edge (SCOPE_EDGES.focus === 'right').
157
+ expect(shell.listActivities({ edge: 'right' })).toHaveLength(1);
158
+ });
159
+ it('re-homes the activity to the new edge when its scope changes (no explicit edge)', () => {
160
+ const shell = createShellState();
161
+ // Map keys scope off the row so a later tick can change it; no `edge` set.
162
+ const reconciler = new ActivityFeedReconciler(shell, (row) => ({
163
+ kind: 'render-job',
164
+ // Reuse the unused `draft` flag as a cheap "move to system scope" toggle.
165
+ scope: row.draft ? 'system' : 'focus',
166
+ label: row.title,
167
+ status: row.state,
168
+ }));
169
+ reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
170
+ // Focus → right edge.
171
+ expect(shell.listActivities({ edge: 'right' })).toHaveLength(1);
172
+ expect(shell.listActivities({ edge: 'bottom' })).toHaveLength(0);
173
+ // Scope flips to system; the map still sets no explicit edge. The activity
174
+ // must RE-DERIVE its edge from the new scope (system → bottom), not stay
175
+ // pinned to the old right edge.
176
+ reconciler.reconcile([
177
+ { id: 'job-1', title: 'Job', state: 'running', draft: true },
178
+ ]);
179
+ expect(shell.listActivities({ edge: 'right' })).toHaveLength(0);
180
+ expect(shell.listActivities({ edge: 'bottom' })).toHaveLength(1);
181
+ expect(shell.listActivities()[0].scope).toBe('system');
182
+ });
183
+ it('clears a previously-set optional field when a later mapping drops it', () => {
184
+ const shell = createShellState();
185
+ // Map emits `message`/`detailHref` only while the job is running.
186
+ const reconciler = new ActivityFeedReconciler(shell, (row) => ({
187
+ kind: 'render-job',
188
+ scope: 'focus',
189
+ label: row.title,
190
+ status: row.state,
191
+ ...(row.state === 'running'
192
+ ? { message: 'Rendering frames', detailHref: '/jobs/job-1' }
193
+ : {}),
194
+ }));
195
+ reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'running' }]);
196
+ let activity = shell.listActivities()[0];
197
+ expect(activity.message).toBe('Rendering frames');
198
+ expect(activity.detailHref).toBe('/jobs/job-1');
199
+ // The job completes → the map no longer emits message/detailHref. Those
200
+ // stale values must be CLEARED, not retained from the previous mapping.
201
+ reconciler.reconcile([{ id: 'job-1', title: 'Job', state: 'completed' }]);
202
+ activity = shell.listActivities()[0];
203
+ expect(activity.status).toBe('completed');
204
+ expect(activity.message).toBeUndefined();
205
+ expect(activity.detailHref).toBeUndefined();
206
+ });
207
+ it('dispose() removes exactly the feed activities and stops reconciling', () => {
208
+ const { shell, reconciler, events } = setup();
209
+ // An activity from ANOTHER source the feed must never touch.
210
+ shell.upsertActivity({
211
+ id: 'external-1',
212
+ label: 'External',
213
+ kind: 'other',
214
+ scope: 'app',
215
+ status: 'running',
216
+ });
217
+ reconciler.reconcile([
218
+ { id: 'job-1', title: 'Render intro', state: 'running' },
219
+ ]);
220
+ expect(shell.listActivities()).toHaveLength(2);
221
+ reconciler.dispose();
222
+ // Only the feed's own activity was retracted; the external one survives.
223
+ const remaining = shell.listActivities();
224
+ expect(remaining).toHaveLength(1);
225
+ expect(remaining[0].id).toBe('external-1');
226
+ expect(reconciler.isDisposed).toBe(true);
227
+ // Reconciling after dispose is a no-op (no new activities, no events).
228
+ const eventsBefore = events.length;
229
+ reconciler.reconcile([
230
+ { id: 'job-2', title: 'Late job', state: 'running' },
231
+ ]);
232
+ expect(shell.listActivities()).toHaveLength(1);
233
+ expect(events.length).toBe(eventsBefore);
234
+ });
235
+ });
@@ -0,0 +1,210 @@
1
+ /**
2
+ * AdminShell activity feed adapter — bridge a `@happyvertical/smrt-web` live
3
+ * collection into the AdminShell activity registry (#1779, part of the WASD
4
+ * AdminShell epic #1766).
5
+ *
6
+ * This is a thin, OPT-IN CONSUMER of already-shipped infrastructure, not a new
7
+ * generated surface. It sits at the intersection of two existing contracts:
8
+ *
9
+ * - the runes-reactive {@link liveCollection} view over a `SmrtWebCollection`
10
+ * (`@happyvertical/smrt-svelte/web`, slice A of #1761), and
11
+ * - the `ShellState` activity registry (`upsertActivity` / `updateActivity` /
12
+ * `removeActivity`, from `components/workspace/admin-shell`).
13
+ *
14
+ * The app supplies (a) the `@smrt()` DOMAIN collection (a `SmrtWebCollection`)
15
+ * and (b) an editorial {@link ActivityFeedMap} that turns each row into the
16
+ * shell-facing activity fields (`kind`, `scope`, `label`, `progress`, …). The
17
+ * adapter reconciles the mapped rows against the shell as rows appear, change,
18
+ * and vanish, and returns a disposer that removes the activities it created.
19
+ *
20
+ * ── Why it lives behind the `/web` opt-in entry ─────────────────────────────
21
+ * `liveCollection` pulls the client-data engine (`@tanstack/db` +
22
+ * `@tanstack/svelte-db`). Keeping this adapter in the `/web` subpath keeps that
23
+ * engine OUT of the AdminShell core: nothing under `components/workspace/` may
24
+ * import this module, so `@happyvertical/smrt-svelte/workspace` stays
25
+ * transport-agnostic and TanStack-free (a hard constraint of #1766). The app
26
+ * wires the adapter at the edge, next to where it already opts into `/web`.
27
+ *
28
+ * ── Engine-absorption boundary ──────────────────────────────────────────────
29
+ * No `@tanstack/*` type appears here. The adapter reconciles against the plain
30
+ * DTO rows exposed by {@link liveCollection} (which already strips the engine's
31
+ * `$`-prefixed virtual props), so the engine stays swappable behind the same
32
+ * boundary the rest of `/web` respects.
33
+ *
34
+ * ── Reconciliation, not change-diffing ──────────────────────────────────────
35
+ * Rather than decode the engine's change payload, the adapter re-derives the
36
+ * activity set from the current live rows on every reactive tick (via an
37
+ * internal `$effect` over `view.rows`) and diffs it against what it last pushed
38
+ * to the shell. This is robust to the change-notification shape (which is
39
+ * engine-internal) and mirrors how the runtime's own consumers treat a change
40
+ * as a "something moved, re-read" signal.
41
+ */
42
+ import type { SmrtWebCollection, SmrtWebRow } from '@happyvertical/smrt-web';
43
+ import type { ShellState } from '../components/workspace/admin-shell/state.svelte.js';
44
+ import type { ShellActivity } from '../components/workspace/admin-shell/types.js';
45
+ /**
46
+ * The shell-facing fields an {@link ActivityFeedMap} produces for one row — the
47
+ * editorial half of a {@link ShellActivity}. The adapter owns the bookkeeping
48
+ * fields (`id`, `createdAt`, `updatedAt`), so they are omitted here; `id` may be
49
+ * supplied to decouple the activity id from the row id (it defaults to the row's
50
+ * `id`). Everything a consumer legitimately controls (`kind`, `scope`, `label`,
51
+ * `status`, `subject`, `progress`, `detailHref`, `message`, `edge`, `cancel`)
52
+ * passes straight through.
53
+ */
54
+ export type ShellActivityInput = Omit<ShellActivity, 'id' | 'createdAt' | 'updatedAt'> & {
55
+ /**
56
+ * Explicit activity id. Defaults to the row's `id` when omitted — the common
57
+ * case (one activity per row). Supply it only to map a row onto a
58
+ * differently-keyed activity.
59
+ */
60
+ id?: string;
61
+ };
62
+ /**
63
+ * Map one live-collection row to its shell activity fields, or `null` to
64
+ * exclude the row from the feed.
65
+ *
66
+ * Returning `null` is a first-class signal: a row that maps to `null` is NOT an
67
+ * activity (e.g. a draft the shell should ignore), and a row that STOPS mapping
68
+ * to an activity — flips from a value to `null` — is removed from the shell,
69
+ * exactly as if it had vanished from the collection. This lets the editorial map
70
+ * gate which rows surface without the consumer filtering the collection.
71
+ *
72
+ * @typeParam TData - the row DTO shape carried by the collection.
73
+ */
74
+ export type ActivityFeedMap<TData extends object> = (row: SmrtWebRow<TData>) => ShellActivityInput | null;
75
+ /**
76
+ * Options for {@link activityFeed}.
77
+ *
78
+ * @typeParam TData - the row DTO shape carried by {@link collection}.
79
+ */
80
+ export interface ActivityFeedOptions<TData extends object> {
81
+ /**
82
+ * The domain live collection to bridge — a `SmrtWebCollection` from
83
+ * `@happyvertical/smrt-web` (e.g. built by the app's `createSmrtCollection`
84
+ * over its `@smrt()` class). Its rows drive the feed.
85
+ */
86
+ collection: SmrtWebCollection<TData>;
87
+ /**
88
+ * The editorial mapping from a row to its shell activity fields (or `null` to
89
+ * exclude the row). See {@link ActivityFeedMap}.
90
+ */
91
+ map: ActivityFeedMap<TData>;
92
+ /** The AdminShell state whose activity registry this feed drives. */
93
+ shell: ShellState;
94
+ /**
95
+ * Forwarded to the underlying {@link liveCollection}: trigger the collection's
96
+ * first load eagerly when the feed is created. Defaults to `true`.
97
+ */
98
+ preload?: boolean;
99
+ }
100
+ /**
101
+ * A handle to a running {@link activityFeed}. Call {@link dispose} to detach the
102
+ * feed and remove every activity it created; read {@link isDisposed} to check.
103
+ */
104
+ export interface ActivityFeedHandle {
105
+ /**
106
+ * Detach the feed: remove every activity this feed still owns from the shell
107
+ * and stop reconciling. Idempotent. The underlying live-query subscription is
108
+ * torn down by Svelte when the hosting component unmounts (that is where
109
+ * `activityFeed` must be called); call this to remove the feed's activities
110
+ * sooner, or when tearing a feed down without unmounting.
111
+ */
112
+ dispose(): void;
113
+ /** True once {@link dispose} has run. */
114
+ readonly isDisposed: boolean;
115
+ }
116
+ /**
117
+ * The pure reconciliation core of {@link activityFeed}: it owns the diff between
118
+ * a set of live rows and the shell's activity registry, with NO Svelte reactive
119
+ * or client-data-engine dependency. {@link activityFeed} wraps one of these in a
120
+ * `$effect` over a {@link liveCollection} view; this split keeps the mapping /
121
+ * create-update-remove / ownership logic testable against a real `ShellState`
122
+ * with plain row arrays (mock only externals — see the `__tests__`).
123
+ *
124
+ * @internal Not part of the public surface — use {@link activityFeed}.
125
+ * @typeParam TData - the row DTO shape being reconciled.
126
+ */
127
+ export declare class ActivityFeedReconciler<TData extends object> {
128
+ private readonly shell;
129
+ private readonly map;
130
+ /**
131
+ * Activities this feed currently owns, keyed by activity id, each with the
132
+ * last resolved input + fingerprint so a reconcile can tell created / changed
133
+ * / unchanged apart and remove exactly its own set on teardown.
134
+ */
135
+ private readonly owned;
136
+ private disposed;
137
+ constructor(shell: ShellState, map: ActivityFeedMap<TData>);
138
+ /** True once {@link dispose} has run. */
139
+ get isDisposed(): boolean;
140
+ /**
141
+ * Reconcile `rows` against the owned activity set: upsert newly-appearing
142
+ * activities, update changed ones, remove vanished ones. A pure diff — an
143
+ * unchanged tick (same rows, same mappings) performs ZERO shell mutations.
144
+ * No-op once disposed.
145
+ */
146
+ reconcile(rows: ReadonlyArray<SmrtWebRow<TData>>): void;
147
+ /**
148
+ * Retract exactly this feed's activities from the shell and stop reconciling.
149
+ * Idempotent. Activities owned by other sources are left untouched.
150
+ */
151
+ dispose(): void;
152
+ }
153
+ /**
154
+ * Bridge a `@happyvertical/smrt-web` live collection into an AdminShell's
155
+ * activity registry: each mapped row becomes a {@link ShellActivity} that
156
+ * appears, updates, and disappears in the shell as the collection changes.
157
+ *
158
+ * MUST be called during Svelte component initialization — it delegates to
159
+ * {@link liveCollection} (which installs a `$effect`) and installs its own
160
+ * reconciliation `$effect`. Both bind to the calling component's lifecycle, so
161
+ * the live subscription and reconciliation tear down automatically on unmount.
162
+ * Unmount ALSO auto-retracts the feed's activities from the shell (via an
163
+ * `$effect` teardown), so they never linger after the host component is gone —
164
+ * no manual cleanup required. The returned {@link ActivityFeedHandle.dispose}
165
+ * retracts them SOONER (without unmounting) and is idempotent, so calling it and
166
+ * then unmounting is safe.
167
+ *
168
+ * Lifecycle per row:
169
+ * - a row that newly maps to an activity → `shell.upsertActivity(...)` (creates
170
+ * it, stamping `createdAt`);
171
+ * - an owned row whose mapping changes → `shell.updateActivity(id, patch)`
172
+ * (preserves `createdAt`, bumps `updatedAt`, and the shell emits a
173
+ * `transition` event when `status` changed — driving toasts);
174
+ * - a row that vanishes, or whose mapping flips to `null` →
175
+ * `shell.removeActivity(id)`.
176
+ *
177
+ * Only activities this feed created are ever touched: activities pushed to the
178
+ * shell by other sources (or a second feed) are left untouched, and `dispose`
179
+ * removes exactly this feed's set.
180
+ *
181
+ * @typeParam TData - the row DTO shape carried by {@link ActivityFeedOptions.collection}.
182
+ *
183
+ * @example
184
+ * ```svelte
185
+ * <script lang="ts">
186
+ * import { activityFeed } from '@happyvertical/smrt-svelte/web';
187
+ * import { useAdminShell } from '@happyvertical/smrt-svelte/workspace';
188
+ * import { createSmrtCollection } from '@happyvertical/smrt-web';
189
+ * import { getCollectionDefinition } from '@happyvertical/smrt-virt-web';
190
+ *
191
+ * const shell = useAdminShell();
192
+ * const encodes = createSmrtCollection(getCollectionDefinition('encodes'), {});
193
+ *
194
+ * // Reconciles `encodes` rows into the shell's Focus scope; disposes on unmount.
195
+ * activityFeed({
196
+ * collection: encodes,
197
+ * shell,
198
+ * map: (row) => ({
199
+ * kind: 'video-encode',
200
+ * scope: 'focus',
201
+ * label: row.title,
202
+ * status: row.state, // 'running' | 'completed' | …
203
+ * progress: row.progress,
204
+ * }),
205
+ * });
206
+ * </script>
207
+ * ```
208
+ */
209
+ export declare function activityFeed<TData extends object>(options: ActivityFeedOptions<TData>): ActivityFeedHandle;
210
+ //# sourceMappingURL=activity-feed.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-feed.svelte.d.ts","sourceRoot":"","sources":["../../src/web/activity-feed.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qDAAqD,CAAC;AACtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAC;AAGlF;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACnC,aAAa,EACb,IAAI,GAAG,WAAW,GAAG,WAAW,CACjC,GAAG;IACF;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,MAAM,IAAI,CAClD,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,KACnB,kBAAkB,GAAG,IAAI,CAAC;AAE/B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,MAAM;IACvD;;;;OAIG;IACH,UAAU,EAAE,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,GAAG,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;IAC5B,qEAAqE;IACrE,KAAK,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,OAAO,IAAI,IAAI,CAAC;IAChB,yCAAyC;IACzC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AA+GD;;;;;;;;;;GAUG;AACH,qBAAa,sBAAsB,CAAC,KAAK,SAAS,MAAM;IAUpD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAVtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,OAAO,CAAC,QAAQ,CAAS;gBAGN,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,eAAe,CAAC,KAAK,CAAC;IAG9C,yCAAyC;IACzC,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;;;OAKG;IACH,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI;IA+CvD;;;OAGG;IACH,OAAO,IAAI,IAAI;CAMhB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,YAAY,CAAC,KAAK,SAAS,MAAM,EAC/C,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,GAClC,kBAAkB,CAyCpB"}