@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,227 @@
|
|
1
|
+
/**
|
2
|
+
* Integration tests for ProteusJS
|
3
|
+
* Tests the complete system working together
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
7
|
+
import { ProteusJS } from './core/ProteusJS';
|
8
|
+
|
9
|
+
describe('ProteusJS Integration', () => {
|
10
|
+
let proteus: ProteusJS;
|
11
|
+
let testContainer: HTMLElement;
|
12
|
+
|
13
|
+
beforeEach(() => {
|
14
|
+
// Reset singleton
|
15
|
+
(ProteusJS as any).instance = null;
|
16
|
+
|
17
|
+
// Create test container
|
18
|
+
testContainer = document.createElement('div');
|
19
|
+
testContainer.className = 'test-container';
|
20
|
+
testContainer.style.width = '500px';
|
21
|
+
testContainer.style.height = '300px';
|
22
|
+
document.body.appendChild(testContainer);
|
23
|
+
|
24
|
+
// Initialize ProteusJS
|
25
|
+
proteus = new ProteusJS({
|
26
|
+
debug: true,
|
27
|
+
autoInit: false,
|
28
|
+
containers: {
|
29
|
+
autoDetect: false,
|
30
|
+
breakpoints: {
|
31
|
+
sm: '300px',
|
32
|
+
md: '500px',
|
33
|
+
lg: '800px'
|
34
|
+
}
|
35
|
+
}
|
36
|
+
});
|
37
|
+
});
|
38
|
+
|
39
|
+
afterEach(() => {
|
40
|
+
if (proteus) {
|
41
|
+
proteus.destroy();
|
42
|
+
}
|
43
|
+
if (testContainer && testContainer.parentNode) {
|
44
|
+
testContainer.parentNode.removeChild(testContainer);
|
45
|
+
}
|
46
|
+
});
|
47
|
+
|
48
|
+
describe('System Integration', () => {
|
49
|
+
it('should initialize all systems', () => {
|
50
|
+
proteus.init();
|
51
|
+
|
52
|
+
expect(proteus.isInitialized()).toBe(true);
|
53
|
+
expect(proteus.getEventSystem()).toBeDefined();
|
54
|
+
expect(proteus.getPluginSystem()).toBeDefined();
|
55
|
+
expect(proteus.getPerformanceMonitor()).toBeDefined();
|
56
|
+
expect(proteus.getMemoryManager()).toBeDefined();
|
57
|
+
expect(proteus.getObserverManager()).toBeDefined();
|
58
|
+
expect(proteus.getContainerManager()).toBeDefined();
|
59
|
+
});
|
60
|
+
|
61
|
+
it('should create containers', () => {
|
62
|
+
proteus.init();
|
63
|
+
|
64
|
+
const container = proteus.container(testContainer);
|
65
|
+
expect(container).toBeDefined();
|
66
|
+
|
67
|
+
const containerManager = proteus.getContainerManager();
|
68
|
+
expect(containerManager.getContainer(testContainer)).toBeDefined();
|
69
|
+
});
|
70
|
+
|
71
|
+
it('should apply fluid typography', () => {
|
72
|
+
proteus.init();
|
73
|
+
|
74
|
+
const textElement = document.createElement('p');
|
75
|
+
textElement.textContent = 'Test text';
|
76
|
+
testContainer.appendChild(textElement);
|
77
|
+
|
78
|
+
proteus.fluidType(textElement, {
|
79
|
+
minSize: 16,
|
80
|
+
maxSize: 24,
|
81
|
+
minContainer: 300,
|
82
|
+
maxContainer: 800,
|
83
|
+
unit: 'px',
|
84
|
+
containerUnit: 'cw',
|
85
|
+
curve: 'linear'
|
86
|
+
});
|
87
|
+
|
88
|
+
expect(textElement.style.fontSize).toBeTruthy();
|
89
|
+
});
|
90
|
+
|
91
|
+
it('should generate typographic scales', () => {
|
92
|
+
proteus.init();
|
93
|
+
|
94
|
+
const scale = proteus.createTypeScale({
|
95
|
+
ratio: 1.2,
|
96
|
+
baseSize: 16,
|
97
|
+
baseUnit: 'px',
|
98
|
+
levels: 5,
|
99
|
+
direction: 'both'
|
100
|
+
});
|
101
|
+
|
102
|
+
expect(scale).toBeDefined();
|
103
|
+
expect(scale.levels).toHaveLength(11); // 5 up + 5 down + 1 base
|
104
|
+
expect(scale.cssCustomProperties).toBeDefined();
|
105
|
+
});
|
106
|
+
});
|
107
|
+
|
108
|
+
describe('Performance Monitoring', () => {
|
109
|
+
it('should track performance metrics', () => {
|
110
|
+
proteus.init();
|
111
|
+
|
112
|
+
const performanceMonitor = proteus.getPerformanceMonitor();
|
113
|
+
performanceMonitor.start();
|
114
|
+
|
115
|
+
const metrics = performanceMonitor.getMetrics();
|
116
|
+
expect(metrics).toBeDefined();
|
117
|
+
expect(typeof metrics.frameRate).toBe('number');
|
118
|
+
expect(typeof metrics.memoryUsage).toBe('number');
|
119
|
+
});
|
120
|
+
});
|
121
|
+
|
122
|
+
describe('Memory Management', () => {
|
123
|
+
it('should track and cleanup resources', () => {
|
124
|
+
proteus.init();
|
125
|
+
|
126
|
+
const memoryManager = proteus.getMemoryManager();
|
127
|
+
|
128
|
+
// Register a test resource
|
129
|
+
const resourceId = memoryManager.register({
|
130
|
+
id: 'test-resource',
|
131
|
+
type: 'observer',
|
132
|
+
cleanup: vi.fn(),
|
133
|
+
element: testContainer
|
134
|
+
});
|
135
|
+
|
136
|
+
expect(resourceId).toBe('test-resource');
|
137
|
+
|
138
|
+
const info = memoryManager.getMemoryInfo();
|
139
|
+
expect(info).toHaveProperty('managedResources', 1);
|
140
|
+
|
141
|
+
// Cleanup
|
142
|
+
const cleaned = memoryManager.unregister(resourceId);
|
143
|
+
expect(cleaned).toBe(true);
|
144
|
+
});
|
145
|
+
});
|
146
|
+
|
147
|
+
describe('Container Queries', () => {
|
148
|
+
it('should respond to container size changes', async () => {
|
149
|
+
proteus.init();
|
150
|
+
|
151
|
+
const container = proteus.container(testContainer, {
|
152
|
+
breakpoints: {
|
153
|
+
small: '200px',
|
154
|
+
medium: '400px',
|
155
|
+
large: '600px'
|
156
|
+
}
|
157
|
+
});
|
158
|
+
|
159
|
+
// Simulate size change
|
160
|
+
testContainer.style.width = '450px';
|
161
|
+
|
162
|
+
// Wait for observer to trigger
|
163
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
164
|
+
|
165
|
+
const state = (container as any).getState();
|
166
|
+
expect(state.width).toBe(450);
|
167
|
+
expect(state.activeBreakpoints).toContain('small');
|
168
|
+
expect(state.activeBreakpoints).toContain('medium');
|
169
|
+
});
|
170
|
+
});
|
171
|
+
|
172
|
+
describe('Event System', () => {
|
173
|
+
it('should emit and handle events', () => {
|
174
|
+
proteus.init();
|
175
|
+
|
176
|
+
const eventSystem = proteus.getEventSystem();
|
177
|
+
const mockCallback = vi.fn();
|
178
|
+
|
179
|
+
eventSystem.on('test-event', mockCallback);
|
180
|
+
eventSystem.emit('test-event', { data: 'test' });
|
181
|
+
|
182
|
+
expect(mockCallback).toHaveBeenCalledWith(
|
183
|
+
expect.objectContaining({
|
184
|
+
type: 'test-event',
|
185
|
+
detail: { data: 'test' }
|
186
|
+
})
|
187
|
+
);
|
188
|
+
});
|
189
|
+
});
|
190
|
+
|
191
|
+
describe('Plugin System', () => {
|
192
|
+
it('should register and install plugins', () => {
|
193
|
+
proteus.init();
|
194
|
+
|
195
|
+
const pluginSystem = proteus.getPluginSystem();
|
196
|
+
const mockPlugin = {
|
197
|
+
name: 'test-plugin',
|
198
|
+
version: '1.0.0',
|
199
|
+
install: vi.fn(),
|
200
|
+
uninstall: vi.fn()
|
201
|
+
};
|
202
|
+
|
203
|
+
pluginSystem.register(mockPlugin);
|
204
|
+
pluginSystem.install('test-plugin');
|
205
|
+
|
206
|
+
expect(mockPlugin.install).toHaveBeenCalledWith(proteus);
|
207
|
+
expect(pluginSystem.isInstalled('test-plugin')).toBe(true);
|
208
|
+
});
|
209
|
+
});
|
210
|
+
|
211
|
+
describe('Error Handling', () => {
|
212
|
+
it('should handle initialization errors gracefully', () => {
|
213
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
214
|
+
|
215
|
+
// Mock unsupported browser
|
216
|
+
vi.spyOn(ProteusJS, 'isSupported').mockReturnValue(false);
|
217
|
+
|
218
|
+
proteus.init();
|
219
|
+
|
220
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
221
|
+
'ProteusJS: Browser not supported. Missing required APIs.'
|
222
|
+
);
|
223
|
+
|
224
|
+
consoleSpy.mockRestore();
|
225
|
+
});
|
226
|
+
});
|
227
|
+
});
|
@@ -0,0 +1,429 @@
|
|
1
|
+
/**
|
2
|
+
* Adaptive Grid System for ProteusJS
|
3
|
+
* Container-aware CSS Grid with auto-column calculation and masonry support
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface GridConfig {
|
7
|
+
minColumnWidth: number;
|
8
|
+
maxColumns: number;
|
9
|
+
gap: number | 'fluid';
|
10
|
+
aspectRatio?: number;
|
11
|
+
masonry: boolean;
|
12
|
+
autoFlow: 'row' | 'column' | 'row dense' | 'column dense';
|
13
|
+
alignment: {
|
14
|
+
justify: 'start' | 'end' | 'center' | 'stretch' | 'space-around' | 'space-between' | 'space-evenly';
|
15
|
+
align: 'start' | 'end' | 'center' | 'stretch';
|
16
|
+
};
|
17
|
+
responsive: boolean;
|
18
|
+
breakpoints?: Record<string, Partial<GridConfig>>;
|
19
|
+
}
|
20
|
+
|
21
|
+
export interface GridState {
|
22
|
+
columns: number;
|
23
|
+
rows: number;
|
24
|
+
gap: number;
|
25
|
+
itemWidth: number;
|
26
|
+
itemHeight: number;
|
27
|
+
containerWidth: number;
|
28
|
+
containerHeight: number;
|
29
|
+
}
|
30
|
+
|
31
|
+
export class AdaptiveGrid {
|
32
|
+
private element: Element;
|
33
|
+
private config: Required<GridConfig>;
|
34
|
+
private state: GridState;
|
35
|
+
private resizeObserver: ResizeObserver | null = null;
|
36
|
+
private mutationObserver: MutationObserver | null = null;
|
37
|
+
|
38
|
+
constructor(element: Element, config: Partial<GridConfig> = {}) {
|
39
|
+
this.element = element;
|
40
|
+
this.config = {
|
41
|
+
minColumnWidth: 250,
|
42
|
+
maxColumns: 12,
|
43
|
+
gap: 16,
|
44
|
+
aspectRatio: 1,
|
45
|
+
masonry: false,
|
46
|
+
autoFlow: 'row',
|
47
|
+
alignment: {
|
48
|
+
justify: 'stretch',
|
49
|
+
align: 'stretch'
|
50
|
+
},
|
51
|
+
responsive: true,
|
52
|
+
breakpoints: {},
|
53
|
+
...config
|
54
|
+
};
|
55
|
+
|
56
|
+
this.state = this.createInitialState();
|
57
|
+
this.setupGrid();
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Initialize and activate the grid
|
62
|
+
*/
|
63
|
+
public activate(): void {
|
64
|
+
this.updateGrid();
|
65
|
+
this.setupObservers();
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Deactivate and clean up the grid
|
70
|
+
*/
|
71
|
+
public deactivate(): void {
|
72
|
+
this.cleanupObservers();
|
73
|
+
this.removeGridStyles();
|
74
|
+
}
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Update grid configuration
|
78
|
+
*/
|
79
|
+
public updateConfig(newConfig: Partial<GridConfig>): void {
|
80
|
+
this.config = { ...this.config, ...newConfig };
|
81
|
+
this.updateGrid();
|
82
|
+
}
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Get current grid state
|
86
|
+
*/
|
87
|
+
public getState(): GridState {
|
88
|
+
return { ...this.state };
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Force grid recalculation
|
93
|
+
*/
|
94
|
+
public recalculate(): void {
|
95
|
+
this.updateGrid();
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Add items to the grid
|
100
|
+
*/
|
101
|
+
public addItems(items: Element[]): void {
|
102
|
+
const fragment = document.createDocumentFragment();
|
103
|
+
items.forEach(item => {
|
104
|
+
this.prepareGridItem(item);
|
105
|
+
fragment.appendChild(item);
|
106
|
+
});
|
107
|
+
this.element.appendChild(fragment);
|
108
|
+
this.updateGrid();
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Remove items from the grid
|
113
|
+
*/
|
114
|
+
public removeItems(items: Element[]): void {
|
115
|
+
items.forEach(item => {
|
116
|
+
if (item.parentNode === this.element) {
|
117
|
+
this.element.removeChild(item);
|
118
|
+
}
|
119
|
+
});
|
120
|
+
this.updateGrid();
|
121
|
+
}
|
122
|
+
|
123
|
+
/**
|
124
|
+
* Get optimal column count for container width
|
125
|
+
*/
|
126
|
+
public calculateOptimalColumns(containerWidth: number): number {
|
127
|
+
const availableWidth = containerWidth - this.getGapValue();
|
128
|
+
const minWidth = this.config.minColumnWidth;
|
129
|
+
|
130
|
+
// Calculate maximum possible columns based on min width
|
131
|
+
const maxPossibleColumns = Math.floor((availableWidth + this.getGapValue()) / (minWidth + this.getGapValue()));
|
132
|
+
|
133
|
+
// Respect max columns limit
|
134
|
+
return Math.min(maxPossibleColumns, this.config.maxColumns);
|
135
|
+
}
|
136
|
+
|
137
|
+
/**
|
138
|
+
* Setup initial grid styles
|
139
|
+
*/
|
140
|
+
private setupGrid(): void {
|
141
|
+
const htmlElement = this.element as HTMLElement;
|
142
|
+
htmlElement.style.display = 'grid';
|
143
|
+
|
144
|
+
if (this.config.masonry) {
|
145
|
+
this.setupMasonryGrid();
|
146
|
+
} else {
|
147
|
+
this.setupRegularGrid();
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Setup regular CSS Grid
|
153
|
+
*/
|
154
|
+
private setupRegularGrid(): void {
|
155
|
+
const htmlElement = this.element as HTMLElement;
|
156
|
+
|
157
|
+
htmlElement.style.gridAutoFlow = this.config.autoFlow;
|
158
|
+
htmlElement.style.justifyContent = this.config.alignment.justify;
|
159
|
+
htmlElement.style.alignContent = this.config.alignment.align;
|
160
|
+
htmlElement.style.justifyItems = this.config.alignment.justify;
|
161
|
+
htmlElement.style.alignItems = this.config.alignment.align;
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Setup masonry grid using CSS Grid Level 3 or fallback
|
166
|
+
*/
|
167
|
+
private setupMasonryGrid(): void {
|
168
|
+
const htmlElement = this.element as HTMLElement;
|
169
|
+
|
170
|
+
// Check for native masonry support
|
171
|
+
if (this.supportsMasonry()) {
|
172
|
+
htmlElement.style.gridTemplateRows = 'masonry';
|
173
|
+
} else {
|
174
|
+
// Fallback to JavaScript masonry
|
175
|
+
this.implementJavaScriptMasonry();
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
/**
|
180
|
+
* Update grid layout
|
181
|
+
*/
|
182
|
+
private updateGrid(): void {
|
183
|
+
const containerRect = this.element.getBoundingClientRect();
|
184
|
+
const containerWidth = containerRect.width;
|
185
|
+
const containerHeight = containerRect.height;
|
186
|
+
|
187
|
+
// Apply responsive config if needed
|
188
|
+
const activeConfig = this.getActiveConfig(containerWidth);
|
189
|
+
|
190
|
+
// Calculate optimal columns
|
191
|
+
const columns = this.calculateOptimalColumns(containerWidth);
|
192
|
+
const gap = this.getGapValue();
|
193
|
+
|
194
|
+
// Calculate item dimensions
|
195
|
+
const itemWidth = (containerWidth - gap * (columns - 1)) / columns;
|
196
|
+
const itemHeight = activeConfig.aspectRatio
|
197
|
+
? itemWidth / activeConfig.aspectRatio
|
198
|
+
: 0; // Auto height
|
199
|
+
|
200
|
+
// Update state
|
201
|
+
this.state = {
|
202
|
+
columns,
|
203
|
+
rows: Math.ceil(this.getItemCount() / columns),
|
204
|
+
gap,
|
205
|
+
itemWidth,
|
206
|
+
itemHeight,
|
207
|
+
containerWidth,
|
208
|
+
containerHeight
|
209
|
+
};
|
210
|
+
|
211
|
+
// Apply grid styles
|
212
|
+
this.applyGridStyles();
|
213
|
+
|
214
|
+
// Handle masonry layout
|
215
|
+
if (activeConfig.masonry && !this.supportsMasonry()) {
|
216
|
+
this.updateMasonryLayout();
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
/**
|
221
|
+
* Apply calculated grid styles
|
222
|
+
*/
|
223
|
+
private applyGridStyles(): void {
|
224
|
+
const htmlElement = this.element as HTMLElement;
|
225
|
+
const { columns, gap, itemHeight } = this.state;
|
226
|
+
|
227
|
+
// Set grid template columns
|
228
|
+
htmlElement.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
229
|
+
|
230
|
+
// Set gap
|
231
|
+
htmlElement.style.gap = `${gap}px`;
|
232
|
+
|
233
|
+
// Set row height if aspect ratio is specified
|
234
|
+
if (itemHeight > 0) {
|
235
|
+
htmlElement.style.gridAutoRows = `${itemHeight}px`;
|
236
|
+
} else {
|
237
|
+
htmlElement.style.gridAutoRows = 'auto';
|
238
|
+
}
|
239
|
+
|
240
|
+
// Prepare all grid items
|
241
|
+
Array.from(this.element.children).forEach(child => {
|
242
|
+
this.prepareGridItem(child);
|
243
|
+
});
|
244
|
+
}
|
245
|
+
|
246
|
+
/**
|
247
|
+
* Prepare individual grid item
|
248
|
+
*/
|
249
|
+
private prepareGridItem(item: Element): void {
|
250
|
+
const htmlItem = item as HTMLElement;
|
251
|
+
|
252
|
+
// Ensure proper box-sizing
|
253
|
+
htmlItem.style.boxSizing = 'border-box';
|
254
|
+
|
255
|
+
// Handle aspect ratio for items if specified
|
256
|
+
if (this.config.aspectRatio && !this.config.masonry) {
|
257
|
+
htmlItem.style.aspectRatio = this.config.aspectRatio.toString();
|
258
|
+
}
|
259
|
+
|
260
|
+
// Add grid item class for styling
|
261
|
+
htmlItem.classList.add('proteus-grid-item');
|
262
|
+
}
|
263
|
+
|
264
|
+
/**
|
265
|
+
* Implement JavaScript masonry layout
|
266
|
+
*/
|
267
|
+
private implementJavaScriptMasonry(): void {
|
268
|
+
const items = Array.from(this.element.children) as HTMLElement[];
|
269
|
+
const { columns, gap } = this.state;
|
270
|
+
|
271
|
+
// Create column height trackers
|
272
|
+
const columnHeights = new Array(columns).fill(0);
|
273
|
+
|
274
|
+
items.forEach((item, index) => {
|
275
|
+
// Find shortest column
|
276
|
+
const shortestColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
|
277
|
+
|
278
|
+
// Position item
|
279
|
+
const column = shortestColumnIndex;
|
280
|
+
const row = Math.floor(columnHeights[shortestColumnIndex] / (this.state.itemHeight + gap));
|
281
|
+
|
282
|
+
item.style.gridColumnStart = (column + 1).toString();
|
283
|
+
item.style.gridRowStart = (row + 1).toString();
|
284
|
+
|
285
|
+
// Update column height
|
286
|
+
const itemHeight = item.getBoundingClientRect().height || this.state.itemHeight;
|
287
|
+
columnHeights[shortestColumnIndex] += itemHeight + gap;
|
288
|
+
});
|
289
|
+
}
|
290
|
+
|
291
|
+
/**
|
292
|
+
* Update masonry layout
|
293
|
+
*/
|
294
|
+
private updateMasonryLayout(): void {
|
295
|
+
// Wait for layout to settle, then update masonry
|
296
|
+
requestAnimationFrame(() => {
|
297
|
+
this.implementJavaScriptMasonry();
|
298
|
+
});
|
299
|
+
}
|
300
|
+
|
301
|
+
/**
|
302
|
+
* Setup observers for responsive behavior
|
303
|
+
*/
|
304
|
+
private setupObservers(): void {
|
305
|
+
if (!this.config.responsive) return;
|
306
|
+
|
307
|
+
// Resize observer for container size changes
|
308
|
+
this.resizeObserver = new ResizeObserver(() => {
|
309
|
+
this.updateGrid();
|
310
|
+
});
|
311
|
+
this.resizeObserver.observe(this.element);
|
312
|
+
|
313
|
+
// Mutation observer for content changes
|
314
|
+
this.mutationObserver = new MutationObserver(() => {
|
315
|
+
this.updateGrid();
|
316
|
+
});
|
317
|
+
this.mutationObserver.observe(this.element, {
|
318
|
+
childList: true,
|
319
|
+
subtree: false
|
320
|
+
});
|
321
|
+
}
|
322
|
+
|
323
|
+
/**
|
324
|
+
* Clean up observers
|
325
|
+
*/
|
326
|
+
private cleanupObservers(): void {
|
327
|
+
if (this.resizeObserver) {
|
328
|
+
this.resizeObserver.disconnect();
|
329
|
+
this.resizeObserver = null;
|
330
|
+
}
|
331
|
+
|
332
|
+
if (this.mutationObserver) {
|
333
|
+
this.mutationObserver.disconnect();
|
334
|
+
this.mutationObserver = null;
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
/**
|
339
|
+
* Remove grid styles
|
340
|
+
*/
|
341
|
+
private removeGridStyles(): void {
|
342
|
+
const htmlElement = this.element as HTMLElement;
|
343
|
+
|
344
|
+
// Remove grid styles
|
345
|
+
htmlElement.style.removeProperty('display');
|
346
|
+
htmlElement.style.removeProperty('grid-template-columns');
|
347
|
+
htmlElement.style.removeProperty('grid-template-rows');
|
348
|
+
htmlElement.style.removeProperty('gap');
|
349
|
+
htmlElement.style.removeProperty('grid-auto-flow');
|
350
|
+
htmlElement.style.removeProperty('justify-content');
|
351
|
+
htmlElement.style.removeProperty('align-content');
|
352
|
+
htmlElement.style.removeProperty('justify-items');
|
353
|
+
htmlElement.style.removeProperty('align-items');
|
354
|
+
htmlElement.style.removeProperty('grid-auto-rows');
|
355
|
+
|
356
|
+
// Remove item styles
|
357
|
+
Array.from(this.element.children).forEach(child => {
|
358
|
+
const htmlChild = child as HTMLElement;
|
359
|
+
htmlChild.style.removeProperty('grid-column-start');
|
360
|
+
htmlChild.style.removeProperty('grid-row-start');
|
361
|
+
htmlChild.style.removeProperty('aspect-ratio');
|
362
|
+
htmlChild.classList.remove('proteus-grid-item');
|
363
|
+
});
|
364
|
+
}
|
365
|
+
|
366
|
+
/**
|
367
|
+
* Get active configuration based on container width
|
368
|
+
*/
|
369
|
+
private getActiveConfig(containerWidth: number): Required<GridConfig> {
|
370
|
+
let activeConfig = { ...this.config };
|
371
|
+
|
372
|
+
// Apply breakpoint-specific configs
|
373
|
+
if (this.config.breakpoints) {
|
374
|
+
const sortedBreakpoints = Object.entries(this.config.breakpoints)
|
375
|
+
.map(([name, config]) => ({ name, width: parseInt(name), config }))
|
376
|
+
.sort((a, b) => a.width - b.width);
|
377
|
+
|
378
|
+
for (const breakpoint of sortedBreakpoints) {
|
379
|
+
if (containerWidth >= breakpoint.width) {
|
380
|
+
activeConfig = { ...activeConfig, ...breakpoint.config };
|
381
|
+
}
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
return activeConfig;
|
386
|
+
}
|
387
|
+
|
388
|
+
/**
|
389
|
+
* Get gap value in pixels
|
390
|
+
*/
|
391
|
+
private getGapValue(): number {
|
392
|
+
if (this.config.gap === 'fluid') {
|
393
|
+
// Fluid gap based on container width
|
394
|
+
const containerWidth = this.element.getBoundingClientRect().width;
|
395
|
+
return Math.max(8, Math.min(32, containerWidth * 0.02));
|
396
|
+
}
|
397
|
+
return this.config.gap as number;
|
398
|
+
}
|
399
|
+
|
400
|
+
/**
|
401
|
+
* Get number of grid items
|
402
|
+
*/
|
403
|
+
private getItemCount(): number {
|
404
|
+
return this.element.children.length;
|
405
|
+
}
|
406
|
+
|
407
|
+
/**
|
408
|
+
* Check if native masonry is supported
|
409
|
+
*/
|
410
|
+
private supportsMasonry(): boolean {
|
411
|
+
if (typeof CSS === 'undefined' || !CSS.supports) return false;
|
412
|
+
return CSS.supports('grid-template-rows', 'masonry');
|
413
|
+
}
|
414
|
+
|
415
|
+
/**
|
416
|
+
* Create initial state
|
417
|
+
*/
|
418
|
+
private createInitialState(): GridState {
|
419
|
+
return {
|
420
|
+
columns: 1,
|
421
|
+
rows: 1,
|
422
|
+
gap: 16,
|
423
|
+
itemWidth: 0,
|
424
|
+
itemHeight: 0,
|
425
|
+
containerWidth: 0,
|
426
|
+
containerHeight: 0
|
427
|
+
};
|
428
|
+
}
|
429
|
+
}
|