@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,759 @@
1
+ /**
2
+ * FluidTypography - Intelligent fluid typography system
3
+ * Provides clamp-based scaling, container-relative typography, and accessibility compliance
4
+ */
5
+
6
+ import { logger } from '../utils/Logger';
7
+ import { PerformanceMonitor } from '../performance/PerformanceMonitor';
8
+
9
+ export interface FluidConfig {
10
+ minSize: number;
11
+ maxSize: number;
12
+ minViewport?: number;
13
+ maxViewport?: number;
14
+ scalingFunction?: 'linear' | 'exponential';
15
+ accessibility?: 'none' | 'AA' | 'AAA';
16
+ enforceAccessibility?: boolean;
17
+ respectUserPreferences?: boolean;
18
+ }
19
+
20
+ export interface ContainerBasedConfig {
21
+ minSize: number;
22
+ maxSize: number;
23
+ containerElement?: Element;
24
+ minContainerWidth?: number;
25
+ maxContainerWidth?: number;
26
+ accessibility?: 'none' | 'AA' | 'AAA';
27
+ }
28
+
29
+ export interface TextFittingConfig {
30
+ maxWidth: number;
31
+ minSize: number;
32
+ maxSize: number;
33
+ allowOverflow?: boolean;
34
+ wordBreak?: 'normal' | 'break-all' | 'keep-all';
35
+ }
36
+
37
+ export class FluidTypography {
38
+ private appliedElements: WeakSet<Element> = new WeakSet();
39
+ private resizeObserver: ResizeObserver | null = null;
40
+ private containerConfigs: Map<Element, ContainerBasedConfig> = new Map();
41
+ private performanceMonitor?: PerformanceMonitor;
42
+
43
+ constructor() {
44
+ this.setupResizeObserver();
45
+ }
46
+
47
+ /**
48
+ * Set performance monitor for integration
49
+ */
50
+ public setPerformanceMonitor(monitor: PerformanceMonitor): void {
51
+ this.performanceMonitor = monitor;
52
+ }
53
+
54
+ /**
55
+ * Generate a typographic scale
56
+ */
57
+ public generateTypographicScale(config: {
58
+ baseSize: number;
59
+ ratio: number;
60
+ steps?: number;
61
+ }): number[] {
62
+ const { baseSize, ratio, steps = 5 } = config;
63
+ const scale: number[] = [];
64
+
65
+ // Generate scale steps as array of numbers
66
+ for (let i = 0; i < steps; i++) {
67
+ const size = baseSize * Math.pow(ratio, i);
68
+ scale.push(parseFloat(size.toFixed(2)));
69
+ }
70
+
71
+ return scale;
72
+ }
73
+
74
+ /**
75
+ * Apply fluid scaling using CSS clamp()
76
+ */
77
+ public applyFluidScaling(element: Element, config: FluidConfig): void {
78
+ const {
79
+ minSize,
80
+ maxSize,
81
+ minViewport = 320,
82
+ maxViewport = 1200,
83
+ scalingFunction = 'linear',
84
+ accessibility = 'AAA',
85
+ enforceAccessibility = true,
86
+ respectUserPreferences = true
87
+ } = config;
88
+
89
+ try {
90
+ // Validate and adjust sizes for accessibility
91
+ const adjustedSizes = this.enforceAccessibilityConstraints(
92
+ minSize,
93
+ maxSize,
94
+ accessibility,
95
+ enforceAccessibility
96
+ );
97
+
98
+ // Apply user preference scaling if enabled
99
+ const finalSizes = respectUserPreferences
100
+ ? this.applyUserPreferences(adjustedSizes.minSize, adjustedSizes.maxSize)
101
+ : adjustedSizes;
102
+
103
+ // Generate clamp() CSS value or static value for small ranges
104
+ let fontValue: string;
105
+
106
+ // If the size range is very small (2px or less), use static value
107
+ if (Math.abs(finalSizes.maxSize - finalSizes.minSize) <= 2) {
108
+ fontValue = `${finalSizes.minSize}px`;
109
+ } else {
110
+ fontValue = this.generateClampValue(
111
+ finalSizes.minSize,
112
+ finalSizes.maxSize,
113
+ minViewport,
114
+ maxViewport,
115
+ scalingFunction
116
+ );
117
+ }
118
+
119
+ // Apply to element
120
+ this.applyFontSize(element, fontValue);
121
+ this.appliedElements.add(element);
122
+
123
+ // Record performance metrics
124
+ if (this.performanceMonitor) {
125
+ this.performanceMonitor.recordOperation();
126
+ }
127
+
128
+ // Add data attributes for debugging
129
+ element.setAttribute('data-proteus-fluid', 'true');
130
+ element.setAttribute('data-proteus-min-size', finalSizes.minSize.toString());
131
+ element.setAttribute('data-proteus-max-size', finalSizes.maxSize.toString());
132
+
133
+ } catch (error) {
134
+ logger.error('Failed to apply fluid scaling', error);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Apply container-based typography scaling
140
+ */
141
+ public applyContainerBasedScaling(element: Element, config: ContainerBasedConfig): void {
142
+ try {
143
+ const container = config.containerElement || this.findNearestContainer(element);
144
+ if (!container) {
145
+ logger.warn('No container found for container-based scaling');
146
+ return;
147
+ }
148
+
149
+ // Store config for resize updates
150
+ this.containerConfigs.set(element, config);
151
+
152
+ // Start observing container
153
+ if (this.resizeObserver) {
154
+ this.resizeObserver.observe(container);
155
+ }
156
+
157
+ // Apply initial scaling
158
+ this.updateContainerBasedScaling(element, container, config);
159
+
160
+ this.appliedElements.add(element);
161
+
162
+ } catch (error) {
163
+ logger.error('Failed to apply container-based scaling', error);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Fit text to container width
169
+ */
170
+ public fitTextToContainer(element: Element, config: TextFittingConfig): void {
171
+ const {
172
+ maxWidth,
173
+ minSize,
174
+ maxSize,
175
+ allowOverflow = false,
176
+ wordBreak = 'normal'
177
+ } = config;
178
+
179
+ try {
180
+ // Measure text width at different sizes
181
+ const optimalSize = this.calculateOptimalTextSize(element, maxWidth, minSize, maxSize);
182
+
183
+ // Apply the calculated size
184
+ this.applyFontSize(element, `${optimalSize}px`);
185
+
186
+ // Handle overflow
187
+ if (!allowOverflow) {
188
+ const htmlElement = element as HTMLElement;
189
+ htmlElement.style.overflow = 'hidden';
190
+ htmlElement.style.textOverflow = 'ellipsis';
191
+ htmlElement.style.wordBreak = wordBreak;
192
+ }
193
+
194
+ this.appliedElements.add(element);
195
+
196
+ } catch (error) {
197
+ logger.error('Failed to fit text to container', error);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Remove fluid typography from element
203
+ */
204
+ public removeFluidScaling(element: Element): void {
205
+ if (!this.appliedElements.has(element)) return;
206
+
207
+ // Remove font-size style
208
+ const style = element.getAttribute('style');
209
+ if (style) {
210
+ const newStyle = style.replace(/font-size:[^;]+;?/g, '');
211
+ if (newStyle.trim()) {
212
+ element.setAttribute('style', newStyle);
213
+ } else {
214
+ element.removeAttribute('style');
215
+ }
216
+ }
217
+
218
+ // Remove data attributes
219
+ element.removeAttribute('data-proteus-fluid');
220
+ element.removeAttribute('data-proteus-min-size');
221
+ element.removeAttribute('data-proteus-max-size');
222
+
223
+ this.appliedElements.delete(element);
224
+ this.containerConfigs.delete(element);
225
+ }
226
+
227
+ /**
228
+ * Clean up resources
229
+ */
230
+ public destroy(): void {
231
+ if (this.resizeObserver) {
232
+ this.resizeObserver.disconnect();
233
+ this.resizeObserver = null;
234
+ }
235
+
236
+ this.containerConfigs = new Map();
237
+ }
238
+
239
+ /**
240
+ * Setup ResizeObserver for container-based scaling
241
+ */
242
+ private setupResizeObserver(): void {
243
+ if (typeof ResizeObserver === 'undefined') {
244
+ logger.warn('ResizeObserver not supported. Container-based typography may not work correctly.');
245
+ return;
246
+ }
247
+
248
+ this.resizeObserver = new ResizeObserver((entries) => {
249
+ for (const entry of entries) {
250
+ this.handleContainerResize(entry.target);
251
+ }
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Handle container resize for container-based scaling
257
+ */
258
+ private handleContainerResize(container: Element): void {
259
+ // Find all elements using this container
260
+ for (const [element, config] of this.containerConfigs) {
261
+ const elementContainer = config.containerElement || this.findNearestContainer(element);
262
+ if (elementContainer === container) {
263
+ this.updateContainerBasedScaling(element, container, config);
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Update container-based scaling when container resizes
270
+ */
271
+ private updateContainerBasedScaling(
272
+ element: Element,
273
+ container: Element,
274
+ config: ContainerBasedConfig
275
+ ): void {
276
+ const containerWidth = container.getBoundingClientRect().width;
277
+ const {
278
+ minSize,
279
+ maxSize,
280
+ minContainerWidth = 300,
281
+ maxContainerWidth = 800,
282
+ accessibility = 'AA'
283
+ } = config;
284
+
285
+ // Calculate scale factor based on container width
286
+ const scaleFactor = Math.max(0, Math.min(1,
287
+ (containerWidth - minContainerWidth) / (maxContainerWidth - minContainerWidth)
288
+ ));
289
+
290
+ // Calculate font size
291
+ let fontSize = minSize + (maxSize - minSize) * scaleFactor;
292
+
293
+ // Apply accessibility constraints
294
+ const adjustedSizes = this.enforceAccessibilityConstraints(fontSize, fontSize, accessibility, true);
295
+ fontSize = adjustedSizes.minSize;
296
+
297
+ // Apply to element
298
+ this.applyFontSize(element, `${fontSize}px`);
299
+ }
300
+
301
+ /**
302
+ * Generate CSS clamp() value
303
+ */
304
+ private generateClampValue(
305
+ minSize: number,
306
+ maxSize: number,
307
+ minViewport: number,
308
+ maxViewport: number,
309
+ scalingFunction: 'linear' | 'exponential'
310
+ ): string {
311
+ // Validate inputs to prevent NaN
312
+ if (!Number.isFinite(minSize) || !Number.isFinite(maxSize) ||
313
+ !Number.isFinite(minViewport) || !Number.isFinite(maxViewport)) {
314
+ logger.warn('Invalid numeric inputs for clamp calculation');
315
+ return `${Number.isFinite(minSize) ? minSize : 16}px`; // Fallback to static size
316
+ }
317
+
318
+ if (minViewport >= maxViewport) {
319
+ logger.warn('Invalid viewport range: minViewport must be less than maxViewport');
320
+ return `${minSize}px`; // Fallback to static size
321
+ }
322
+
323
+ if (minSize >= maxSize) {
324
+ logger.warn('Invalid size range: minSize must be less than maxSize');
325
+ return `${minSize}px`; // Fallback to static size
326
+ }
327
+
328
+ if (scalingFunction === 'exponential') {
329
+ // For exponential scaling, use a more complex calculation
330
+ const midSize = Math.sqrt(minSize * maxSize);
331
+ const midViewport = Math.sqrt(minViewport * maxViewport);
332
+
333
+ // Ensure we don't divide by zero
334
+ const viewportDiff = midViewport - minViewport;
335
+ if (viewportDiff === 0) {
336
+ return `${minSize}px`;
337
+ }
338
+
339
+ return `clamp(${minSize}px, ${minSize}px + (${midSize - minSize}) * ((100vw - ${minViewport}px) / ${viewportDiff}px), ${maxSize}px)`;
340
+ }
341
+
342
+ // Linear scaling (default)
343
+ const viewportRange = maxViewport - minViewport;
344
+ const sizeRange = maxSize - minSize;
345
+
346
+ // Ensure we don't divide by zero
347
+ if (viewportRange === 0) {
348
+ return `${minSize}px`;
349
+ }
350
+
351
+ // Calculate slope (change in size per viewport unit)
352
+ const slope = sizeRange / viewportRange;
353
+
354
+ // Calculate y-intercept (size when viewport is 0)
355
+ const yIntercept = minSize - slope * minViewport;
356
+
357
+ // Validate calculated values
358
+ if (!Number.isFinite(slope) || !Number.isFinite(yIntercept)) {
359
+ logger.warn('Invalid clamp calculation, falling back to static size');
360
+ return `${minSize}px`;
361
+ }
362
+
363
+ // Generate the clamp value with proper units
364
+ return `clamp(${minSize}px, ${yIntercept.toFixed(3)}px + ${(slope * 100).toFixed(3)}vw, ${maxSize}px)`;
365
+ }
366
+
367
+ /**
368
+ * Generate linear clamp value
369
+ */
370
+ private generateLinearClamp(
371
+ minSize: number,
372
+ maxSize: number,
373
+ minViewport: number,
374
+ maxViewport: number
375
+ ): string {
376
+ const viewportRange = maxViewport - minViewport;
377
+ const sizeRange = maxSize - minSize;
378
+ const slope = sizeRange / viewportRange;
379
+ const yIntercept = minSize - slope * minViewport;
380
+
381
+ return `clamp(${minSize}px, ${yIntercept.toFixed(3)}px + ${(slope * 100).toFixed(3)}vw, ${maxSize}px)`;
382
+ }
383
+
384
+ /**
385
+ * Generate exponential clamp value
386
+ */
387
+ private generateExponentialClamp(
388
+ minSize: number,
389
+ maxSize: number,
390
+ minViewport: number,
391
+ maxViewport: number
392
+ ): string {
393
+ const midSize = Math.sqrt(minSize * maxSize);
394
+ const midViewport = Math.sqrt(minViewport * maxViewport);
395
+ const viewportDiff = midViewport - minViewport;
396
+
397
+ if (Math.abs(viewportDiff) < 0.001) {
398
+ return `${minSize}px`;
399
+ }
400
+
401
+ const sizeChange = midSize - minSize;
402
+ const rate = sizeChange / viewportDiff;
403
+
404
+ return `clamp(${minSize}px, ${minSize}px + ${rate.toFixed(4)} * (100vw - ${minViewport}px), ${maxSize}px)`;
405
+ }
406
+
407
+ /**
408
+ * Validate that a clamp value is properly formatted
409
+ */
410
+ private isValidClampValue(clampValue: string): boolean {
411
+ // Check basic clamp format
412
+ const clampRegex = /^clamp\(\s*[\d.]+px\s*,\s*[^,]+\s*,\s*[\d.]+px\s*\)$/;
413
+ if (!clampRegex.test(clampValue)) {
414
+ return false;
415
+ }
416
+
417
+ // Check for NaN or undefined values
418
+ if (clampValue.includes('NaN') || clampValue.includes('undefined')) {
419
+ return false;
420
+ }
421
+
422
+ return true;
423
+ }
424
+
425
+ /**
426
+ * Enforce accessibility constraints on font sizes
427
+ */
428
+ private enforceAccessibilityConstraints(
429
+ minSize: number,
430
+ maxSize: number,
431
+ level: 'none' | 'AA' | 'AAA',
432
+ enforce: boolean
433
+ ): { minSize: number; maxSize: number } {
434
+ if (level === 'none' || !enforce) {
435
+ return { minSize, maxSize };
436
+ }
437
+
438
+ // WCAG minimum font sizes
439
+ const minimums = {
440
+ AA: 14,
441
+ AAA: 16
442
+ };
443
+
444
+ const minimum = minimums[level];
445
+
446
+ return {
447
+ minSize: Math.max(minSize, minimum),
448
+ maxSize: Math.max(maxSize, minimum)
449
+ };
450
+ }
451
+
452
+ /**
453
+ * Apply user preference scaling
454
+ */
455
+ private applyUserPreferences(minSize: number, maxSize: number): { minSize: number; maxSize: number } {
456
+ try {
457
+ // Get user's preferred font size from root element
458
+ const rootFontSizeStr = getComputedStyle(document.documentElement).fontSize;
459
+ const rootFontSize = parseFloat(rootFontSizeStr || '16');
460
+ const defaultFontSize = 16; // Browser default
461
+
462
+ // Validate the root font size
463
+ if (!Number.isFinite(rootFontSize) || rootFontSize <= 0) {
464
+ logger.warn('Invalid root font size, using default scaling');
465
+ return { minSize, maxSize };
466
+ }
467
+
468
+ const userScale = rootFontSize / defaultFontSize;
469
+
470
+ // Validate the scale factor
471
+ if (!Number.isFinite(userScale) || userScale <= 0) {
472
+ logger.warn('Invalid user scale factor, using default scaling');
473
+ return { minSize, maxSize };
474
+ }
475
+
476
+ const scaledMinSize = minSize * userScale;
477
+ const scaledMaxSize = maxSize * userScale;
478
+
479
+ // Validate the scaled values
480
+ if (!Number.isFinite(scaledMinSize) || !Number.isFinite(scaledMaxSize)) {
481
+ logger.warn('Invalid scaled sizes, using default scaling');
482
+ return { minSize, maxSize };
483
+ }
484
+
485
+ return {
486
+ minSize: scaledMinSize,
487
+ maxSize: scaledMaxSize
488
+ };
489
+ } catch (error) {
490
+ logger.warn('Error applying user preferences, using default scaling', error);
491
+ return { minSize, maxSize };
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Apply font size to element
497
+ */
498
+ private applyFontSize(element: Element, fontSize: string): void {
499
+ const htmlElement = element as HTMLElement;
500
+ htmlElement.style.fontSize = fontSize;
501
+ }
502
+
503
+ /**
504
+ * Safely get computed font size with fallbacks
505
+ */
506
+ private getComputedFontSize(element: Element): number {
507
+ try {
508
+ const computedStyle = getComputedStyle(element);
509
+ const fontSize = parseFloat(computedStyle.fontSize);
510
+
511
+ // Return valid number or fallback
512
+ if (Number.isFinite(fontSize) && fontSize > 0) {
513
+ return fontSize;
514
+ }
515
+
516
+ // Try to get from inline style
517
+ const htmlElement = element as HTMLElement;
518
+ if (htmlElement.style.fontSize) {
519
+ const inlineSize = parseFloat(htmlElement.style.fontSize);
520
+ if (Number.isFinite(inlineSize) && inlineSize > 0) {
521
+ return inlineSize;
522
+ }
523
+ }
524
+
525
+ // Default fallback
526
+ return 16;
527
+ } catch (error) {
528
+ logger.warn('Failed to get computed font size, using fallback', error);
529
+ return 16;
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Find the nearest container element
535
+ */
536
+ private findNearestContainer(element: Element): Element | null {
537
+ let current = element.parentElement;
538
+
539
+ while (current) {
540
+ const style = getComputedStyle(current);
541
+
542
+ // Look for elements that are likely containers
543
+ if (
544
+ style.display.includes('grid') ||
545
+ style.display.includes('flex') ||
546
+ style.display === 'block' ||
547
+ current.matches('main, article, section, aside, div, header, footer')
548
+ ) {
549
+ return current;
550
+ }
551
+
552
+ current = current.parentElement;
553
+ }
554
+
555
+ return document.body;
556
+ }
557
+
558
+ /**
559
+ * Calculate optimal text size to fit within width
560
+ */
561
+ private calculateOptimalTextSize(
562
+ element: Element,
563
+ maxWidth: number,
564
+ minSize: number,
565
+ maxSize: number
566
+ ): number {
567
+ const text = element.textContent || '';
568
+ if (!text.trim()) return minSize;
569
+
570
+ // Create a temporary element for measurement
571
+ const temp = document.createElement('span');
572
+ temp.style.visibility = 'hidden';
573
+ temp.style.position = 'absolute';
574
+ temp.style.whiteSpace = 'nowrap';
575
+ temp.textContent = text;
576
+
577
+ // Copy relevant styles with fallbacks
578
+ const computedStyle = getComputedStyle(element);
579
+ temp.style.fontFamily = computedStyle.fontFamily || 'Arial, sans-serif';
580
+ temp.style.fontWeight = computedStyle.fontWeight || 'normal';
581
+ temp.style.fontStyle = computedStyle.fontStyle || 'normal';
582
+
583
+ document.body.appendChild(temp);
584
+
585
+ try {
586
+ // Binary search for optimal size
587
+ let low = minSize;
588
+ let high = maxSize;
589
+ let optimalSize = minSize;
590
+
591
+ while (low <= high) {
592
+ const mid = Math.floor((low + high) / 2);
593
+ temp.style.fontSize = `${mid}px`;
594
+
595
+ const width = temp.getBoundingClientRect().width;
596
+
597
+ if (width <= maxWidth) {
598
+ optimalSize = mid;
599
+ low = mid + 1;
600
+ } else {
601
+ high = mid - 1;
602
+ }
603
+ }
604
+
605
+ return optimalSize;
606
+ } finally {
607
+ document.body.removeChild(temp);
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Production-grade font optimization with performance considerations
613
+ */
614
+ public optimizeFontPerformance(element: Element): void {
615
+ const htmlElement = element as HTMLElement;
616
+
617
+ // Optimize line height based on font size
618
+ this.optimizeLineHeightForElement(element);
619
+
620
+ // Apply font loading optimization
621
+ this.optimizeFontLoading(element);
622
+
623
+ // Add performance hints
624
+ this.addPerformanceHints(element);
625
+
626
+ // Apply font smoothing
627
+ this.applyFontSmoothing(htmlElement);
628
+ }
629
+
630
+ /**
631
+ * Enhanced line height optimization for better readability
632
+ */
633
+ private optimizeLineHeightForElement(element: Element): void {
634
+ const fontSize = this.getComputedFontSize(element);
635
+
636
+ if (!Number.isFinite(fontSize) || fontSize <= 0) return;
637
+
638
+ // Calculate optimal line height based on font size and content type
639
+ let optimalLineHeight: number;
640
+
641
+ if (fontSize <= 14) {
642
+ optimalLineHeight = 1.7; // Better readability for small text
643
+ } else if (fontSize <= 18) {
644
+ optimalLineHeight = 1.6; // Standard body text
645
+ } else if (fontSize <= 24) {
646
+ optimalLineHeight = 1.4; // Balanced for medium text
647
+ } else if (fontSize <= 32) {
648
+ optimalLineHeight = 1.3; // Headings
649
+ } else {
650
+ optimalLineHeight = 1.2; // Large headings
651
+ }
652
+
653
+ // Apply WCAG AAA line height requirements (minimum 1.5)
654
+ optimalLineHeight = Math.max(optimalLineHeight, 1.5);
655
+
656
+ (element as HTMLElement).style.lineHeight = optimalLineHeight.toString();
657
+ }
658
+
659
+ /**
660
+ * Optimize font loading for performance
661
+ */
662
+ private optimizeFontLoading(element: Element): void {
663
+ const htmlElement = element as HTMLElement;
664
+ const computedStyle = window.getComputedStyle(element);
665
+ const fontFamily = computedStyle.fontFamily;
666
+
667
+ // Add font-display: swap for better loading performance
668
+ if (fontFamily && !this.isSystemFont(fontFamily)) {
669
+ htmlElement.style.setProperty('font-display', 'swap');
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Check if font is a system font
675
+ */
676
+ private isSystemFont(fontFamily: string): boolean {
677
+ const systemFonts = [
678
+ 'system-ui', '-apple-system', 'BlinkMacSystemFont',
679
+ 'Segoe UI', 'Roboto', 'Arial', 'sans-serif', 'serif', 'monospace'
680
+ ];
681
+
682
+ return systemFonts.some(font => fontFamily.toLowerCase().includes(font.toLowerCase()));
683
+ }
684
+
685
+ /**
686
+ * Add performance hints for better rendering
687
+ */
688
+ private addPerformanceHints(element: Element): void {
689
+ const htmlElement = element as HTMLElement;
690
+
691
+ // Add will-change hint for elements that will be animated
692
+ if (this.isAnimatedElement(element)) {
693
+ htmlElement.style.willChange = 'font-size';
694
+ }
695
+
696
+ // Add contain hint for better layout performance
697
+ htmlElement.style.contain = 'layout style';
698
+ }
699
+
700
+ /**
701
+ * Check if element is likely to be animated
702
+ */
703
+ private isAnimatedElement(element: Element): boolean {
704
+ const computedStyle = window.getComputedStyle(element);
705
+ return computedStyle.transition.includes('font-size') ||
706
+ computedStyle.animation !== 'none' ||
707
+ element.hasAttribute('data-proteus-animated');
708
+ }
709
+
710
+ /**
711
+ * Apply font smoothing for better text rendering
712
+ */
713
+ private applyFontSmoothing(element: HTMLElement): void {
714
+ // Apply font smoothing for better text rendering
715
+ element.style.setProperty('-webkit-font-smoothing', 'antialiased');
716
+ element.style.setProperty('-moz-osx-font-smoothing', 'grayscale');
717
+
718
+ // Add text rendering optimization
719
+ element.style.setProperty('text-rendering', 'optimizeLegibility');
720
+ }
721
+
722
+ /**
723
+ * Record performance metrics for monitoring
724
+ */
725
+ public recordPerformanceMetrics(element: Element, clampValue: string): void {
726
+ const startTime = performance.now();
727
+
728
+ // Measure font application time
729
+ requestAnimationFrame(() => {
730
+ const endTime = performance.now();
731
+ const renderTime = endTime - startTime;
732
+
733
+ // Store metrics for performance monitoring
734
+ if (typeof (window as any).proteusMetrics === 'undefined') {
735
+ (window as any).proteusMetrics = {
736
+ fontApplicationTimes: [],
737
+ averageRenderTime: 0
738
+ };
739
+ }
740
+
741
+ const metrics = (window as any).proteusMetrics;
742
+ metrics.fontApplicationTimes.push(renderTime);
743
+
744
+ // Keep only last 100 measurements
745
+ if (metrics.fontApplicationTimes.length > 100) {
746
+ metrics.fontApplicationTimes.shift();
747
+ }
748
+
749
+ // Calculate average
750
+ metrics.averageRenderTime = metrics.fontApplicationTimes.reduce((a: number, b: number) => a + b, 0) / metrics.fontApplicationTimes.length;
751
+
752
+ // Log performance warnings
753
+ if (renderTime > 16) { // More than one frame
754
+ const elementTag = element.tagName.toLowerCase();
755
+ logger.warn(`Slow font application detected: ${renderTime.toFixed(2)}ms for ${elementTag} with clamp: ${clampValue}`);
756
+ }
757
+ });
758
+ }
759
+ }