@sc4rfurryx/proteusjs 1.0.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.
Files changed (82) hide show
  1. package/API.md +438 -0
  2. package/FEATURES.md +286 -0
  3. package/LICENSE +21 -0
  4. package/README.md +645 -0
  5. package/dist/.tsbuildinfo +1 -0
  6. package/dist/proteus.cjs.js +16014 -0
  7. package/dist/proteus.cjs.js.map +1 -0
  8. package/dist/proteus.d.ts +3018 -0
  9. package/dist/proteus.esm.js +16005 -0
  10. package/dist/proteus.esm.js.map +1 -0
  11. package/dist/proteus.esm.min.js +8 -0
  12. package/dist/proteus.esm.min.js.map +1 -0
  13. package/dist/proteus.js +16020 -0
  14. package/dist/proteus.js.map +1 -0
  15. package/dist/proteus.min.js +8 -0
  16. package/dist/proteus.min.js.map +1 -0
  17. package/package.json +98 -0
  18. package/src/__tests__/mvp-integration.test.ts +518 -0
  19. package/src/accessibility/AccessibilityEngine.ts +2106 -0
  20. package/src/accessibility/ScreenReaderSupport.ts +444 -0
  21. package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
  22. package/src/animations/FLIPAnimationSystem.ts +491 -0
  23. package/src/compatibility/BrowserCompatibility.ts +1076 -0
  24. package/src/containers/BreakpointSystem.ts +347 -0
  25. package/src/containers/ContainerBreakpoints.ts +726 -0
  26. package/src/containers/ContainerManager.ts +370 -0
  27. package/src/containers/ContainerUnits.ts +336 -0
  28. package/src/containers/ContextIsolation.ts +394 -0
  29. package/src/containers/ElementQueries.ts +411 -0
  30. package/src/containers/SmartContainer.ts +536 -0
  31. package/src/containers/SmartContainers.ts +376 -0
  32. package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
  33. package/src/containers/__tests__/SmartContainers.test.ts +281 -0
  34. package/src/content/ResponsiveImages.ts +570 -0
  35. package/src/core/EventSystem.ts +147 -0
  36. package/src/core/MemoryManager.ts +321 -0
  37. package/src/core/PerformanceMonitor.ts +238 -0
  38. package/src/core/PluginSystem.ts +275 -0
  39. package/src/core/ProteusJS.test.ts +164 -0
  40. package/src/core/ProteusJS.ts +962 -0
  41. package/src/developer/PerformanceProfiler.ts +567 -0
  42. package/src/developer/VisualDebuggingTools.ts +656 -0
  43. package/src/developer/ZeroConfigSystem.ts +593 -0
  44. package/src/index.ts +35 -0
  45. package/src/integration.test.ts +227 -0
  46. package/src/layout/AdaptiveGrid.ts +429 -0
  47. package/src/layout/ContentReordering.ts +532 -0
  48. package/src/layout/FlexboxEnhancer.ts +406 -0
  49. package/src/layout/FlowLayout.ts +545 -0
  50. package/src/layout/SpacingSystem.ts +512 -0
  51. package/src/observers/IntersectionObserverPolyfill.ts +289 -0
  52. package/src/observers/ObserverManager.ts +299 -0
  53. package/src/observers/ResizeObserverPolyfill.ts +179 -0
  54. package/src/performance/BatchDOMOperations.ts +519 -0
  55. package/src/performance/CSSOptimizationEngine.ts +646 -0
  56. package/src/performance/CacheOptimizationSystem.ts +601 -0
  57. package/src/performance/EfficientEventHandler.ts +740 -0
  58. package/src/performance/LazyEvaluationSystem.ts +532 -0
  59. package/src/performance/MemoryManagementSystem.ts +497 -0
  60. package/src/performance/PerformanceMonitor.ts +931 -0
  61. package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
  62. package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
  63. package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
  64. package/src/polyfills/BrowserPolyfills.ts +586 -0
  65. package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
  66. package/src/test/setup.ts +115 -0
  67. package/src/theming/SmartThemeSystem.ts +591 -0
  68. package/src/types/index.ts +134 -0
  69. package/src/typography/ClampScaling.ts +356 -0
  70. package/src/typography/FluidTypography.ts +759 -0
  71. package/src/typography/LineHeightOptimization.ts +430 -0
  72. package/src/typography/LineHeightOptimizer.ts +326 -0
  73. package/src/typography/TextFitting.ts +355 -0
  74. package/src/typography/TypographicScale.ts +428 -0
  75. package/src/typography/VerticalRhythm.ts +369 -0
  76. package/src/typography/__tests__/FluidTypography.test.ts +432 -0
  77. package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
  78. package/src/utils/Logger.ts +173 -0
  79. package/src/utils/debounce.ts +259 -0
  80. package/src/utils/performance.ts +371 -0
  81. package/src/utils/support.ts +106 -0
  82. package/src/utils/version.ts +24 -0
