@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,370 @@
|
|
1
|
+
/**
|
2
|
+
* Container Manager for ProteusJS
|
3
|
+
* Manages multiple SmartContainer instances and provides the main API
|
4
|
+
*/
|
5
|
+
|
6
|
+
import type { ContainerConfig, BreakpointConfig } from '../types';
|
7
|
+
import { SmartContainer, ContainerOptions, ContainerState } from './SmartContainer';
|
8
|
+
import { ObserverManager } from '../observers/ObserverManager';
|
9
|
+
import { MemoryManager } from '../core/MemoryManager';
|
10
|
+
import { EventSystem } from '../core/EventSystem';
|
11
|
+
import { performanceTracker } from '../utils/performance';
|
12
|
+
|
13
|
+
export class ContainerManager {
|
14
|
+
private containers: Map<Element, SmartContainer> = new Map();
|
15
|
+
private observerManager: ObserverManager;
|
16
|
+
private memoryManager: MemoryManager;
|
17
|
+
private eventSystem: EventSystem;
|
18
|
+
private config: Required<ContainerConfig>;
|
19
|
+
private autoDetectionEnabled: boolean = false;
|
20
|
+
|
21
|
+
constructor(
|
22
|
+
config: ContainerConfig,
|
23
|
+
observerManager: ObserverManager,
|
24
|
+
memoryManager: MemoryManager,
|
25
|
+
eventSystem: EventSystem
|
26
|
+
) {
|
27
|
+
this.observerManager = observerManager;
|
28
|
+
this.memoryManager = memoryManager;
|
29
|
+
this.eventSystem = eventSystem;
|
30
|
+
|
31
|
+
this.config = {
|
32
|
+
autoDetect: true,
|
33
|
+
breakpoints: {
|
34
|
+
sm: '300px',
|
35
|
+
md: '500px',
|
36
|
+
lg: '800px',
|
37
|
+
xl: '1200px'
|
38
|
+
},
|
39
|
+
units: true,
|
40
|
+
isolation: true,
|
41
|
+
polyfill: true,
|
42
|
+
...config
|
43
|
+
};
|
44
|
+
|
45
|
+
if (this.config.autoDetect) {
|
46
|
+
this.enableAutoDetection();
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Create and manage a container
|
52
|
+
*/
|
53
|
+
public container(
|
54
|
+
selector: string | Element | Element[],
|
55
|
+
options: ContainerOptions = {}
|
56
|
+
): SmartContainer | SmartContainer[] {
|
57
|
+
performanceTracker.mark('container-create');
|
58
|
+
|
59
|
+
const elements = this.normalizeSelector(selector);
|
60
|
+
const containers: SmartContainer[] = [];
|
61
|
+
|
62
|
+
elements.forEach(element => {
|
63
|
+
let container = this.containers.get(element);
|
64
|
+
|
65
|
+
if (container) {
|
66
|
+
// Update existing container
|
67
|
+
if (options.breakpoints) {
|
68
|
+
container.updateBreakpoints(options.breakpoints);
|
69
|
+
}
|
70
|
+
} else {
|
71
|
+
// Create new container
|
72
|
+
const mergedOptions: ContainerOptions = {
|
73
|
+
breakpoints: this.config.breakpoints,
|
74
|
+
...options
|
75
|
+
};
|
76
|
+
|
77
|
+
container = new SmartContainer(
|
78
|
+
element,
|
79
|
+
mergedOptions,
|
80
|
+
this.observerManager,
|
81
|
+
this.memoryManager
|
82
|
+
);
|
83
|
+
|
84
|
+
this.containers.set(element, container);
|
85
|
+
container.activate();
|
86
|
+
|
87
|
+
// Emit container created event
|
88
|
+
this.eventSystem.emit('containerCreated', {
|
89
|
+
element,
|
90
|
+
container,
|
91
|
+
options: mergedOptions
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
containers.push(container);
|
96
|
+
});
|
97
|
+
|
98
|
+
performanceTracker.measure('container-create');
|
99
|
+
|
100
|
+
return elements.length === 1 ? containers[0]! : containers;
|
101
|
+
}
|
102
|
+
|
103
|
+
/**
|
104
|
+
* Remove container management from element(s)
|
105
|
+
*/
|
106
|
+
public removeContainer(selector: string | Element | Element[]): boolean {
|
107
|
+
const elements = this.normalizeSelector(selector);
|
108
|
+
let removed = false;
|
109
|
+
|
110
|
+
elements.forEach(element => {
|
111
|
+
const container = this.containers.get(element);
|
112
|
+
if (container) {
|
113
|
+
container.deactivate();
|
114
|
+
this.containers.delete(element);
|
115
|
+
removed = true;
|
116
|
+
|
117
|
+
// Emit container removed event
|
118
|
+
this.eventSystem.emit('containerRemoved', {
|
119
|
+
element,
|
120
|
+
container
|
121
|
+
});
|
122
|
+
}
|
123
|
+
});
|
124
|
+
|
125
|
+
return removed;
|
126
|
+
}
|
127
|
+
|
128
|
+
/**
|
129
|
+
* Get container instance for element
|
130
|
+
*/
|
131
|
+
public getContainer(element: Element): SmartContainer | undefined {
|
132
|
+
return this.containers.get(element);
|
133
|
+
}
|
134
|
+
|
135
|
+
/**
|
136
|
+
* Get all managed containers
|
137
|
+
*/
|
138
|
+
public getAllContainers(): SmartContainer[] {
|
139
|
+
return Array.from(this.containers.values());
|
140
|
+
}
|
141
|
+
|
142
|
+
/**
|
143
|
+
* Get containers by breakpoint
|
144
|
+
*/
|
145
|
+
public getContainersByBreakpoint(breakpoint: string): SmartContainer[] {
|
146
|
+
return this.getAllContainers().filter(container =>
|
147
|
+
container.isBreakpointActive(breakpoint)
|
148
|
+
);
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Update global breakpoints
|
153
|
+
*/
|
154
|
+
public updateGlobalBreakpoints(breakpoints: BreakpointConfig): void {
|
155
|
+
const stringBreakpoints = Object.fromEntries(
|
156
|
+
Object.entries(breakpoints).map(([key, value]) => [key, String(value)])
|
157
|
+
);
|
158
|
+
this.config.breakpoints = { ...this.config.breakpoints, ...stringBreakpoints };
|
159
|
+
|
160
|
+
// Update all existing containers
|
161
|
+
this.containers.forEach(container => {
|
162
|
+
container.updateBreakpoints(this.config.breakpoints);
|
163
|
+
});
|
164
|
+
|
165
|
+
// Emit breakpoints updated event
|
166
|
+
this.eventSystem.emit('breakpointsUpdated', {
|
167
|
+
breakpoints: this.config.breakpoints
|
168
|
+
});
|
169
|
+
}
|
170
|
+
|
171
|
+
/**
|
172
|
+
* Enable automatic container detection
|
173
|
+
*/
|
174
|
+
public enableAutoDetection(): void {
|
175
|
+
if (this.autoDetectionEnabled) return;
|
176
|
+
|
177
|
+
this.autoDetectionEnabled = true;
|
178
|
+
this.scanForContainers();
|
179
|
+
|
180
|
+
// Set up mutation observer for new elements
|
181
|
+
this.setupMutationObserver();
|
182
|
+
}
|
183
|
+
|
184
|
+
/**
|
185
|
+
* Disable automatic container detection
|
186
|
+
*/
|
187
|
+
public disableAutoDetection(): void {
|
188
|
+
this.autoDetectionEnabled = false;
|
189
|
+
// Note: We don't remove existing containers, just stop auto-detection
|
190
|
+
}
|
191
|
+
|
192
|
+
/**
|
193
|
+
* Manually scan for containers
|
194
|
+
*/
|
195
|
+
public scanForContainers(): void {
|
196
|
+
if (!this.autoDetectionEnabled) return;
|
197
|
+
|
198
|
+
performanceTracker.mark('container-scan');
|
199
|
+
|
200
|
+
// Look for elements with container-related attributes or classes
|
201
|
+
const candidates = this.findContainerCandidates();
|
202
|
+
|
203
|
+
candidates.forEach(element => {
|
204
|
+
if (!this.containers.has(element)) {
|
205
|
+
const options = this.extractOptionsFromElement(element);
|
206
|
+
this.container(element, options);
|
207
|
+
}
|
208
|
+
});
|
209
|
+
|
210
|
+
performanceTracker.measure('container-scan');
|
211
|
+
}
|
212
|
+
|
213
|
+
/**
|
214
|
+
* Get container statistics
|
215
|
+
*/
|
216
|
+
public getStats(): object {
|
217
|
+
const containers = this.getAllContainers();
|
218
|
+
const stats = {
|
219
|
+
totalContainers: containers.length,
|
220
|
+
activeContainers: containers.filter(c => c.getState().lastUpdate > 0).length,
|
221
|
+
autoDetectionEnabled: this.autoDetectionEnabled,
|
222
|
+
breakpoints: Object.keys(this.config.breakpoints),
|
223
|
+
containersByBreakpoint: {} as Record<string, number>
|
224
|
+
};
|
225
|
+
|
226
|
+
// Count containers by active breakpoints
|
227
|
+
Object.keys(this.config.breakpoints).forEach(bp => {
|
228
|
+
stats.containersByBreakpoint[bp] = this.getContainersByBreakpoint(bp).length;
|
229
|
+
});
|
230
|
+
|
231
|
+
return stats;
|
232
|
+
}
|
233
|
+
|
234
|
+
/**
|
235
|
+
* Destroy all containers and clean up
|
236
|
+
*/
|
237
|
+
public destroy(): void {
|
238
|
+
// Deactivate all containers
|
239
|
+
this.containers.forEach(container => {
|
240
|
+
container.deactivate();
|
241
|
+
});
|
242
|
+
|
243
|
+
this.containers.clear();
|
244
|
+
this.autoDetectionEnabled = false;
|
245
|
+
}
|
246
|
+
|
247
|
+
/**
|
248
|
+
* Normalize selector to array of elements
|
249
|
+
*/
|
250
|
+
private normalizeSelector(selector: string | Element | Element[]): Element[] {
|
251
|
+
if (typeof selector === 'string') {
|
252
|
+
return Array.from(document.querySelectorAll(selector));
|
253
|
+
} else if (selector instanceof Element) {
|
254
|
+
return [selector];
|
255
|
+
} else if (Array.isArray(selector)) {
|
256
|
+
return selector;
|
257
|
+
}
|
258
|
+
return [];
|
259
|
+
}
|
260
|
+
|
261
|
+
/**
|
262
|
+
* Find potential container elements
|
263
|
+
*/
|
264
|
+
private findContainerCandidates(): Element[] {
|
265
|
+
const candidates: Element[] = [];
|
266
|
+
|
267
|
+
// Elements with data-container attribute
|
268
|
+
candidates.push(...Array.from(document.querySelectorAll('[data-container]')));
|
269
|
+
|
270
|
+
// Elements with container-related classes
|
271
|
+
candidates.push(...Array.from(document.querySelectorAll('.container, .card, .widget, .component')));
|
272
|
+
|
273
|
+
// Elements with CSS container-type
|
274
|
+
if (typeof CSS !== 'undefined' && CSS.supports && CSS.supports('container-type', 'inline-size')) {
|
275
|
+
const allElements = document.querySelectorAll('*');
|
276
|
+
allElements.forEach(element => {
|
277
|
+
const style = getComputedStyle(element);
|
278
|
+
if (style.containerType && style.containerType !== 'normal') {
|
279
|
+
candidates.push(element);
|
280
|
+
}
|
281
|
+
});
|
282
|
+
}
|
283
|
+
|
284
|
+
// Remove duplicates
|
285
|
+
return Array.from(new Set(candidates));
|
286
|
+
}
|
287
|
+
|
288
|
+
/**
|
289
|
+
* Extract container options from element attributes
|
290
|
+
*/
|
291
|
+
private extractOptionsFromElement(element: Element): ContainerOptions {
|
292
|
+
const options: ContainerOptions = {};
|
293
|
+
|
294
|
+
// Extract from data attributes
|
295
|
+
const containerData = element.getAttribute('data-container');
|
296
|
+
if (containerData) {
|
297
|
+
try {
|
298
|
+
const parsed = JSON.parse(containerData);
|
299
|
+
Object.assign(options, parsed);
|
300
|
+
} catch (error) {
|
301
|
+
// Ignore invalid JSON
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
// Extract breakpoints from data-breakpoints
|
306
|
+
const breakpointsData = element.getAttribute('data-breakpoints');
|
307
|
+
if (breakpointsData) {
|
308
|
+
try {
|
309
|
+
options.breakpoints = JSON.parse(breakpointsData);
|
310
|
+
} catch (error) {
|
311
|
+
// Ignore invalid JSON
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
// Extract container type from CSS or data attribute
|
316
|
+
const containerType = element.getAttribute('data-container-type');
|
317
|
+
if (containerType) {
|
318
|
+
options.containerType = containerType as any;
|
319
|
+
}
|
320
|
+
|
321
|
+
return options;
|
322
|
+
}
|
323
|
+
|
324
|
+
/**
|
325
|
+
* Set up mutation observer for auto-detection
|
326
|
+
*/
|
327
|
+
private setupMutationObserver(): void {
|
328
|
+
if (typeof MutationObserver === 'undefined') return;
|
329
|
+
|
330
|
+
const observer = new MutationObserver((mutations) => {
|
331
|
+
let shouldScan = false;
|
332
|
+
|
333
|
+
mutations.forEach(mutation => {
|
334
|
+
if (mutation.type === 'childList') {
|
335
|
+
mutation.addedNodes.forEach(node => {
|
336
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
337
|
+
shouldScan = true;
|
338
|
+
}
|
339
|
+
});
|
340
|
+
} else if (mutation.type === 'attributes') {
|
341
|
+
if (
|
342
|
+
mutation.attributeName?.startsWith('data-container') ||
|
343
|
+
mutation.attributeName === 'class'
|
344
|
+
) {
|
345
|
+
shouldScan = true;
|
346
|
+
}
|
347
|
+
}
|
348
|
+
});
|
349
|
+
|
350
|
+
if (shouldScan) {
|
351
|
+
// Debounce scanning to avoid excessive calls
|
352
|
+
setTimeout(() => this.scanForContainers(), 100);
|
353
|
+
}
|
354
|
+
});
|
355
|
+
|
356
|
+
observer.observe(document.body, {
|
357
|
+
childList: true,
|
358
|
+
subtree: true,
|
359
|
+
attributes: true,
|
360
|
+
attributeFilter: ['data-container', 'data-breakpoints', 'data-container-type', 'class']
|
361
|
+
});
|
362
|
+
|
363
|
+
// Register cleanup
|
364
|
+
this.memoryManager.register({
|
365
|
+
id: 'container-mutation-observer',
|
366
|
+
type: 'observer',
|
367
|
+
cleanup: () => observer.disconnect()
|
368
|
+
});
|
369
|
+
}
|
370
|
+
}
|
@@ -0,0 +1,336 @@
|
|
1
|
+
/**
|
2
|
+
* Container Units System for ProteusJS
|
3
|
+
* Implements CSS-like container units (cw, ch, cmin, cmax, cqi, cqb)
|
4
|
+
*/
|
5
|
+
|
6
|
+
import type { ContainerUnit } from '../types';
|
7
|
+
|
8
|
+
export interface ContainerDimensions {
|
9
|
+
width: number;
|
10
|
+
height: number;
|
11
|
+
inlineSize: number;
|
12
|
+
blockSize: number;
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface UnitCalculation {
|
16
|
+
value: number;
|
17
|
+
unit: ContainerUnit;
|
18
|
+
pixelValue: number;
|
19
|
+
containerDimensions: ContainerDimensions;
|
20
|
+
}
|
21
|
+
|
22
|
+
export class ContainerUnits {
|
23
|
+
private containerDimensions: Map<Element, ContainerDimensions> = new Map();
|
24
|
+
private unitProperties: Map<Element, Set<string>> = new Map();
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Update container dimensions for an element
|
28
|
+
*/
|
29
|
+
public updateDimensions(element: Element, dimensions: ContainerDimensions): void {
|
30
|
+
this.containerDimensions.set(element, dimensions);
|
31
|
+
this.updateUnitProperties(element);
|
32
|
+
}
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Calculate container unit value in pixels
|
36
|
+
*/
|
37
|
+
public calculateUnit(
|
38
|
+
element: Element,
|
39
|
+
value: number,
|
40
|
+
unit: ContainerUnit
|
41
|
+
): number {
|
42
|
+
const dimensions = this.containerDimensions.get(element);
|
43
|
+
if (!dimensions) {
|
44
|
+
console.warn('ProteusJS: No container dimensions found for element');
|
45
|
+
return 0;
|
46
|
+
}
|
47
|
+
|
48
|
+
switch (unit) {
|
49
|
+
case 'cw':
|
50
|
+
return (value / 100) * dimensions.width;
|
51
|
+
case 'ch':
|
52
|
+
return (value / 100) * dimensions.height;
|
53
|
+
case 'cmin':
|
54
|
+
return (value / 100) * Math.min(dimensions.width, dimensions.height);
|
55
|
+
case 'cmax':
|
56
|
+
return (value / 100) * Math.max(dimensions.width, dimensions.height);
|
57
|
+
case 'cqi':
|
58
|
+
return (value / 100) * dimensions.inlineSize;
|
59
|
+
case 'cqb':
|
60
|
+
return (value / 100) * dimensions.blockSize;
|
61
|
+
default:
|
62
|
+
return 0;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Parse container unit string (e.g., "50cw", "25ch")
|
68
|
+
*/
|
69
|
+
public parseUnit(unitString: string): { value: number; unit: ContainerUnit } | null {
|
70
|
+
const match = unitString.match(/^(\d*\.?\d+)(cw|ch|cmin|cmax|cqi|cqb)$/);
|
71
|
+
if (!match) return null;
|
72
|
+
|
73
|
+
return {
|
74
|
+
value: parseFloat(match[1]!),
|
75
|
+
unit: match[2]! as ContainerUnit
|
76
|
+
};
|
77
|
+
}
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Convert container unit to pixels for specific element
|
81
|
+
*/
|
82
|
+
public toPixels(element: Element, unitString: string): number {
|
83
|
+
const parsed = this.parseUnit(unitString);
|
84
|
+
if (!parsed) return 0;
|
85
|
+
|
86
|
+
return this.calculateUnit(element, parsed.value, parsed.unit);
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Apply container units to element styles
|
91
|
+
*/
|
92
|
+
public applyUnits(element: Element, styles: Record<string, string>): void {
|
93
|
+
const htmlElement = element as HTMLElement;
|
94
|
+
const appliedProperties = new Set<string>();
|
95
|
+
|
96
|
+
Object.entries(styles).forEach(([property, value]) => {
|
97
|
+
const convertedValue = this.convertUnitsInValue(element, value);
|
98
|
+
if (convertedValue !== value) {
|
99
|
+
htmlElement.style.setProperty(property, convertedValue);
|
100
|
+
appliedProperties.add(property);
|
101
|
+
}
|
102
|
+
});
|
103
|
+
|
104
|
+
// Track applied properties for cleanup
|
105
|
+
if (appliedProperties.size > 0) {
|
106
|
+
if (!this.unitProperties.has(element)) {
|
107
|
+
this.unitProperties.set(element, new Set());
|
108
|
+
}
|
109
|
+
appliedProperties.forEach(prop => {
|
110
|
+
this.unitProperties.get(element)!.add(prop);
|
111
|
+
});
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Remove container unit styles from element
|
117
|
+
*/
|
118
|
+
public removeUnits(element: Element): void {
|
119
|
+
const htmlElement = element as HTMLElement;
|
120
|
+
const properties = this.unitProperties.get(element);
|
121
|
+
|
122
|
+
if (properties) {
|
123
|
+
properties.forEach(property => {
|
124
|
+
htmlElement.style.removeProperty(property);
|
125
|
+
});
|
126
|
+
this.unitProperties.delete(element);
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Generate CSS custom properties for container units
|
132
|
+
*/
|
133
|
+
public generateCustomProperties(element: Element): Record<string, string> {
|
134
|
+
const dimensions = this.containerDimensions.get(element);
|
135
|
+
if (!dimensions) return {};
|
136
|
+
|
137
|
+
return {
|
138
|
+
'--cw': `${dimensions.width / 100}px`,
|
139
|
+
'--ch': `${dimensions.height / 100}px`,
|
140
|
+
'--cmin': `${Math.min(dimensions.width, dimensions.height) / 100}px`,
|
141
|
+
'--cmax': `${Math.max(dimensions.width, dimensions.height) / 100}px`,
|
142
|
+
'--cqi': `${dimensions.inlineSize / 100}px`,
|
143
|
+
'--cqb': `${dimensions.blockSize / 100}px`
|
144
|
+
};
|
145
|
+
}
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Apply custom properties to element
|
149
|
+
*/
|
150
|
+
public applyCustomProperties(element: Element): void {
|
151
|
+
const htmlElement = element as HTMLElement;
|
152
|
+
const properties = this.generateCustomProperties(element);
|
153
|
+
|
154
|
+
Object.entries(properties).forEach(([property, value]) => {
|
155
|
+
htmlElement.style.setProperty(property, value);
|
156
|
+
});
|
157
|
+
}
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Create responsive value using container units
|
161
|
+
*/
|
162
|
+
public createResponsiveValue(
|
163
|
+
baseValue: number,
|
164
|
+
unit: ContainerUnit,
|
165
|
+
options: {
|
166
|
+
min?: number;
|
167
|
+
max?: number;
|
168
|
+
scale?: number;
|
169
|
+
} = {}
|
170
|
+
): string {
|
171
|
+
const { min, max, scale = 1 } = options;
|
172
|
+
const scaledValue = baseValue * scale;
|
173
|
+
|
174
|
+
if (min !== undefined && max !== undefined) {
|
175
|
+
return `clamp(${min}px, ${scaledValue}${unit}, ${max}px)`;
|
176
|
+
} else if (min !== undefined) {
|
177
|
+
return `max(${min}px, ${scaledValue}${unit})`;
|
178
|
+
} else if (max !== undefined) {
|
179
|
+
return `min(${scaledValue}${unit}, ${max}px)`;
|
180
|
+
}
|
181
|
+
|
182
|
+
return `${scaledValue}${unit}`;
|
183
|
+
}
|
184
|
+
|
185
|
+
/**
|
186
|
+
* Get container unit statistics
|
187
|
+
*/
|
188
|
+
public getStats(): object {
|
189
|
+
return {
|
190
|
+
trackedContainers: this.containerDimensions.size,
|
191
|
+
elementsWithUnits: this.unitProperties.size,
|
192
|
+
totalUnitProperties: Array.from(this.unitProperties.values())
|
193
|
+
.reduce((sum, props) => sum + props.size, 0)
|
194
|
+
};
|
195
|
+
}
|
196
|
+
|
197
|
+
/**
|
198
|
+
* Clean up container data
|
199
|
+
*/
|
200
|
+
public cleanup(element: Element): void {
|
201
|
+
this.removeUnits(element);
|
202
|
+
this.containerDimensions.delete(element);
|
203
|
+
}
|
204
|
+
|
205
|
+
/**
|
206
|
+
* Clear all data
|
207
|
+
*/
|
208
|
+
public clear(): void {
|
209
|
+
// Remove all unit styles
|
210
|
+
this.unitProperties.forEach((_properties, element) => {
|
211
|
+
this.removeUnits(element);
|
212
|
+
});
|
213
|
+
|
214
|
+
this.containerDimensions.clear();
|
215
|
+
this.unitProperties.clear();
|
216
|
+
}
|
217
|
+
|
218
|
+
/**
|
219
|
+
* Convert container units in a CSS value string
|
220
|
+
*/
|
221
|
+
private convertUnitsInValue(element: Element, value: string): string {
|
222
|
+
// Match container unit patterns in the value
|
223
|
+
const unitRegex = /(\d*\.?\d+)(cw|ch|cmin|cmax|cqi|cqb)/g;
|
224
|
+
|
225
|
+
return value.replace(unitRegex, (_match, numStr, unit) => {
|
226
|
+
const num = parseFloat(numStr);
|
227
|
+
const pixels = this.calculateUnit(element, num, unit as ContainerUnit);
|
228
|
+
return `${pixels}px`;
|
229
|
+
});
|
230
|
+
}
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Update unit properties when dimensions change
|
234
|
+
*/
|
235
|
+
private updateUnitProperties(element: Element): void {
|
236
|
+
const properties = this.unitProperties.get(element);
|
237
|
+
if (!properties) return;
|
238
|
+
|
239
|
+
const htmlElement = element as HTMLElement;
|
240
|
+
// const computedStyle = getComputedStyle(htmlElement); // Currently unused
|
241
|
+
|
242
|
+
// Re-apply container unit styles with new dimensions
|
243
|
+
properties.forEach(property => {
|
244
|
+
const currentValue = htmlElement.style.getPropertyValue(property);
|
245
|
+
if (currentValue && this.hasContainerUnits(currentValue)) {
|
246
|
+
const convertedValue = this.convertUnitsInValue(element, currentValue);
|
247
|
+
htmlElement.style.setProperty(property, convertedValue);
|
248
|
+
}
|
249
|
+
});
|
250
|
+
|
251
|
+
// Update custom properties
|
252
|
+
this.applyCustomProperties(element);
|
253
|
+
}
|
254
|
+
|
255
|
+
/**
|
256
|
+
* Check if value contains container units
|
257
|
+
*/
|
258
|
+
private hasContainerUnits(value: string): boolean {
|
259
|
+
return /\d+(cw|ch|cmin|cmax|cqi|cqb)/.test(value);
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Utility functions for container units
|
265
|
+
*/
|
266
|
+
export const containerUnitUtils = {
|
267
|
+
/**
|
268
|
+
* Create fluid typography using container units
|
269
|
+
*/
|
270
|
+
createFluidType(
|
271
|
+
minSize: number,
|
272
|
+
maxSize: number,
|
273
|
+
minContainer: number,
|
274
|
+
maxContainer: number,
|
275
|
+
unit: ContainerUnit = 'cw'
|
276
|
+
): string {
|
277
|
+
const slope = (maxSize - minSize) / (maxContainer - minContainer);
|
278
|
+
const yIntercept = minSize - slope * minContainer;
|
279
|
+
|
280
|
+
return `clamp(${minSize}px, ${yIntercept}px + ${slope * 100}${unit}, ${maxSize}px)`;
|
281
|
+
},
|
282
|
+
|
283
|
+
/**
|
284
|
+
* Create responsive spacing using container units
|
285
|
+
*/
|
286
|
+
createFluidSpacing(
|
287
|
+
baseSpacing: number,
|
288
|
+
scale: number = 1,
|
289
|
+
unit: ContainerUnit = 'cw'
|
290
|
+
): string {
|
291
|
+
const scaledValue = baseSpacing * scale;
|
292
|
+
return `${scaledValue}${unit}`;
|
293
|
+
},
|
294
|
+
|
295
|
+
/**
|
296
|
+
* Convert viewport units to container units
|
297
|
+
*/
|
298
|
+
convertViewportToContainer(
|
299
|
+
value: string,
|
300
|
+
containerWidth: number,
|
301
|
+
containerHeight: number,
|
302
|
+
viewportWidth: number = window.innerWidth,
|
303
|
+
viewportHeight: number = window.innerHeight
|
304
|
+
): string {
|
305
|
+
return value.replace(/(\d*\.?\d+)(vw|vh|vmin|vmax)/g, (match, numStr, unit) => {
|
306
|
+
const num = parseFloat(numStr);
|
307
|
+
let containerValue: number;
|
308
|
+
let containerUnit: ContainerUnit;
|
309
|
+
|
310
|
+
switch (unit) {
|
311
|
+
case 'vw':
|
312
|
+
containerValue = (num * viewportWidth) / containerWidth * 100;
|
313
|
+
containerUnit = 'cw';
|
314
|
+
break;
|
315
|
+
case 'vh':
|
316
|
+
containerValue = (num * viewportHeight) / containerHeight * 100;
|
317
|
+
containerUnit = 'ch';
|
318
|
+
break;
|
319
|
+
case 'vmin':
|
320
|
+
const vminPixels = (num / 100) * Math.min(viewportWidth, viewportHeight);
|
321
|
+
containerValue = (vminPixels / Math.min(containerWidth, containerHeight)) * 100;
|
322
|
+
containerUnit = 'cmin';
|
323
|
+
break;
|
324
|
+
case 'vmax':
|
325
|
+
const vmaxPixels = (num / 100) * Math.max(viewportWidth, viewportHeight);
|
326
|
+
containerValue = (vmaxPixels / Math.max(containerWidth, containerHeight)) * 100;
|
327
|
+
containerUnit = 'cmax';
|
328
|
+
break;
|
329
|
+
default:
|
330
|
+
return match;
|
331
|
+
}
|
332
|
+
|
333
|
+
return `${containerValue.toFixed(2)}${containerUnit}`;
|
334
|
+
});
|
335
|
+
}
|
336
|
+
};
|