svelte-pdf-view 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.
@@ -0,0 +1,229 @@
1
+ <script lang="ts">
2
+ import {
3
+ ZoomIn,
4
+ ZoomOut,
5
+ RotateCcw,
6
+ RotateCw,
7
+ Search,
8
+ ChevronLeft,
9
+ ChevronRight
10
+ } from '@lucide/svelte';
11
+ import { getPdfViewerContext } from './pdf-viewer/context.js';
12
+
13
+ const { state: viewerState, actions } = getPdfViewerContext();
14
+
15
+ let searchInput = $state('');
16
+
17
+ function handlePageChange(e: Event) {
18
+ const input = e.target as HTMLInputElement;
19
+ const pageNum = parseInt(input.value, 10);
20
+ if (pageNum >= 1 && pageNum <= viewerState.totalPages) {
21
+ actions.goToPage(pageNum);
22
+ }
23
+ }
24
+
25
+ async function handleSearch() {
26
+ if (!searchInput.trim()) {
27
+ actions.clearSearch();
28
+ return;
29
+ }
30
+ await actions.search(searchInput);
31
+ }
32
+
33
+ function handleSearchKeydown(e: KeyboardEvent) {
34
+ if (e.key === 'Enter') {
35
+ if (e.shiftKey) {
36
+ actions.searchPrevious();
37
+ } else if (viewerState.searchTotal > 0) {
38
+ actions.searchNext();
39
+ } else {
40
+ handleSearch();
41
+ }
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <div class="pdf-toolbar">
47
+ <!-- Page navigation -->
48
+ <div class="pdf-toolbar-group">
49
+ <input
50
+ type="number"
51
+ value={viewerState.currentPage}
52
+ min="1"
53
+ max={viewerState.totalPages}
54
+ onchange={handlePageChange}
55
+ aria-label="Current page"
56
+ />
57
+ <span class="page-info">/ {viewerState.totalPages}</span>
58
+ </div>
59
+
60
+ <!-- Zoom controls -->
61
+ <div class="pdf-toolbar-group">
62
+ <button onclick={() => actions.zoomOut()} aria-label="Zoom out" title="Zoom Out">
63
+ <ZoomOut size={18} />
64
+ </button>
65
+ <span class="zoom-level">{Math.round(viewerState.scale * 100)}%</span>
66
+ <button onclick={() => actions.zoomIn()} aria-label="Zoom in" title="Zoom In">
67
+ <ZoomIn size={18} />
68
+ </button>
69
+ </div>
70
+
71
+ <!-- Rotation controls -->
72
+ <div class="pdf-toolbar-group">
73
+ <button
74
+ onclick={() => actions.rotateCounterClockwise()}
75
+ aria-label="Rotate counter-clockwise"
76
+ title="Rotate Left"
77
+ >
78
+ <RotateCcw size={18} />
79
+ </button>
80
+ <button
81
+ onclick={() => actions.rotateClockwise()}
82
+ aria-label="Rotate clockwise"
83
+ title="Rotate Right"
84
+ >
85
+ <RotateCw size={18} />
86
+ </button>
87
+ </div>
88
+
89
+ <!-- Search -->
90
+ <div class="pdf-toolbar-group">
91
+ <input
92
+ type="text"
93
+ class="search-input"
94
+ placeholder="Search..."
95
+ bind:value={searchInput}
96
+ onkeydown={handleSearchKeydown}
97
+ aria-label="Search in document"
98
+ />
99
+ <button
100
+ onclick={handleSearch}
101
+ disabled={viewerState.isSearching}
102
+ aria-label="Search"
103
+ title="Search"
104
+ >
105
+ <Search size={18} />
106
+ </button>
107
+ {#if viewerState.searchTotal > 0}
108
+ <button onclick={() => actions.searchPrevious()} aria-label="Previous match" title="Previous">
109
+ <ChevronLeft size={18} />
110
+ </button>
111
+ <button onclick={() => actions.searchNext()} aria-label="Next match" title="Next">
112
+ <ChevronRight size={18} />
113
+ </button>
114
+ <span class="match-info">{viewerState.searchCurrent}/{viewerState.searchTotal}</span>
115
+ {/if}
116
+ </div>
117
+ </div>
118
+
119
+ <style>
120
+ /* Toolbar */
121
+ .pdf-toolbar {
122
+ display: flex;
123
+ justify-content: center;
124
+ align-items: center;
125
+ gap: 1rem;
126
+ padding: 0.625rem 1rem;
127
+ background-color: #ffffff;
128
+ color: #333;
129
+ flex-shrink: 0;
130
+ flex-wrap: wrap;
131
+ border-bottom: 1px solid #e0e0e0;
132
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
133
+ }
134
+
135
+ .pdf-toolbar-group {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 0.375rem;
139
+ }
140
+
141
+ .pdf-toolbar button {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ width: 32px;
146
+ height: 32px;
147
+ padding: 0;
148
+ border: 1px solid #e0e0e0;
149
+ background-color: #fafafa;
150
+ color: #555;
151
+ border-radius: 6px;
152
+ cursor: pointer;
153
+ transition: all 0.15s ease;
154
+ }
155
+
156
+ .pdf-toolbar button:hover:not(:disabled) {
157
+ background-color: #f0f0f0;
158
+ border-color: #d0d0d0;
159
+ color: #333;
160
+ }
161
+
162
+ .pdf-toolbar button:active:not(:disabled) {
163
+ background-color: #e8e8e8;
164
+ }
165
+
166
+ .pdf-toolbar button:disabled {
167
+ opacity: 0.4;
168
+ cursor: not-allowed;
169
+ }
170
+
171
+ .pdf-toolbar input[type='text'],
172
+ .pdf-toolbar input[type='number'] {
173
+ height: 28px;
174
+ padding: 0 0.5rem;
175
+ border: 1px solid #e0e0e0;
176
+ border-radius: 6px;
177
+ background-color: #fff;
178
+ color: #333;
179
+ font-size: 0.8rem;
180
+ outline: none;
181
+ transition:
182
+ border-color 0.15s,
183
+ box-shadow 0.15s;
184
+ }
185
+
186
+ .pdf-toolbar input[type='text']:focus,
187
+ .pdf-toolbar input[type='number']:focus {
188
+ border-color: #0066cc;
189
+ box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.15);
190
+ }
191
+
192
+ .pdf-toolbar input[type='number'] {
193
+ width: 40px;
194
+ text-align: center;
195
+ appearance: textfield;
196
+ -moz-appearance: textfield;
197
+ }
198
+
199
+ .pdf-toolbar input[type='number']::-webkit-outer-spin-button,
200
+ .pdf-toolbar input[type='number']::-webkit-inner-spin-button {
201
+ -webkit-appearance: none;
202
+ margin: 0;
203
+ }
204
+
205
+ .pdf-toolbar .search-input {
206
+ width: 160px;
207
+ }
208
+
209
+ .pdf-toolbar .zoom-level {
210
+ min-width: 48px;
211
+ text-align: center;
212
+ font-size: 0.8rem;
213
+ color: #666;
214
+ font-weight: 500;
215
+ }
216
+
217
+ .pdf-toolbar .page-info {
218
+ font-size: 0.8rem;
219
+ color: #888;
220
+ margin-left: 0.25rem;
221
+ }
222
+
223
+ .pdf-toolbar .match-info {
224
+ font-size: 0.75rem;
225
+ color: #888;
226
+ min-width: 60px;
227
+ margin-left: 0.25rem;
228
+ }
229
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const PdfToolbar: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PdfToolbar = ReturnType<typeof PdfToolbar>;
3
+ export default PdfToolbar;
@@ -0,0 +1,118 @@
1
+ <script lang="ts" module>
2
+ // Export compound components
3
+ export { default as Toolbar } from './PdfToolbar.svelte';
4
+ export { default as Renderer } from './PdfRenderer.svelte';
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import type { Snippet } from 'svelte';
9
+ import {
10
+ setPdfViewerContext,
11
+ type PdfViewerState,
12
+ type PdfViewerActions
13
+ } from './pdf-viewer/context.js';
14
+ import type { PdfSource } from './PdfRenderer.svelte';
15
+
16
+ interface Props {
17
+ /** PDF source - URL string, ArrayBuffer, Uint8Array, or Blob */
18
+ src: PdfSource;
19
+ /** Initial scale (default: 1.0) */
20
+ scale?: number;
21
+ /** CSS class for the container */
22
+ class?: string;
23
+ /** Children (toolbar and renderer) */
24
+ children?: Snippet;
25
+ }
26
+
27
+ let { src, scale: initialScale = 1.0, class: className = '', children }: Props = $props();
28
+
29
+ // Reactive state that will be shared via context
30
+ let state = $state<PdfViewerState>({
31
+ loading: true,
32
+ error: null,
33
+ totalPages: 0,
34
+ currentPage: 1,
35
+ scale: initialScale,
36
+ rotation: 0,
37
+ searchQuery: '',
38
+ searchCurrent: 0,
39
+ searchTotal: 0,
40
+ isSearching: false
41
+ });
42
+
43
+ // Renderer actions - will be populated when renderer mounts
44
+ let rendererActions: PdfViewerActions | null = null;
45
+
46
+ // Actions that proxy to the renderer
47
+ const actions: PdfViewerActions = {
48
+ zoomIn: () => rendererActions?.zoomIn(),
49
+ zoomOut: () => rendererActions?.zoomOut(),
50
+ setScale: (scale: number) => rendererActions?.setScale(scale),
51
+ rotateClockwise: () => rendererActions?.rotateClockwise(),
52
+ rotateCounterClockwise: () => rendererActions?.rotateCounterClockwise(),
53
+ goToPage: (page: number) => rendererActions?.goToPage(page),
54
+ search: async (query: string) => {
55
+ if (rendererActions) {
56
+ await rendererActions.search(query);
57
+ }
58
+ },
59
+ searchNext: () => rendererActions?.searchNext(),
60
+ searchPrevious: () => rendererActions?.searchPrevious(),
61
+ clearSearch: () => rendererActions?.clearSearch()
62
+ };
63
+
64
+ // Set up context
65
+ setPdfViewerContext({
66
+ state,
67
+ actions,
68
+ _registerRenderer: (renderer: PdfViewerActions) => {
69
+ rendererActions = renderer;
70
+ }
71
+ });
72
+ </script>
73
+
74
+ <div class="pdf-viewer-container {className}">
75
+ {#if state.loading}
76
+ <div class="pdf-loading">Loading PDF...</div>
77
+ {:else if state.error}
78
+ <div class="pdf-error">Error: {state.error}</div>
79
+ {/if}
80
+
81
+ {#if children}
82
+ {@render children()}
83
+ {:else}
84
+ <!-- Default layout if no children provided -->
85
+ {#await import('./PdfToolbar.svelte') then { default: Toolbar }}
86
+ <Toolbar />
87
+ {/await}
88
+ {#await import('./PdfRenderer.svelte') then { default: Renderer }}
89
+ <Renderer {src} />
90
+ {/await}
91
+ {/if}
92
+ </div>
93
+
94
+ <style>
95
+ .pdf-viewer-container {
96
+ display: flex;
97
+ flex-direction: column;
98
+ width: 100%;
99
+ height: 100%;
100
+ background-color: #f0f0f0;
101
+ overflow: hidden;
102
+ }
103
+
104
+ .pdf-loading,
105
+ .pdf-error {
106
+ position: absolute;
107
+ top: 50%;
108
+ left: 50%;
109
+ transform: translate(-50%, -50%);
110
+ color: #666;
111
+ font-size: 1rem;
112
+ z-index: 10;
113
+ }
114
+
115
+ .pdf-error {
116
+ color: #dc3545;
117
+ }
118
+ </style>
@@ -0,0 +1,17 @@
1
+ export { default as Toolbar } from './PdfToolbar.svelte';
2
+ export { default as Renderer } from './PdfRenderer.svelte';
3
+ import type { Snippet } from 'svelte';
4
+ import type { PdfSource } from './PdfRenderer.svelte';
5
+ interface Props {
6
+ /** PDF source - URL string, ArrayBuffer, Uint8Array, or Blob */
7
+ src: PdfSource;
8
+ /** Initial scale (default: 1.0) */
9
+ scale?: number;
10
+ /** CSS class for the container */
11
+ class?: string;
12
+ /** Children (toolbar and renderer) */
13
+ children?: Snippet;
14
+ }
15
+ declare const PdfViewer: import("svelte").Component<Props, {}, "">;
16
+ type PdfViewer = ReturnType<typeof PdfViewer>;
17
+ export default PdfViewer;
@@ -0,0 +1,302 @@
1
+ <script lang="ts">
2
+ import { onDestroy, onMount } from 'svelte';
3
+
4
+ const browser = typeof window !== 'undefined';
5
+ import {
6
+ ZoomIn,
7
+ ZoomOut,
8
+ RotateCcw,
9
+ RotateCw,
10
+ Search,
11
+ ChevronLeft,
12
+ ChevronRight
13
+ } from '@lucide/svelte';
14
+
15
+ interface Props {
16
+ /** URL or path to the PDF file */
17
+ src: string;
18
+ /** Initial scale (default: 1.0) */
19
+ scale?: number;
20
+ /** CSS class for the container */
21
+ class?: string;
22
+ }
23
+
24
+ let { src, scale: initialScale = 1.0, class: className = '' }: Props = $props();
25
+
26
+ let scrollContainerEl: HTMLDivElement | undefined = $state();
27
+ let mounted = $state(false);
28
+
29
+ // Viewer state
30
+ let loading = $state(true);
31
+ let error = $state<string | null>(null);
32
+ let currentScale = $state(initialScale);
33
+ let currentRotation = $state(0);
34
+ let currentPage = $state(1);
35
+ let totalPages = $state(0);
36
+
37
+ // Search state
38
+ let searchQuery = $state('');
39
+ let searchCurrent = $state(0);
40
+ let searchTotal = $state(0);
41
+ let isSearching = $state(false);
42
+
43
+ // Core instances (loaded dynamically)
44
+ let viewer: import('./pdf-viewer/PDFViewerCore.js').PDFViewerCore | null = null;
45
+ let findController: import('./pdf-viewer/FindController.js').FindController | null = null;
46
+ let pdfjsLib: typeof import('pdfjs-dist') | null = null;
47
+
48
+ async function initPdfJs() {
49
+ if (!browser) return null;
50
+
51
+ pdfjsLib = await import('pdfjs-dist');
52
+ const pdfjsWorker = await import('pdfjs-dist/build/pdf.worker.min.mjs?url');
53
+ pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker.default;
54
+
55
+ return pdfjsLib;
56
+ }
57
+
58
+ async function loadPdf(url: string) {
59
+ if (!browser || !scrollContainerEl) return;
60
+
61
+ loading = true;
62
+ error = null;
63
+
64
+ try {
65
+ // Initialize PDF.js
66
+ const pdfjs = await initPdfJs();
67
+ if (!pdfjs) return;
68
+
69
+ // Initialize viewer
70
+ const { PDFViewerCore } = await import('./pdf-viewer/PDFViewerCore.js');
71
+ const { FindController } = await import('./pdf-viewer/FindController.js');
72
+ const { EventBus } = await import('./pdf-viewer/EventBus.js');
73
+
74
+ // Cleanup existing viewer
75
+ if (viewer) {
76
+ viewer.destroy();
77
+ }
78
+
79
+ const eventBus = new EventBus();
80
+
81
+ const newViewer = new PDFViewerCore({
82
+ container: scrollContainerEl,
83
+ eventBus,
84
+ initialScale: currentScale,
85
+ initialRotation: currentRotation
86
+ });
87
+
88
+ findController = new FindController(newViewer, eventBus);
89
+
90
+ // Setup event listeners
91
+ eventBus.on('scalechanged', (data: Record<string, unknown>) => {
92
+ currentScale = data.scale as number;
93
+ });
94
+
95
+ eventBus.on('rotationchanged', (data: Record<string, unknown>) => {
96
+ currentRotation = data.rotation as number;
97
+ });
98
+
99
+ eventBus.on('updateviewarea', (data: Record<string, unknown>) => {
100
+ const location = data.location as { pageNumber: number };
101
+ currentPage = location.pageNumber;
102
+ });
103
+
104
+ eventBus.on('pagesloaded', (data: Record<string, unknown>) => {
105
+ totalPages = data.pagesCount as number;
106
+ });
107
+
108
+ eventBus.on('updatefindmatchescount', (data: Record<string, unknown>) => {
109
+ const matchesCount = data.matchesCount as { current: number; total: number };
110
+ searchCurrent = matchesCount.current;
111
+ searchTotal = matchesCount.total;
112
+ });
113
+
114
+ // Load document
115
+ const loadingTask = pdfjs.getDocument(url);
116
+ const pdfDocument = await loadingTask.promise;
117
+
118
+ await newViewer.setDocument(pdfDocument);
119
+
120
+ // Set document on find controller for text extraction
121
+ findController.setDocument(pdfDocument);
122
+
123
+ viewer = newViewer;
124
+
125
+ loading = false;
126
+ } catch (e) {
127
+ error = e instanceof Error ? e.message : 'Failed to load PDF';
128
+ loading = false;
129
+ }
130
+ }
131
+
132
+ function handleZoomIn() {
133
+ if (viewer) {
134
+ viewer.zoomIn();
135
+ }
136
+ }
137
+
138
+ function handleZoomOut() {
139
+ if (viewer) {
140
+ viewer.zoomOut();
141
+ }
142
+ }
143
+
144
+ function handleRotateRight() {
145
+ if (viewer) {
146
+ viewer.rotateClockwise();
147
+ }
148
+ }
149
+
150
+ function handleRotateLeft() {
151
+ if (viewer) {
152
+ viewer.rotateCounterClockwise();
153
+ }
154
+ }
155
+
156
+ function handlePageChange(e: Event) {
157
+ const input = e.target as HTMLInputElement;
158
+ const pageNum = parseInt(input.value, 10);
159
+ if (viewer && pageNum >= 1 && pageNum <= totalPages) {
160
+ viewer.scrollToPage(pageNum);
161
+ }
162
+ }
163
+
164
+ async function handleSearch() {
165
+ if (!findController || !searchQuery.trim()) {
166
+ searchCurrent = 0;
167
+ searchTotal = 0;
168
+ return;
169
+ }
170
+
171
+ isSearching = true;
172
+ await findController.find({
173
+ query: searchQuery,
174
+ highlightAll: true,
175
+ caseSensitive: false
176
+ });
177
+ isSearching = false;
178
+ }
179
+
180
+ function handleSearchNext() {
181
+ if (findController) {
182
+ findController.findNext();
183
+ }
184
+ }
185
+
186
+ function handleSearchPrev() {
187
+ if (findController) {
188
+ findController.findPrevious();
189
+ }
190
+ }
191
+
192
+ function handleSearchKeydown(e: KeyboardEvent) {
193
+ if (e.key === 'Enter') {
194
+ if (e.shiftKey) {
195
+ handleSearchPrev();
196
+ } else if (searchTotal > 0) {
197
+ handleSearchNext();
198
+ } else {
199
+ handleSearch();
200
+ }
201
+ }
202
+ }
203
+
204
+ // Load PDF when src changes
205
+ $effect(() => {
206
+ if (browser && src && scrollContainerEl && mounted) {
207
+ loadPdf(src);
208
+ }
209
+ });
210
+
211
+ onMount(() => {
212
+ mounted = true;
213
+ });
214
+
215
+ onDestroy(() => {
216
+ if (viewer) {
217
+ viewer.destroy();
218
+ viewer = null;
219
+ }
220
+ findController = null;
221
+ });
222
+ </script>
223
+
224
+ <div class="pdf-viewer-container {className}">
225
+ {#if loading}
226
+ <div class="pdf-loading">
227
+ <span>Loading PDF...</span>
228
+ </div>
229
+ {:else if error}
230
+ <div class="pdf-error">
231
+ <span>Error: {error}</span>
232
+ </div>
233
+ {:else}
234
+ <!-- Toolbar -->
235
+ <div class="pdf-toolbar">
236
+ <!-- Page navigation -->
237
+ <div class="pdf-toolbar-group">
238
+ <input
239
+ type="number"
240
+ value={currentPage}
241
+ min="1"
242
+ max={totalPages}
243
+ onchange={handlePageChange}
244
+ aria-label="Current page"
245
+ />
246
+ <span class="page-info">/ {totalPages}</span>
247
+ </div>
248
+
249
+ <!-- Zoom controls -->
250
+ <div class="pdf-toolbar-group">
251
+ <button onclick={handleZoomOut} aria-label="Zoom out" title="Zoom Out"
252
+ ><ZoomOut size={18} /></button
253
+ >
254
+ <span class="zoom-level">{Math.round(currentScale * 100)}%</span>
255
+ <button onclick={handleZoomIn} aria-label="Zoom in" title="Zoom In"
256
+ ><ZoomIn size={18} /></button
257
+ >
258
+ </div>
259
+
260
+ <!-- Rotation controls -->
261
+ <div class="pdf-toolbar-group">
262
+ <button
263
+ onclick={handleRotateLeft}
264
+ aria-label="Rotate counter-clockwise"
265
+ title="Rotate Left"
266
+ >
267
+ <RotateCcw size={18} />
268
+ </button>
269
+ <button onclick={handleRotateRight} aria-label="Rotate clockwise" title="Rotate Right">
270
+ <RotateCw size={18} />
271
+ </button>
272
+ </div>
273
+
274
+ <!-- Search -->
275
+ <div class="pdf-toolbar-group">
276
+ <input
277
+ type="text"
278
+ class="search-input"
279
+ placeholder="Search..."
280
+ bind:value={searchQuery}
281
+ onkeydown={handleSearchKeydown}
282
+ aria-label="Search in document"
283
+ />
284
+ <button onclick={handleSearch} disabled={isSearching} aria-label="Search" title="Search">
285
+ <Search size={18} />
286
+ </button>
287
+ {#if searchTotal > 0}
288
+ <button onclick={handleSearchPrev} aria-label="Previous match" title="Previous">
289
+ <ChevronLeft size={18} />
290
+ </button>
291
+ <button onclick={handleSearchNext} aria-label="Next match" title="Next">
292
+ <ChevronRight size={18} />
293
+ </button>
294
+ <span class="match-info">{searchCurrent}/{searchTotal}</span>
295
+ {/if}
296
+ </div>
297
+ </div>
298
+ {/if}
299
+
300
+ <!-- PDF scroll container -->
301
+ <div class="pdf-scroll-container" bind:this={scrollContainerEl}></div>
302
+ </div>
@@ -0,0 +1,11 @@
1
+ interface Props {
2
+ /** URL or path to the PDF file */
3
+ src: string;
4
+ /** Initial scale (default: 1.0) */
5
+ scale?: number;
6
+ /** CSS class for the container */
7
+ class?: string;
8
+ }
9
+ declare const PdfViewerInner: import("svelte").Component<Props, {}, "">;
10
+ type PdfViewerInner = ReturnType<typeof PdfViewerInner>;
11
+ export default PdfViewerInner;
@@ -0,0 +1,3 @@
1
+ export { default as PdfViewer, Toolbar as PdfToolbar, Renderer as PdfRenderer } from './PdfViewer.svelte';
2
+ export type { PdfSource } from './PdfRenderer.svelte';
3
+ export { getPdfViewerContext, type PdfViewerState, type PdfViewerActions, type PdfViewerContext } from './pdf-viewer/context.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Reexport your entry components here
2
+ export { default as PdfViewer, Toolbar as PdfToolbar, Renderer as PdfRenderer } from './PdfViewer.svelte';
3
+ // Export context for custom toolbars
4
+ export { getPdfViewerContext } from './pdf-viewer/context.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Simple event bus for PDF viewer component communication.
3
+ * This is a derivative work based on PDF.js event_utils.js
4
+ */
5
+ export declare class EventBus {
6
+ private listeners;
7
+ on(eventName: string, listener: EventListener): void;
8
+ off(eventName: string, listener: EventListener): void;
9
+ dispatch(eventName: string, data?: Record<string, unknown>): void;
10
+ destroy(): void;
11
+ }
12
+ export type EventListener = (data: Record<string, unknown>) => void;