@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,326 @@
1
+ /**
2
+ * Smart Line Height Optimization for ProteusJS
3
+ * Dynamic line-height optimization for optimal readability
4
+ */
5
+
6
+ export interface LineHeightConfig {
7
+ baseFontSize: number;
8
+ baseLineHeight: number;
9
+ minLineHeight: number;
10
+ maxLineHeight: number;
11
+ language: string;
12
+ contentType: 'heading' | 'body' | 'caption' | 'code' | 'display';
13
+ accessibility: boolean;
14
+ density: 'compact' | 'comfortable' | 'spacious';
15
+ }
16
+
17
+ export interface OptimizationResult {
18
+ lineHeight: number;
19
+ ratio: number;
20
+ reasoning: string[];
21
+ accessibility: {
22
+ wcagCompliant: boolean;
23
+ readabilityScore: number;
24
+ };
25
+ }
26
+
27
+ export class LineHeightOptimizer {
28
+ private static readonly LANGUAGE_ADJUSTMENTS = {
29
+ 'en': 1.0, // English baseline
30
+ 'zh': 1.1, // Chinese - needs more space
31
+ 'ja': 1.1, // Japanese - needs more space
32
+ 'ko': 1.1, // Korean - needs more space
33
+ 'ar': 1.05, // Arabic - slightly more space
34
+ 'hi': 1.05, // Hindi - slightly more space
35
+ 'th': 1.15, // Thai - needs significant space
36
+ 'vi': 1.05, // Vietnamese - slightly more space
37
+ 'de': 0.98, // German - can be slightly tighter
38
+ 'fr': 1.0, // French - baseline
39
+ 'es': 1.0, // Spanish - baseline
40
+ 'it': 1.0, // Italian - baseline
41
+ 'pt': 1.0, // Portuguese - baseline
42
+ 'ru': 1.02, // Russian - slightly more space
43
+ 'default': 1.0
44
+ };
45
+
46
+ private static readonly CONTENT_TYPE_RATIOS = {
47
+ 'heading': { min: 1.0, optimal: 1.2, max: 1.4 },
48
+ 'body': { min: 1.3, optimal: 1.5, max: 1.8 },
49
+ 'caption': { min: 1.2, optimal: 1.4, max: 1.6 },
50
+ 'code': { min: 1.2, optimal: 1.4, max: 1.6 },
51
+ 'display': { min: 0.9, optimal: 1.1, max: 1.3 }
52
+ };
53
+
54
+ private static readonly DENSITY_MULTIPLIERS = {
55
+ 'compact': 0.9,
56
+ 'comfortable': 1.0,
57
+ 'spacious': 1.1
58
+ };
59
+
60
+ /**
61
+ * Calculate optimal line height
62
+ */
63
+ public calculateOptimalLineHeight(
64
+ fontSize: number,
65
+ lineLength: number,
66
+ containerWidth: number,
67
+ config: LineHeightConfig
68
+ ): OptimizationResult {
69
+ const reasoning: string[] = [];
70
+ let lineHeight = config.baseLineHeight;
71
+
72
+ // 1. Base ratio for content type
73
+ const contentRatios = LineHeightOptimizer.CONTENT_TYPE_RATIOS[config.contentType];
74
+ lineHeight = contentRatios.optimal;
75
+ reasoning.push(`Base ratio for ${config.contentType}: ${lineHeight}`);
76
+
77
+ // 2. Font size adjustment
78
+ const sizeAdjustment = this.calculateSizeAdjustment(fontSize, config.baseFontSize);
79
+ lineHeight *= sizeAdjustment;
80
+ reasoning.push(`Font size adjustment (${fontSize}px): ×${sizeAdjustment.toFixed(3)}`);
81
+
82
+ // 3. Line length adjustment
83
+ const lengthAdjustment = this.calculateLineLengthAdjustment(lineLength, fontSize);
84
+ lineHeight *= lengthAdjustment;
85
+ reasoning.push(`Line length adjustment (${lineLength} chars): ×${lengthAdjustment.toFixed(3)}`);
86
+
87
+ // 4. Language adjustment
88
+ const langCode = config.language?.split('-')[0]?.toLowerCase() || 'en';
89
+ const languageMultiplier = LineHeightOptimizer.LANGUAGE_ADJUSTMENTS[langCode as keyof typeof LineHeightOptimizer.LANGUAGE_ADJUSTMENTS] || LineHeightOptimizer.LANGUAGE_ADJUSTMENTS.default;
90
+ lineHeight *= languageMultiplier;
91
+ if (languageMultiplier !== 1.0) {
92
+ reasoning.push(`Language adjustment (${langCode}): ×${languageMultiplier}`);
93
+ }
94
+
95
+ // 5. Density adjustment
96
+ const densityMultiplier = LineHeightOptimizer.DENSITY_MULTIPLIERS[config.density];
97
+ lineHeight *= densityMultiplier;
98
+ reasoning.push(`Density adjustment (${config.density}): ×${densityMultiplier}`);
99
+
100
+ // 6. Container width adjustment
101
+ const widthAdjustment = this.calculateWidthAdjustment(containerWidth, fontSize);
102
+ lineHeight *= widthAdjustment;
103
+ if (widthAdjustment !== 1.0) {
104
+ reasoning.push(`Container width adjustment: ×${widthAdjustment.toFixed(3)}`);
105
+ }
106
+
107
+ // 7. Accessibility adjustments
108
+ if (config.accessibility) {
109
+ const accessibilityAdjustment = this.calculateAccessibilityAdjustment(lineHeight, config);
110
+ lineHeight = accessibilityAdjustment.lineHeight;
111
+ if (accessibilityAdjustment.adjusted) {
112
+ reasoning.push(`Accessibility adjustment: ${accessibilityAdjustment.reason}`);
113
+ }
114
+ }
115
+
116
+ // 8. Clamp to bounds
117
+ const originalLineHeight = lineHeight;
118
+ lineHeight = Math.max(config.minLineHeight, Math.min(config.maxLineHeight, lineHeight));
119
+ if (lineHeight !== originalLineHeight) {
120
+ reasoning.push(`Clamped to bounds: ${config.minLineHeight}-${config.maxLineHeight}`);
121
+ }
122
+
123
+ // Calculate accessibility metrics
124
+ const accessibility = this.calculateAccessibilityMetrics(lineHeight, fontSize, config);
125
+
126
+ return {
127
+ lineHeight: Math.round(lineHeight * 1000) / 1000, // Round to 3 decimal places
128
+ ratio: lineHeight,
129
+ reasoning,
130
+ accessibility
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Apply line height optimization to element
136
+ */
137
+ public applyOptimization(
138
+ element: Element,
139
+ result: OptimizationResult,
140
+ config: LineHeightConfig
141
+ ): void {
142
+ const htmlElement = element as HTMLElement;
143
+ htmlElement.style.lineHeight = result.lineHeight.toString();
144
+
145
+ // Add data attributes for debugging
146
+ if (config.accessibility) {
147
+ htmlElement.setAttribute('data-proteus-line-height', result.lineHeight.toString());
148
+ htmlElement.setAttribute('data-proteus-wcag-compliant', result.accessibility.wcagCompliant.toString());
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Create responsive line height optimization
154
+ */
155
+ public createResponsiveOptimization(
156
+ element: Element,
157
+ config: LineHeightConfig
158
+ ): () => void {
159
+ const updateLineHeight = () => {
160
+ const computedStyle = getComputedStyle(element);
161
+ const fontSize = parseFloat(computedStyle.fontSize);
162
+ const containerWidth = element.getBoundingClientRect().width;
163
+
164
+ // Estimate line length based on average character width
165
+ const averageCharWidth = fontSize * 0.5; // Approximate
166
+ const lineLength = Math.floor(containerWidth / averageCharWidth);
167
+
168
+ const result = this.calculateOptimalLineHeight(
169
+ fontSize,
170
+ lineLength,
171
+ containerWidth,
172
+ config
173
+ );
174
+
175
+ this.applyOptimization(element, result, config);
176
+ };
177
+
178
+ // Initial optimization
179
+ updateLineHeight();
180
+
181
+ // Set up resize observer
182
+ const resizeObserver = new ResizeObserver(() => {
183
+ updateLineHeight();
184
+ });
185
+
186
+ resizeObserver.observe(element);
187
+
188
+ // Return cleanup function
189
+ return () => {
190
+ resizeObserver.disconnect();
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Calculate font size adjustment factor
196
+ */
197
+ private calculateSizeAdjustment(fontSize: number, baseFontSize: number): number {
198
+ const ratio = fontSize / baseFontSize;
199
+
200
+ // Smaller fonts need relatively larger line heights
201
+ // Larger fonts can have relatively smaller line heights
202
+ if (ratio < 1) {
203
+ return 1 + (1 - ratio) * 0.2; // Up to 20% increase for small fonts
204
+ } else if (ratio > 1) {
205
+ return 1 - Math.min((ratio - 1) * 0.1, 0.15); // Up to 15% decrease for large fonts
206
+ }
207
+
208
+ return 1;
209
+ }
210
+
211
+ /**
212
+ * Calculate line length adjustment factor
213
+ */
214
+ private calculateLineLengthAdjustment(lineLength: number, fontSize: number): number {
215
+ // Optimal line length is 45-75 characters
216
+ const optimalMin = 45;
217
+ const optimalMax = 75;
218
+
219
+ if (lineLength < optimalMin) {
220
+ // Short lines can have tighter line height
221
+ return 0.95;
222
+ } else if (lineLength > optimalMax) {
223
+ // Long lines need more line height for readability
224
+ const excess = lineLength - optimalMax;
225
+ return 1 + Math.min(excess * 0.002, 0.2); // Up to 20% increase
226
+ }
227
+
228
+ return 1;
229
+ }
230
+
231
+ /**
232
+ * Calculate container width adjustment factor
233
+ */
234
+ private calculateWidthAdjustment(containerWidth: number, fontSize: number): number {
235
+ // Very narrow containers need slightly more line height
236
+ const minComfortableWidth = fontSize * 20; // 20em
237
+
238
+ if (containerWidth < minComfortableWidth) {
239
+ const ratio = containerWidth / minComfortableWidth;
240
+ return 1 + (1 - ratio) * 0.1; // Up to 10% increase
241
+ }
242
+
243
+ return 1;
244
+ }
245
+
246
+ /**
247
+ * Calculate accessibility adjustments
248
+ */
249
+ private calculateAccessibilityAdjustment(
250
+ lineHeight: number,
251
+ config: LineHeightConfig
252
+ ): { lineHeight: number; adjusted: boolean; reason: string } {
253
+ // WCAG 2.1 AA requires line height to be at least 1.5 times the font size for body text
254
+ const wcagMinimum = config.contentType === 'body' ? 1.5 : 1.3;
255
+
256
+ if (lineHeight < wcagMinimum) {
257
+ return {
258
+ lineHeight: wcagMinimum,
259
+ adjusted: true,
260
+ reason: `WCAG compliance requires minimum ${wcagMinimum}`
261
+ };
262
+ }
263
+
264
+ return {
265
+ lineHeight,
266
+ adjusted: false,
267
+ reason: ''
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Calculate accessibility metrics
273
+ */
274
+ private calculateAccessibilityMetrics(
275
+ lineHeight: number,
276
+ fontSize: number,
277
+ config: LineHeightConfig
278
+ ): { wcagCompliant: boolean; readabilityScore: number } {
279
+ const wcagMinimum = config.contentType === 'body' ? 1.5 : 1.3;
280
+ const wcagCompliant = lineHeight >= wcagMinimum;
281
+
282
+ // Calculate readability score (0-100)
283
+ let score = 50; // Base score
284
+
285
+ // Line height contribution (40 points max)
286
+ const optimalRatio = LineHeightOptimizer.CONTENT_TYPE_RATIOS[config.contentType].optimal;
287
+ const heightDiff = Math.abs(lineHeight - optimalRatio);
288
+ const heightScore = Math.max(0, 40 - (heightDiff * 100));
289
+ score += heightScore;
290
+
291
+ // WCAG compliance bonus (10 points)
292
+ if (wcagCompliant) {
293
+ score += 10;
294
+ }
295
+
296
+ // Clamp to 0-100
297
+ score = Math.max(0, Math.min(100, score));
298
+
299
+ return {
300
+ wcagCompliant,
301
+ readabilityScore: Math.round(score)
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Get optimal configuration for language and content type
307
+ */
308
+ public static getOptimalConfig(
309
+ language: string,
310
+ contentType: LineHeightConfig['contentType'],
311
+ accessibility: boolean = true
312
+ ): Partial<LineHeightConfig> {
313
+ const contentRatios = this.CONTENT_TYPE_RATIOS[contentType];
314
+
315
+ return {
316
+ baseFontSize: 16,
317
+ baseLineHeight: contentRatios.optimal,
318
+ minLineHeight: contentRatios.min,
319
+ maxLineHeight: contentRatios.max,
320
+ language,
321
+ contentType,
322
+ accessibility,
323
+ density: 'comfortable'
324
+ };
325
+ }
326
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Text Fitting Algorithms for ProteusJS
3
+ * Dynamic text sizing for optimal readability and layout
4
+ */
5
+
6
+ export interface FittingConfig {
7
+ mode: 'single-line' | 'multi-line' | 'overflow-aware';
8
+ minSize: number;
9
+ maxSize: number;
10
+ unit: 'px' | 'rem' | 'em';
11
+ precision: number;
12
+ maxIterations: number;
13
+ lineHeight?: number;
14
+ wordBreak?: 'normal' | 'break-all' | 'keep-all';
15
+ overflow?: 'hidden' | 'ellipsis' | 'clip';
16
+ }
17
+
18
+ export interface FittingResult {
19
+ fontSize: number;
20
+ lineHeight: number;
21
+ actualLines: number;
22
+ overflow: boolean;
23
+ iterations: number;
24
+ success: boolean;
25
+ }
26
+
27
+ export class TextFitting {
28
+ private canvas: HTMLCanvasElement;
29
+ private context: CanvasRenderingContext2D;
30
+
31
+ constructor() {
32
+ this.canvas = document.createElement('canvas');
33
+ this.context = this.canvas.getContext('2d')!;
34
+ }
35
+
36
+ /**
37
+ * Fit text to container with optimal sizing
38
+ */
39
+ public fitText(
40
+ element: Element,
41
+ text: string,
42
+ containerWidth: number,
43
+ containerHeight: number,
44
+ config: FittingConfig
45
+ ): FittingResult {
46
+ const computedStyle = getComputedStyle(element);
47
+
48
+ switch (config.mode) {
49
+ case 'single-line':
50
+ return this.fitSingleLine(text, containerWidth, config, computedStyle);
51
+ case 'multi-line':
52
+ return this.fitMultiLine(text, containerWidth, containerHeight, config, computedStyle);
53
+ case 'overflow-aware':
54
+ return this.fitOverflowAware(text, containerWidth, containerHeight, config, computedStyle);
55
+ default:
56
+ throw new Error(`Unknown fitting mode: ${config.mode}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Apply fitting result to element
62
+ */
63
+ public applyFitting(element: Element, result: FittingResult, config: FittingConfig): void {
64
+ const htmlElement = element as HTMLElement;
65
+
66
+ htmlElement.style.fontSize = `${result.fontSize}${config.unit}`;
67
+ htmlElement.style.lineHeight = result.lineHeight.toString();
68
+
69
+ if (config.mode === 'overflow-aware' && result.overflow) {
70
+ htmlElement.style.overflow = config.overflow || 'hidden';
71
+ if (config.overflow === 'ellipsis') {
72
+ htmlElement.style.textOverflow = 'ellipsis';
73
+ htmlElement.style.whiteSpace = 'nowrap';
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Measure text dimensions
80
+ */
81
+ public measureText(
82
+ text: string,
83
+ fontSize: number,
84
+ fontFamily: string,
85
+ fontWeight: string = 'normal'
86
+ ): { width: number; height: number } {
87
+ this.context.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
88
+ const metrics = this.context.measureText(text);
89
+
90
+ return {
91
+ width: metrics.width,
92
+ height: fontSize * 1.2 // Approximate line height
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Calculate optimal font size for single line
98
+ */
99
+ private fitSingleLine(
100
+ text: string,
101
+ containerWidth: number,
102
+ config: FittingConfig,
103
+ computedStyle: CSSStyleDeclaration
104
+ ): FittingResult {
105
+ const fontFamily = computedStyle.fontFamily;
106
+ const fontWeight = computedStyle.fontWeight;
107
+
108
+ let minSize = config.minSize;
109
+ let maxSize = config.maxSize;
110
+ let iterations = 0;
111
+ let bestSize = minSize;
112
+
113
+ // Binary search for optimal size
114
+ while (maxSize - minSize > config.precision && iterations < config.maxIterations) {
115
+ const testSize = (minSize + maxSize) / 2;
116
+ const measurement = this.measureText(text, testSize, fontFamily, fontWeight);
117
+
118
+ if (measurement.width <= containerWidth) {
119
+ bestSize = testSize;
120
+ minSize = testSize;
121
+ } else {
122
+ maxSize = testSize;
123
+ }
124
+
125
+ iterations++;
126
+ }
127
+
128
+ const finalMeasurement = this.measureText(text, bestSize, fontFamily, fontWeight);
129
+
130
+ return {
131
+ fontSize: bestSize,
132
+ lineHeight: config.lineHeight || 1.2,
133
+ actualLines: 1,
134
+ overflow: finalMeasurement.width > containerWidth,
135
+ iterations,
136
+ success: finalMeasurement.width <= containerWidth
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Calculate optimal font size for multi-line text
142
+ */
143
+ private fitMultiLine(
144
+ text: string,
145
+ containerWidth: number,
146
+ containerHeight: number,
147
+ config: FittingConfig,
148
+ computedStyle: CSSStyleDeclaration
149
+ ): FittingResult {
150
+ const fontFamily = computedStyle.fontFamily;
151
+ const fontWeight = computedStyle.fontWeight;
152
+ const lineHeight = config.lineHeight || 1.4;
153
+
154
+ let minSize = config.minSize;
155
+ let maxSize = config.maxSize;
156
+ let iterations = 0;
157
+ let bestSize = minSize;
158
+ let bestLines = 0;
159
+
160
+ while (maxSize - minSize > config.precision && iterations < config.maxIterations) {
161
+ const testSize = (minSize + maxSize) / 2;
162
+ const lines = this.calculateLines(text, testSize, containerWidth, fontFamily, fontWeight);
163
+ const totalHeight = lines * testSize * lineHeight;
164
+
165
+ if (totalHeight <= containerHeight) {
166
+ bestSize = testSize;
167
+ bestLines = lines;
168
+ minSize = testSize;
169
+ } else {
170
+ maxSize = testSize;
171
+ }
172
+
173
+ iterations++;
174
+ }
175
+
176
+ const finalLines = this.calculateLines(text, bestSize, containerWidth, fontFamily, fontWeight);
177
+ const finalHeight = finalLines * bestSize * lineHeight;
178
+
179
+ return {
180
+ fontSize: bestSize,
181
+ lineHeight,
182
+ actualLines: finalLines,
183
+ overflow: finalHeight > containerHeight,
184
+ iterations,
185
+ success: finalHeight <= containerHeight
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Calculate optimal font size with overflow awareness
191
+ */
192
+ private fitOverflowAware(
193
+ text: string,
194
+ containerWidth: number,
195
+ containerHeight: number,
196
+ config: FittingConfig,
197
+ computedStyle: CSSStyleDeclaration
198
+ ): FittingResult {
199
+ // First try multi-line fitting
200
+ const multiLineResult = this.fitMultiLine(text, containerWidth, containerHeight, config, computedStyle);
201
+
202
+ if (multiLineResult.success) {
203
+ return multiLineResult;
204
+ }
205
+
206
+ // If multi-line doesn't fit, try single-line with ellipsis
207
+ const singleLineResult = this.fitSingleLine(text, containerWidth, config, computedStyle);
208
+
209
+ // Calculate how many lines we can fit at this font size
210
+ const lineHeight = config.lineHeight || 1.4;
211
+ const maxLines = Math.floor(containerHeight / (singleLineResult.fontSize * lineHeight));
212
+
213
+ return {
214
+ fontSize: singleLineResult.fontSize,
215
+ lineHeight,
216
+ actualLines: Math.min(maxLines, 1),
217
+ overflow: true,
218
+ iterations: singleLineResult.iterations,
219
+ success: maxLines >= 1
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Calculate number of lines for given text and font size
225
+ */
226
+ private calculateLines(
227
+ text: string,
228
+ fontSize: number,
229
+ containerWidth: number,
230
+ fontFamily: string,
231
+ fontWeight: string
232
+ ): number {
233
+ this.context.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
234
+
235
+ const words = text.split(/\s+/);
236
+ let lines = 1;
237
+ let currentLineWidth = 0;
238
+
239
+ for (const word of words) {
240
+ const wordWidth = this.context.measureText(`${word } `).width;
241
+
242
+ if (currentLineWidth + wordWidth > containerWidth) {
243
+ lines++;
244
+ currentLineWidth = wordWidth;
245
+ } else {
246
+ currentLineWidth += wordWidth;
247
+ }
248
+ }
249
+
250
+ return lines;
251
+ }
252
+
253
+ /**
254
+ * Get optimal configuration for content type
255
+ */
256
+ public static getOptimalConfig(contentType: 'heading' | 'body' | 'caption' | 'button'): Partial<FittingConfig> {
257
+ switch (contentType) {
258
+ case 'heading':
259
+ return {
260
+ mode: 'single-line',
261
+ minSize: 18,
262
+ maxSize: 48,
263
+ precision: 0.5,
264
+ maxIterations: 20,
265
+ lineHeight: 1.2
266
+ };
267
+ case 'body':
268
+ return {
269
+ mode: 'multi-line',
270
+ minSize: 14,
271
+ maxSize: 20,
272
+ precision: 0.25,
273
+ maxIterations: 15,
274
+ lineHeight: 1.5
275
+ };
276
+ case 'caption':
277
+ return {
278
+ mode: 'overflow-aware',
279
+ minSize: 12,
280
+ maxSize: 16,
281
+ precision: 0.25,
282
+ maxIterations: 10,
283
+ lineHeight: 1.3,
284
+ overflow: 'ellipsis'
285
+ };
286
+ case 'button':
287
+ return {
288
+ mode: 'single-line',
289
+ minSize: 14,
290
+ maxSize: 18,
291
+ precision: 0.25,
292
+ maxIterations: 10,
293
+ lineHeight: 1,
294
+ overflow: 'ellipsis'
295
+ };
296
+ default:
297
+ return {};
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Create responsive text fitting
303
+ */
304
+ public createResponsiveFitting(
305
+ element: Element,
306
+ configs: Array<{
307
+ containerSize: number;
308
+ config: FittingConfig;
309
+ }>
310
+ ): void {
311
+ const htmlElement = element as HTMLElement;
312
+ const text = element.textContent || '';
313
+
314
+ // Sort configs by container size
315
+ const sortedConfigs = configs.sort((a, b) => a.containerSize - b.containerSize);
316
+
317
+ // Create resize observer to update fitting
318
+ const resizeObserver = new ResizeObserver((entries) => {
319
+ for (const entry of entries) {
320
+ const containerWidth = entry.contentRect.width;
321
+ const containerHeight = entry.contentRect.height;
322
+
323
+ // Find appropriate config
324
+ let activeConfig = sortedConfigs[0]!.config;
325
+ for (const { containerSize, config } of sortedConfigs) {
326
+ if (containerWidth >= containerSize) {
327
+ activeConfig = config;
328
+ }
329
+ }
330
+
331
+ // Apply fitting
332
+ const result = this.fitText(element, text, containerWidth, containerHeight, activeConfig);
333
+ this.applyFitting(element, result, activeConfig);
334
+ }
335
+ });
336
+
337
+ resizeObserver.observe(element);
338
+
339
+ // Store observer for cleanup
340
+ (htmlElement as any)._proteusTextFittingObserver = resizeObserver;
341
+ }
342
+
343
+ /**
344
+ * Remove responsive text fitting
345
+ */
346
+ public removeResponsiveFitting(element: Element): void {
347
+ const htmlElement = element as HTMLElement;
348
+ const observer = (htmlElement as any)._proteusTextFittingObserver;
349
+
350
+ if (observer) {
351
+ observer.disconnect();
352
+ delete (htmlElement as any)._proteusTextFittingObserver;
353
+ }
354
+ }
355
+ }