@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.
- package/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- 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
|
+
}
|