@@ -0,0 +1,570 @@
1
+ /**
2
+ * Responsive Images System for ProteusJS
3
+ * Next-generation image optimization with container-based sizing
4
+ */
5
+
6
+ export interface ImageConfig {
7
+ formats: ('webp' | 'avif' | 'jpeg' | 'png')[];
8
+ sizes: number[];
9
+ quality: number;
10
+ lazyLoading: boolean;
11
+ artDirection: boolean;
12
+ containerBased: boolean;
13
+ placeholder: 'blur' | 'color' | 'svg' | 'none';
14
+ placeholderColor?: string;
15
+ fadeInDuration: number;
16
+ retina: boolean;
17
+ progressive: boolean;
18
+ }
19
+
20
+ export interface ImageSource {
21
+ src: string;
22
+ format: string;
23
+ width: number;
24
+ height: number;
25
+ quality: number;
26
+ media?: string;
27
+ }
28
+
29
+ export interface ImageState {
30
+ loaded: boolean;
31
+ loading: boolean;
32
+ error: boolean;
33
+ currentSrc: string;
34
+ containerSize: { width: number; height: number };
35
+ optimalSource: ImageSource | null;
36
+ intersecting: boolean;
37
+ }
38
+
39
+ export class ResponsiveImages {
40
+ private element: Element;
41
+ private config: Required<ImageConfig>;
42
+ private state: ImageState;
43
+ private resizeObserver: ResizeObserver | null = null;
44
+ private intersectionObserver: IntersectionObserver | null = null;
45
+ private sources: ImageSource[] = [];
46
+
47
+ private static readonly FORMAT_SUPPORT = new Map<string, boolean>();
48
+ private static readonly MIME_TYPES = {
49
+ 'webp': 'image/webp',
50
+ 'avif': 'image/avif',
51
+ 'jpeg': 'image/jpeg',
52
+ 'png': 'image/png'
53
+ };
54
+
55
+ constructor(element: Element, config: Partial<ImageConfig> = {}) {
56
+ this.element = element;
57
+ this.config = {
58
+ formats: ['avif', 'webp', 'jpeg'],
59
+ sizes: [320, 640, 768, 1024, 1280, 1920],
60
+ quality: 80,
61
+ lazyLoading: true,
62
+ artDirection: false,
63
+ containerBased: true,
64
+ placeholder: 'blur',
65
+ placeholderColor: '#f0f0f0',
66
+ fadeInDuration: 300,
67
+ retina: true,
68
+ progressive: true,
69
+ ...config
70
+ };
71
+
72
+ this.state = this.createInitialState();
73
+ this.setupImage();
74
+ }
75
+
76
+ /**
77
+ * Activate responsive image system
78
+ */
79
+ public activate(): void {
80
+ this.detectFormatSupport();
81
+ this.generateSources();
82
+ this.setupObservers();
83
+
84
+ if (!this.config.lazyLoading) {
85
+ this.loadImage();
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Deactivate and clean up
91
+ */
92
+ public deactivate(): void {
93
+ this.cleanupObservers();
94
+ this.removeImageFeatures();
95
+ }
96
+
97
+ /**
98
+ * Update image configuration
99
+ */
100
+ public updateConfig(newConfig: Partial<ImageConfig>): void {
101
+ this.config = { ...this.config, ...newConfig };
102
+ this.generateSources();
103
+ this.updateImage();
104
+ }
105
+
106
+ /**
107
+ * Get current image state
108
+ */
109
+ public getState(): ImageState {
110
+ return { ...this.state };
111
+ }
112
+
113
+ /**
114
+ * Force image load
115
+ */
116
+ public load(): void {
117
+ this.loadImage();
118
+ }
119
+
120
+ /**
121
+ * Preload image
122
+ */
123
+ public preload(): Promise<void> {
124
+ return new Promise((resolve, reject) => {
125
+ const optimalSource = this.getOptimalSource();
126
+ if (!optimalSource) {
127
+ reject(new Error('No optimal source found'));
128
+ return;
129
+ }
130
+
131
+ const img = new Image();
132
+ img.onload = () => resolve();
133
+ img.onerror = () => reject(new Error('Failed to preload image'));
134
+ img.src = optimalSource.src;
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Setup initial image
140
+ */
141
+ private setupImage(): void {
142
+ this.addPlaceholder();
143
+ this.setupImageElement();
144
+ }
145
+
146
+ /**
147
+ * Detect browser format support
148
+ */
149
+ private async detectFormatSupport(): Promise<void> {
150
+ const formats = ['webp', 'avif'];
151
+
152
+ for (const format of formats) {
153
+ if (!ResponsiveImages.FORMAT_SUPPORT.has(format)) {
154
+ const supported = await this.testFormatSupport(format);
155
+ ResponsiveImages.FORMAT_SUPPORT.set(format, supported);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Test if browser supports image format
162
+ */
163
+ private testFormatSupport(format: string): Promise<boolean> {
164
+ return new Promise((resolve) => {
165
+ const img = new Image();
166
+ img.onload = () => resolve(img.width > 0 && img.height > 0);
167
+ img.onerror = () => resolve(false);
168
+
169
+ // Test images (1x1 pixel)
170
+ const testImages = {
171
+ 'webp': 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA',
172
+ 'avif': 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgABogQEAwgMg8f8D///8WfhwB8+ErK42A='
173
+ };
174
+
175
+ img.src = testImages[format as keyof typeof testImages] || '';
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Generate image sources
181
+ */
182
+ private generateSources(): void {
183
+ const baseSrc = this.getBaseSrc();
184
+ if (!baseSrc) return;
185
+
186
+ this.sources = [];
187
+
188
+ // Generate sources for each format and size
189
+ this.config.formats.forEach(format => {
190
+ if (this.isFormatSupported(format)) {
191
+ this.config.sizes.forEach(width => {
192
+ const source: ImageSource = {
193
+ src: this.generateSrcUrl(baseSrc, format, width),
194
+ format,
195
+ width,
196
+ height: this.calculateHeight(width),
197
+ quality: this.config.quality
198
+ };
199
+
200
+ if (this.config.containerBased) {
201
+ source.media = `(min-width: ${width}px)`;
202
+ }
203
+
204
+ this.sources.push(source);
205
+
206
+ // Add retina version
207
+ if (this.config.retina) {
208
+ const retinaSource = {
209
+ ...source,
210
+ src: this.generateSrcUrl(baseSrc, format, width * 2),
211
+ width: width * 2,
212
+ height: this.calculateHeight(width * 2)
213
+ };
214
+ if (source.media) {
215
+ retinaSource.media = `${source.media} and (-webkit-min-device-pixel-ratio: 2)`;
216
+ }
217
+ this.sources.push(retinaSource);
218
+ }
219
+ });
220
+ }
221
+ });
222
+
223
+ // Sort by format preference and size
224
+ this.sources.sort((a, b) => {
225
+ const formatPriorityA = this.config.formats.indexOf(a.format as any);
226
+ const formatPriorityB = this.config.formats.indexOf(b.format as any);
227
+
228
+ if (formatPriorityA !== formatPriorityB) {
229
+ return formatPriorityA - formatPriorityB;
230
+ }
231
+
232
+ return a.width - b.width;
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Get optimal image source
238
+ */
239
+ private getOptimalSource(): ImageSource | null {
240
+ if (this.sources.length === 0) return null;
241
+
242
+ const containerWidth = this.state.containerSize.width;
243
+ const devicePixelRatio = window.devicePixelRatio || 1;
244
+ const targetWidth = containerWidth * devicePixelRatio;
245
+
246
+ // Find best size match
247
+ const suitableSources = this.sources.filter(source =>
248
+ source.width >= targetWidth || source === this.sources[this.sources.length - 1]
249
+ );
250
+
251
+ // Return smallest suitable source
252
+ return suitableSources[0] || this.sources[this.sources.length - 1] || null;
253
+ }
254
+
255
+ /**
256
+ * Load image
257
+ */
258
+ private loadImage(): void {
259
+ if (this.state.loading || this.state.loaded) return;
260
+
261
+ this.state.loading = true;
262
+ const optimalSource = this.getOptimalSource();
263
+
264
+ if (!optimalSource) {
265
+ this.state.loading = false;
266
+ this.state.error = true;
267
+ return;
268
+ }
269
+
270
+ const img = new Image();
271
+
272
+ img.onload = () => {
273
+ this.state.loaded = true;
274
+ this.state.loading = false;
275
+ this.state.currentSrc = optimalSource.src;
276
+ this.state.optimalSource = optimalSource;
277
+
278
+ this.applyImage(img);
279
+ this.removePlaceholder();
280
+ };
281
+
282
+ img.onerror = () => {
283
+ this.state.loading = false;
284
+ this.state.error = true;
285
+ this.handleImageError();
286
+ };
287
+
288
+ img.src = optimalSource.src;
289
+ }
290
+
291
+ /**
292
+ * Apply loaded image
293
+ */
294
+ private applyImage(img: HTMLImageElement): void {
295
+ if (this.element.tagName === 'IMG') {
296
+ const imgElement = this.element as HTMLImageElement;
297
+ imgElement.src = img.src;
298
+ imgElement.style.opacity = '0';
299
+
300
+ // Fade in animation
301
+ requestAnimationFrame(() => {
302
+ imgElement.style.transition = `opacity ${this.config.fadeInDuration}ms ease-in-out`;
303
+ imgElement.style.opacity = '1';
304
+ });
305
+ } else {
306
+ // Apply as background image
307
+ const htmlElement = this.element as HTMLElement;
308
+ htmlElement.style.backgroundImage = `url(${img.src})`;
309
+ htmlElement.style.backgroundSize = 'cover';
310
+ htmlElement.style.backgroundPosition = 'center';
311
+ htmlElement.style.opacity = '0';
312
+
313
+ requestAnimationFrame(() => {
314
+ htmlElement.style.transition = `opacity ${this.config.fadeInDuration}ms ease-in-out`;
315
+ htmlElement.style.opacity = '1';
316
+ });
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Add placeholder
322
+ */
323
+ private addPlaceholder(): void {
324
+ if (this.config.placeholder === 'none') return;
325
+
326
+ const htmlElement = this.element as HTMLElement;
327
+
328
+ switch (this.config.placeholder) {
329
+ case 'color':
330
+ htmlElement.style.backgroundColor = this.config.placeholderColor || '#f0f0f0';
331
+ break;
332
+ case 'blur':
333
+ this.addBlurPlaceholder();
334
+ break;
335
+ case 'svg':
336
+ this.addSvgPlaceholder();
337
+ break;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Add blur placeholder
343
+ */
344
+ private addBlurPlaceholder(): void {
345
+ const canvas = document.createElement('canvas');
346
+ canvas.width = 40;
347
+ canvas.height = 30;
348
+
349
+ const ctx = canvas.getContext('2d');
350
+ if (ctx) {
351
+ const gradient = ctx.createLinearGradient(0, 0, 40, 30);
352
+ gradient.addColorStop(0, '#f0f0f0');
353
+ gradient.addColorStop(1, '#e0e0e0');
354
+
355
+ ctx.fillStyle = gradient;
356
+ ctx.fillRect(0, 0, 40, 30);
357
+ }
358
+
359
+ const blurDataUrl = canvas.toDataURL();
360
+ const htmlElement = this.element as HTMLElement;
361
+
362
+ if (this.element.tagName === 'IMG') {
363
+ (this.element as HTMLImageElement).src = blurDataUrl;
364
+ } else {
365
+ htmlElement.style.backgroundImage = `url(${blurDataUrl})`;
366
+ htmlElement.style.filter = 'blur(5px)';
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Add SVG placeholder
372
+ */
373
+ private addSvgPlaceholder(): void {
374
+ const svg = `data:image/svg+xml;base64,${btoa(`
375
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300">
376
+ <rect width="400" height="300" fill="${this.config.placeholderColor}"/>
377
+ <circle cx="200" cy="150" r="50" fill="#ddd"/>
378
+ </svg>
379
+ `)}`;
380
+
381
+ if (this.element.tagName === 'IMG') {
382
+ (this.element as HTMLImageElement).src = svg;
383
+ } else {
384
+ (this.element as HTMLElement).style.backgroundImage = `url(${svg})`;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Remove placeholder
390
+ */
391
+ private removePlaceholder(): void {
392
+ const htmlElement = this.element as HTMLElement;
393
+
394
+ if (this.config.placeholder === 'blur') {
395
+ htmlElement.style.filter = '';
396
+ }
397
+
398
+ if (this.config.placeholder === 'color') {
399
+ htmlElement.style.backgroundColor = '';
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Handle image loading error
405
+ */
406
+ private handleImageError(): void {
407
+ console.warn('ProteusJS: Failed to load image', this.element);
408
+
409
+ // Try fallback format
410
+ const fallbackSource = this.sources.find(source => source.format === 'jpeg');
411
+ if (fallbackSource && fallbackSource !== this.state.optimalSource) {
412
+ this.state.error = false;
413
+ this.state.loading = true;
414
+
415
+ const img = new Image();
416
+ img.onload = () => {
417
+ this.state.loaded = true;
418
+ this.state.loading = false;
419
+ this.applyImage(img);
420
+ };
421
+ img.src = fallbackSource.src;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Setup image element
427
+ */
428
+ private setupImageElement(): void {
429
+ if (this.element.tagName === 'IMG') {
430
+ const imgElement = this.element as HTMLImageElement;
431
+ imgElement.loading = this.config.lazyLoading ? 'lazy' : 'eager';
432
+ imgElement.decoding = 'async';
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Update image based on container size
438
+ */
439
+ private updateImage(): void {
440
+ const newOptimalSource = this.getOptimalSource();
441
+
442
+ if (newOptimalSource && newOptimalSource !== this.state.optimalSource) {
443
+ this.state.loaded = false;
444
+ this.loadImage();
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Get base source URL
450
+ */
451
+ private getBaseSrc(): string | null {
452
+ if (this.element.tagName === 'IMG') {
453
+ return (this.element as HTMLImageElement).src ||
454
+ (this.element as HTMLImageElement).dataset['src'] || null;
455
+ }
456
+
457
+ return (this.element as HTMLElement).dataset['src'] || null;
458
+ }
459
+
460
+ /**
461
+ * Generate source URL with format and size
462
+ */
463
+ private generateSrcUrl(baseSrc: string, format: string, width: number): string {
464
+ // This would typically integrate with an image CDN or processing service
465
+ // For now, return the base URL with query parameters
466
+ const url = new URL(baseSrc, window.location.origin);
467
+ url.searchParams.set('format', format);
468
+ url.searchParams.set('width', width.toString());
469
+ url.searchParams.set('quality', this.config.quality.toString());
470
+
471
+ if (this.config.progressive) {
472
+ url.searchParams.set('progressive', 'true');
473
+ }
474
+
475
+ return url.toString();
476
+ }
477
+
478
+ /**
479
+ * Calculate height based on width (maintaining aspect ratio)
480
+ */
481
+ private calculateHeight(width: number): number {
482
+ // Default 16:9 aspect ratio, could be made configurable
483
+ return Math.round(width * (9 / 16));
484
+ }
485
+
486
+ /**
487
+ * Check if format is supported
488
+ */
489
+ private isFormatSupported(format: string): boolean {
490
+ if (format === 'jpeg' || format === 'png') return true;
491
+ return ResponsiveImages.FORMAT_SUPPORT.get(format) || false;
492
+ }
493
+
494
+ /**
495
+ * Setup observers
496
+ */
497
+ private setupObservers(): void {
498
+ // Container size observer
499
+ this.resizeObserver = new ResizeObserver((entries) => {
500
+ for (const entry of entries) {
501
+ this.state.containerSize = {
502
+ width: entry.contentRect.width,
503
+ height: entry.contentRect.height
504
+ };
505
+
506
+ if (this.config.containerBased) {
507
+ this.updateImage();
508
+ }
509
+ }
510
+ });
511
+ this.resizeObserver.observe(this.element);
512
+
513
+ // Lazy loading observer
514
+ if (this.config.lazyLoading) {
515
+ this.intersectionObserver = new IntersectionObserver((entries) => {
516
+ for (const entry of entries) {
517
+ if (entry.isIntersecting) {
518
+ this.state.intersecting = true;
519
+ this.loadImage();
520
+ this.intersectionObserver?.unobserve(this.element);
521
+ }
522
+ }
523
+ }, { rootMargin: '50px' });
524
+
525
+ this.intersectionObserver.observe(this.element);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Clean up observers
531
+ */
532
+ private cleanupObservers(): void {
533
+ if (this.resizeObserver) {
534
+ this.resizeObserver.disconnect();
535
+ this.resizeObserver = null;
536
+ }
537
+
538
+ if (this.intersectionObserver) {
539
+ this.intersectionObserver.disconnect();
540
+ this.intersectionObserver = null;
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Remove image features
546
+ */
547
+ private removeImageFeatures(): void {
548
+ this.removePlaceholder();
549
+
550
+ const htmlElement = this.element as HTMLElement;
551
+ htmlElement.style.transition = '';
552
+ htmlElement.style.opacity = '';
553
+ htmlElement.style.filter = '';
554
+ }
555
+
556
+ /**
557
+ * Create initial state
558
+ */
559
+ private createInitialState(): ImageState {
560
+ return {
561
+ loaded: false,
562
+ loading: false,
563
+ error: false,
564
+ currentSrc: '',
565
+ containerSize: { width: 0, height: 0 },
566
+ optimalSource: null,
567
+ intersecting: false
568
+ };
569
+ }
570
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Event System for ProteusJS
3
+ * Handles all internal and external events
4
+ */
5
+
6
+ import type { ProteusEvent } from '../types';
7
+
8
+ export type EventCallback = (event: ProteusEvent) => void;
9
+
10
+ export class EventSystem {
11
+ private listeners: Map<string, Set<EventCallback>> = new Map();
12
+ private initialized: boolean = false;
13
+
14
+ /**
15
+ * Initialize the event system
16
+ */
17
+ public init(): void {
18
+ if (this.initialized) return;
19
+ this.initialized = true;
20
+ }
21
+
22
+ /**
23
+ * Add event listener
24
+ */
25
+ public on(eventType: string, callback: EventCallback): () => void {
26
+ if (!this.listeners.has(eventType)) {
27
+ this.listeners.set(eventType, new Set());
28
+ }
29
+
30
+ const callbacks = this.listeners.get(eventType)!;
31
+ callbacks.add(callback);
32
+
33
+ // Return unsubscribe function
34
+ return () => {
35
+ callbacks.delete(callback);
36
+ if (callbacks.size === 0) {
37
+ this.listeners.delete(eventType);
38
+ }
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Add one-time event listener
44
+ */
45
+ public once(eventType: string, callback: EventCallback): () => void {
46
+ const unsubscribe = this.on(eventType, (event) => {
47
+ callback(event);
48
+ unsubscribe();
49
+ });
50
+ return unsubscribe;
51
+ }
52
+
53
+ /**
54
+ * Remove event listener
55
+ */
56
+ public off(eventType: string, callback?: EventCallback): void {
57
+ if (!callback) {
58
+ // Remove all listeners for this event type
59
+ this.listeners.delete(eventType);
60
+ return;
61
+ }
62
+
63
+ const callbacks = this.listeners.get(eventType);
64
+ if (callbacks) {
65
+ callbacks.delete(callback);
66
+ if (callbacks.size === 0) {
67
+ this.listeners.delete(eventType);
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Emit event
74
+ */
75
+ public emit(eventType: string, detail?: any, target?: Element): void {
76
+ const callbacks = this.listeners.get(eventType);
77
+ if (!callbacks || callbacks.size === 0) return;
78
+
79
+ const event: ProteusEvent = {
80
+ type: eventType,
81
+ target: target || document.documentElement,
82
+ detail,
83
+ timestamp: Date.now()
84
+ };
85
+
86
+ // Execute callbacks
87
+ callbacks.forEach(callback => {
88
+ try {
89
+ callback(event);
90
+ } catch (error) {
91
+ console.error(`ProteusJS: Error in event listener for "${eventType}":`, error);
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Get all event types with listeners
98
+ */
99
+ public getEventTypes(): string[] {
100
+ return Array.from(this.listeners.keys());
101
+ }
102
+
103
+ /**
104
+ * Get listener count for event type
105
+ */
106
+ public getListenerCount(eventType: string): number {
107
+ const callbacks = this.listeners.get(eventType);
108
+ return callbacks ? callbacks.size : 0;
109
+ }
110
+
111
+ /**
112
+ * Check if event type has listeners
113
+ */
114
+ public hasListeners(eventType: string): boolean {
115
+ return this.getListenerCount(eventType) > 0;
116
+ }
117
+
118
+ /**
119
+ * Clear all listeners
120
+ */
121
+ public clear(): void {
122
+ this.listeners.clear();
123
+ }
124
+
125
+ /**
126
+ * Destroy the event system
127
+ */
128
+ public destroy(): void {
129
+ this.clear();
130
+ this.initialized = false;
131
+ }
132
+
133
+ /**
134
+ * Get debug information
135
+ */
136
+ public getDebugInfo(): object {
137
+ const info: Record<string, number> = {};
138
+ this.listeners.forEach((callbacks, eventType) => {
139
+ info[eventType] = callbacks.size;
140
+ });
141
+ return {
142
+ initialized: this.initialized,
143
+ totalEventTypes: this.listeners.size,
144
+ listeners: info
145
+ };
146
+ }
147
+ }