@syntrologie/adapt-overlays 0.0.0-semantically-released

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,22 @@
1
+ /**
2
+ * Adaptive Overlays - Editor Module
3
+ *
4
+ * Editor panel for configuring visual overlays.
5
+ */
6
+ import type { EditorPanelProps } from './types';
7
+ /**
8
+ * Overlays editor panel component.
9
+ */
10
+ export declare function OverlaysEditor({ config: _config, onChange: _onChange, editor: _editor, }: EditorPanelProps): import("react/jsx-runtime").JSX.Element;
11
+ /**
12
+ * Editor module configuration.
13
+ */
14
+ export declare const editor: {
15
+ panel: {
16
+ title: string;
17
+ icon: string;
18
+ description: string;
19
+ };
20
+ component: typeof OverlaysEditor;
21
+ };
22
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,SAAS,EACnB,MAAM,EAAE,OAAO,GAChB,EAAE,gBAAgB,2CAOlB;AAED;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC"}
package/dist/editor.js ADDED
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Overlays editor panel component.
4
+ */
5
+ export function OverlaysEditor({ config: _config, onChange: _onChange, editor: _editor, }) {
6
+ return (_jsxs("div", { className: "syntro-overlays-editor", children: [_jsx("p", { children: "Overlays editor coming soon..." }), _jsx("p", { children: "Configure tooltips, highlights, badges, and pulse effects." })] }));
7
+ }
8
+ /**
9
+ * Editor module configuration.
10
+ */
11
+ export const editor = {
12
+ panel: {
13
+ title: 'Overlays',
14
+ icon: '💬',
15
+ description: 'Tooltips and visual highlights',
16
+ },
17
+ component: OverlaysEditor,
18
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Highlight Overlay
3
+ *
4
+ * Creates a spotlight effect around an element with a scrim backdrop.
5
+ */
6
+ export type HighlightHandle = {
7
+ destroy(): void;
8
+ };
9
+ export interface HighlightOptions {
10
+ paddingPx?: number;
11
+ radiusPx?: number;
12
+ scrimOpacity?: number;
13
+ ringColor?: string;
14
+ blocking?: boolean;
15
+ onClickOutside?: boolean;
16
+ onEsc?: boolean;
17
+ }
18
+ export declare function showHighlight(anchorEl: HTMLElement, overlayRoot: HTMLElement, opts?: HighlightOptions): HighlightHandle;
19
+ //# sourceMappingURL=highlight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,MAAM,eAAe,GAAG;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,WAAW,EACrB,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE,gBAAgB,GACtB,eAAe,CAkKjB"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Highlight Overlay
3
+ *
4
+ * Creates a spotlight effect around an element with a scrim backdrop.
5
+ */
6
+ const supportsPathClip = typeof CSS !== 'undefined' && CSS.supports?.('clip-path', "path('M0 0 H1 V1 Z')");
7
+ export function showHighlight(anchorEl, overlayRoot, opts) {
8
+ const padding = opts?.paddingPx ?? 12;
9
+ const radius = opts?.radiusPx ?? 12;
10
+ const opacity = Math.min(Math.max(opts?.scrimOpacity ?? 0.55, 0), 1);
11
+ const ringColor = opts?.ringColor ?? 'var(--syntro-ring, #5b8cff)';
12
+ const blocking = opts?.blocking ?? false;
13
+ const onClickOutside = opts?.onClickOutside ?? true;
14
+ const onEsc = opts?.onEsc ?? true;
15
+ const rootStyles = getComputedStyle(document.documentElement);
16
+ const tokenScrim = rootStyles.getPropertyValue('--syntro-spotlight-backdrop').trim();
17
+ const tokenRing = rootStyles.getPropertyValue('--syntro-ring').trim();
18
+ const scrim = document.createElement('div');
19
+ scrim.className = 'syntro-spotlight-scrim';
20
+ const needsPointerEvents = blocking || onClickOutside;
21
+ Object.assign(scrim.style, {
22
+ position: 'fixed',
23
+ inset: '0',
24
+ zIndex: '2147483646',
25
+ pointerEvents: needsPointerEvents ? 'auto' : 'none',
26
+ background: tokenScrim || `rgba(2, 6, 23, ${opacity})`,
27
+ transition: 'opacity 220ms ease',
28
+ opacity: '0',
29
+ });
30
+ overlayRoot.appendChild(scrim);
31
+ requestAnimationFrame(() => (scrim.style.opacity = '1'));
32
+ const ring = document.createElement('div');
33
+ ring.className = 'syntro-spotlight-ring';
34
+ Object.assign(ring.style, {
35
+ position: 'fixed',
36
+ pointerEvents: 'none',
37
+ borderRadius: `${radius}px`,
38
+ border: `2px solid ${ringColor || tokenRing || '#5b8cff'}`,
39
+ boxShadow: `0 0 0 4px rgba(255,255,255,0.35)`,
40
+ zIndex: '2147483647',
41
+ transition: 'all 220ms cubic-bezier(0.16,1,0.3,1)',
42
+ });
43
+ overlayRoot.appendChild(ring);
44
+ const fallbackSlices = [];
45
+ if (!supportsPathClip) {
46
+ for (let i = 0; i < 4; i++) {
47
+ const slice = document.createElement('div');
48
+ slice.style.position = 'fixed';
49
+ slice.style.background = 'inherit';
50
+ fallbackSlices.push(slice);
51
+ scrim.appendChild(slice);
52
+ }
53
+ }
54
+ const setClipPath = (path) => {
55
+ scrim.style.clipPath = path;
56
+ scrim.style.webkitClipPath = path;
57
+ };
58
+ const update = () => {
59
+ const rect = anchorEl.getBoundingClientRect();
60
+ const x = Math.max(0, rect.left - padding);
61
+ const y = Math.max(0, rect.top - padding);
62
+ const w = Math.min(window.innerWidth, rect.width + padding * 2);
63
+ const h = Math.min(window.innerHeight, rect.height + padding * 2);
64
+ Object.assign(ring.style, {
65
+ left: `${x}px`,
66
+ top: `${y}px`,
67
+ width: `${w}px`,
68
+ height: `${h}px`,
69
+ });
70
+ if (supportsPathClip) {
71
+ const vw = window.innerWidth;
72
+ const vh = window.innerHeight;
73
+ const r = Math.min(radius, w / 2, h / 2);
74
+ const outer = `M 0 0 L ${vw} 0 L ${vw} ${vh} L 0 ${vh} Z`;
75
+ const inner = `M ${x + r} ${y} ` +
76
+ `A ${r} ${r} 0 0 0 ${x} ${y + r} ` +
77
+ `L ${x} ${y + h - r} ` +
78
+ `A ${r} ${r} 0 0 0 ${x + r} ${y + h} ` +
79
+ `L ${x + w - r} ${y + h} ` +
80
+ `A ${r} ${r} 0 0 0 ${x + w} ${y + h - r} ` +
81
+ `L ${x + w} ${y + r} ` +
82
+ `A ${r} ${r} 0 0 0 ${x + w - r} ${y} ` +
83
+ `L ${x + r} ${y} ` +
84
+ `Z`;
85
+ setClipPath(`path('${outer} ${inner}')`);
86
+ }
87
+ else {
88
+ const [top, right, bottom, left] = fallbackSlices;
89
+ Object.assign(top.style, {
90
+ left: '0px',
91
+ top: '0px',
92
+ width: '100vw',
93
+ height: `${y}px`,
94
+ });
95
+ Object.assign(bottom.style, {
96
+ left: '0px',
97
+ top: `${y + h}px`,
98
+ width: '100vw',
99
+ height: `${Math.max(0, window.innerHeight - (y + h))}px`,
100
+ });
101
+ Object.assign(left.style, {
102
+ left: '0px',
103
+ top: `${y}px`,
104
+ width: `${x}px`,
105
+ height: `${h}px`,
106
+ });
107
+ Object.assign(right.style, {
108
+ left: `${x + w}px`,
109
+ top: `${y}px`,
110
+ width: `${Math.max(0, window.innerWidth - (x + w))}px`,
111
+ height: `${h}px`,
112
+ });
113
+ }
114
+ };
115
+ const ro = new ResizeObserver(() => requestAnimationFrame(update));
116
+ ro.observe(anchorEl);
117
+ const onScroll = () => requestAnimationFrame(update);
118
+ const onResize = () => requestAnimationFrame(update);
119
+ window.addEventListener('scroll', onScroll, true);
120
+ window.addEventListener('resize', onResize);
121
+ const onKey = (e) => {
122
+ if (e.key === 'Escape' && onEsc)
123
+ handle.destroy();
124
+ };
125
+ if (onEsc) {
126
+ window.addEventListener('keydown', onKey);
127
+ }
128
+ const onClick = (event) => {
129
+ if (blocking) {
130
+ event.preventDefault();
131
+ event.stopPropagation();
132
+ }
133
+ else if (onClickOutside) {
134
+ handle.destroy();
135
+ }
136
+ };
137
+ scrim.addEventListener('click', onClick);
138
+ const handle = {
139
+ destroy() {
140
+ ro.disconnect();
141
+ window.removeEventListener('scroll', onScroll, true);
142
+ window.removeEventListener('resize', onResize);
143
+ if (onEsc) {
144
+ window.removeEventListener('keydown', onKey);
145
+ }
146
+ scrim.removeEventListener('click', onClick);
147
+ scrim.style.opacity = '0';
148
+ setTimeout(() => {
149
+ scrim.remove();
150
+ ring.remove();
151
+ }, 220);
152
+ },
153
+ };
154
+ update();
155
+ return handle;
156
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Modal Executor
3
+ *
4
+ * Displays a centered modal dialog with optional CTA buttons.
5
+ */
6
+ import type { ModalAction, ActionExecutor } from './types';
7
+ /**
8
+ * Execute a modal action
9
+ */
10
+ export declare const executeModal: ActionExecutor<ModalAction>;
11
+ //# sourceMappingURL=modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["../src/modal.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAkB,cAAc,EAAE,MAAM,SAAS,CAAC;AAG3E;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,WAAW,CAoNpD,CAAC"}
package/dist/modal.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Modal Executor
3
+ *
4
+ * Displays a centered modal dialog with optional CTA buttons.
5
+ */
6
+ import { sanitizeHtml } from './sanitizer';
7
+ /**
8
+ * Execute a modal action
9
+ */
10
+ export const executeModal = async (action, context) => {
11
+ const { content, size = 'md', blocking = false, scrim, dismiss, ctaButtons } = action;
12
+ // Create scrim backdrop
13
+ const scrimEl = document.createElement('div');
14
+ scrimEl.className = 'syntro-modal-scrim';
15
+ scrimEl.style.cssText = `
16
+ position: fixed;
17
+ inset: 0;
18
+ background: rgba(0, 0, 0, ${scrim?.opacity ?? 0.6});
19
+ z-index: 2147483645;
20
+ opacity: 0;
21
+ transition: opacity 200ms ease-out;
22
+ `;
23
+ context.overlayRoot.appendChild(scrimEl);
24
+ // Create modal container
25
+ const modal = document.createElement('div');
26
+ modal.className = `syntro-modal syntro-modal-${size}`;
27
+ modal.setAttribute('role', 'dialog');
28
+ modal.setAttribute('aria-modal', 'true');
29
+ // Size-based max-widths
30
+ const sizeMap = { sm: '360px', md: '480px', lg: '640px' };
31
+ modal.style.cssText = `
32
+ position: fixed;
33
+ top: 50%;
34
+ left: 50%;
35
+ transform: translate(-50%, -50%) scale(0.95);
36
+ max-width: ${sizeMap[size]};
37
+ width: 90%;
38
+ background: white;
39
+ border-radius: 12px;
40
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
41
+ z-index: 2147483646;
42
+ opacity: 0;
43
+ transition: opacity 200ms ease-out, transform 200ms ease-out;
44
+ padding: 24px;
45
+ `;
46
+ // Build modal HTML content
47
+ let html = '';
48
+ if (content.title) {
49
+ html += `<h2 class="syntro-modal-title" style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: #111827;">${sanitizeHtml(content.title)}</h2>`;
50
+ }
51
+ html += `<div class="syntro-modal-body" style="color: #4b5563; line-height: 1.5;">${sanitizeHtml(content.body)}</div>`;
52
+ // Add close button if enabled
53
+ if (dismiss?.closeButton !== false) {
54
+ html += `
55
+ <button class="syntro-modal-close" data-syntro-action="dismiss" style="
56
+ position: absolute;
57
+ top: 16px;
58
+ right: 16px;
59
+ background: none;
60
+ border: none;
61
+ cursor: pointer;
62
+ padding: 4px;
63
+ color: #6b7280;
64
+ " aria-label="Close">
65
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
66
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
67
+ </svg>
68
+ </button>
69
+ `;
70
+ }
71
+ // Add CTA buttons
72
+ if (ctaButtons && ctaButtons.length > 0) {
73
+ html += `<div class="syntro-modal-actions" style="display: flex; gap: 12px; margin-top: 24px; justify-content: flex-end;">`;
74
+ for (const btn of ctaButtons) {
75
+ const isPrimary = btn.primary ?? false;
76
+ html += `
77
+ <button
78
+ class="syntro-modal-btn ${isPrimary ? 'syntro-modal-btn-primary' : ''}"
79
+ data-syntro-action="${sanitizeHtml(btn.actionId)}"
80
+ style="
81
+ padding: 10px 20px;
82
+ border-radius: 8px;
83
+ font-size: 14px;
84
+ font-weight: 500;
85
+ cursor: pointer;
86
+ transition: background 150ms ease;
87
+ ${isPrimary
88
+ ? 'background: #4f46e5; color: white; border: none;'
89
+ : 'background: white; color: #374151; border: 1px solid #d1d5db;'}
90
+ "
91
+ >
92
+ ${sanitizeHtml(btn.label)}
93
+ </button>
94
+ `;
95
+ }
96
+ html += `</div>`;
97
+ }
98
+ modal.innerHTML = html;
99
+ context.overlayRoot.appendChild(modal);
100
+ // Track state for waitFor
101
+ let actionClicked = null;
102
+ // Handle action button clicks
103
+ const actionBtns = modal.querySelectorAll('[data-syntro-action]');
104
+ const actionHandler = (e) => {
105
+ const btn = e.currentTarget;
106
+ const actionId = btn.getAttribute('data-syntro-action');
107
+ if (actionId) {
108
+ actionClicked = actionId;
109
+ context.publishEvent('action.modal_cta_clicked', {
110
+ actionId,
111
+ });
112
+ if (actionId === 'dismiss') {
113
+ handle.destroy();
114
+ }
115
+ }
116
+ };
117
+ actionBtns.forEach((btn) => btn.addEventListener('click', actionHandler));
118
+ // Handle escape key
119
+ const onKey = (e) => {
120
+ if (e.key === 'Escape' && dismiss?.onEsc !== false) {
121
+ handle.destroy();
122
+ }
123
+ };
124
+ window.addEventListener('keydown', onKey);
125
+ // Handle click outside (on scrim)
126
+ const onScrimClick = () => {
127
+ if (!blocking) {
128
+ handle.destroy();
129
+ }
130
+ };
131
+ scrimEl.addEventListener('click', onScrimClick);
132
+ // Blocking mode - make siblings inert
133
+ const originalInert = [];
134
+ if (blocking) {
135
+ Array.from(document.body.children).forEach((el) => {
136
+ if (el !== context.overlayRoot && el.getAttribute('inert') === null) {
137
+ el.setAttribute('inert', '');
138
+ originalInert.push(el);
139
+ }
140
+ });
141
+ }
142
+ // Set up timeout if specified
143
+ let timeoutId;
144
+ if (dismiss?.timeoutMs) {
145
+ timeoutId = setTimeout(() => {
146
+ handle.destroy();
147
+ }, dismiss.timeoutMs);
148
+ }
149
+ // Focus first focusable element
150
+ const focusableEls = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
151
+ if (focusableEls.length > 0) {
152
+ requestAnimationFrame(() => focusableEls[0].focus());
153
+ }
154
+ // Fade in
155
+ requestAnimationFrame(() => {
156
+ scrimEl.style.opacity = '1';
157
+ modal.style.opacity = '1';
158
+ modal.style.transform = 'translate(-50%, -50%) scale(1)';
159
+ });
160
+ context.publishEvent('action.applied', {
161
+ id: context.generateId(),
162
+ kind: 'overlays:modal',
163
+ size,
164
+ blocking,
165
+ });
166
+ const handle = {
167
+ destroy() {
168
+ if (timeoutId) {
169
+ clearTimeout(timeoutId);
170
+ }
171
+ window.removeEventListener('keydown', onKey);
172
+ scrimEl.removeEventListener('click', onScrimClick);
173
+ actionBtns.forEach((btn) => btn.removeEventListener('click', actionHandler));
174
+ // Restore inert state
175
+ originalInert.forEach((el) => el.removeAttribute('inert'));
176
+ // Fade out then remove
177
+ modal.style.opacity = '0';
178
+ modal.style.transform = 'translate(-50%, -50%) scale(0.95)';
179
+ scrimEl.style.opacity = '0';
180
+ setTimeout(() => {
181
+ modal.remove();
182
+ scrimEl.remove();
183
+ }, 200);
184
+ context.publishEvent('action.modal_dismissed', {
185
+ actionClicked,
186
+ });
187
+ },
188
+ };
189
+ return {
190
+ cleanup: () => {
191
+ handle.destroy();
192
+ },
193
+ };
194
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Adaptive Overlays - Runtime Module
3
+ *
4
+ * Visual overlay actions: highlight, pulse, badge, tooltip.
5
+ */
6
+ import type { HighlightAction, PulseAction, BadgeAction, TooltipAction, ActionExecutor } from './types';
7
+ import { executeModal } from './modal';
8
+ export { executeModal };
9
+ /**
10
+ * Execute a highlight action
11
+ */
12
+ export declare const executeHighlight: ActionExecutor<HighlightAction>;
13
+ /**
14
+ * Execute a pulse action
15
+ */
16
+ export declare const executePulse: ActionExecutor<PulseAction>;
17
+ /**
18
+ * Execute a badge action
19
+ */
20
+ export declare const executeBadge: ActionExecutor<BadgeAction>;
21
+ /**
22
+ * Execute a tooltip action
23
+ */
24
+ export declare const executeTooltip: ActionExecutor<TooltipAction>;
25
+ /**
26
+ * All executors provided by this app.
27
+ * These are registered with the runtime's ExecutorRegistry.
28
+ */
29
+ export declare const executors: readonly [{
30
+ readonly kind: "overlays:highlight";
31
+ readonly executor: ActionExecutor<HighlightAction>;
32
+ }, {
33
+ readonly kind: "overlays:pulse";
34
+ readonly executor: ActionExecutor<PulseAction>;
35
+ }, {
36
+ readonly kind: "overlays:badge";
37
+ readonly executor: ActionExecutor<BadgeAction>;
38
+ }, {
39
+ readonly kind: "overlays:tooltip";
40
+ readonly executor: ActionExecutor<TooltipAction>;
41
+ }, {
42
+ readonly kind: "overlays:modal";
43
+ readonly executor: ActionExecutor<import("./types").ModalAction>;
44
+ }];
45
+ /**
46
+ * App runtime manifest.
47
+ */
48
+ export declare const runtime: {
49
+ id: string;
50
+ version: string;
51
+ name: string;
52
+ description: string;
53
+ executors: readonly [{
54
+ readonly kind: "overlays:highlight";
55
+ readonly executor: ActionExecutor<HighlightAction>;
56
+ }, {
57
+ readonly kind: "overlays:pulse";
58
+ readonly executor: ActionExecutor<PulseAction>;
59
+ }, {
60
+ readonly kind: "overlays:badge";
61
+ readonly executor: ActionExecutor<BadgeAction>;
62
+ }, {
63
+ readonly kind: "overlays:tooltip";
64
+ readonly executor: ActionExecutor<TooltipAction>;
65
+ }, {
66
+ readonly kind: "overlays:modal";
67
+ readonly executor: ActionExecutor<import("./types").ModalAction>;
68
+ }];
69
+ };
70
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,WAAW,EACX,aAAa,EAEb,cAAc,EACf,MAAM,SAAS,CAAC;AAIjB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,EAAE,YAAY,EAAE,CAAC;AAMxB;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,cAAc,CAAC,eAAe,CA8B5D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,WAAW,CAqDpD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,WAAW,CAsFpD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CA4ExD,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;EAMZ,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;CAMnB,CAAC"}