climaybe 1.7.3 → 1.8.1
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/README.md +8 -6
- package/bin/version.txt +1 -1
- package/package.json +1 -1
- package/src/commands/add-cursor-skill.js +12 -7
- package/src/commands/init.js +10 -5
- package/src/cursor/rules/00-rule-index.mdc +24 -0
- package/src/cursor/rules/accessibility-rules.mdc +527 -0
- package/src/cursor/rules/commit-rules.mdc +286 -0
- package/src/cursor/rules/cursor-rule-template.mdc +66 -0
- package/src/cursor/rules/examples/section-example.liquid +52 -0
- package/src/cursor/rules/examples/snippet-example.liquid +83 -0
- package/src/cursor/rules/figma-design-system.mdc +182 -0
- package/src/cursor/rules/global-rules-reference.mdc +62 -0
- package/src/cursor/rules/javascript-standards.mdc +1125 -0
- package/src/cursor/rules/js-refactor-tasks.mdc +123 -0
- package/src/cursor/rules/linear-task-creation.mdc +105 -0
- package/src/cursor/rules/liquid-doc-rules.mdc +595 -0
- package/src/cursor/rules/liquid.mdc +228 -0
- package/src/cursor/rules/project-overview.mdc +81 -0
- package/src/cursor/rules/schemas.mdc +150 -0
- package/src/cursor/rules/sections.mdc +25 -0
- package/src/cursor/rules/snippets.mdc +134 -0
- package/src/cursor/rules/tailwindcss-rules.mdc +410 -0
- package/src/cursor/skills/accessibility-pass/SKILL.md +54 -0
- package/src/cursor/skills/changelog-release/SKILL.md +50 -0
- package/src/cursor/skills/commit/SKILL.md +27 -0
- package/src/cursor/skills/commit-in-groups/SKILL.md +55 -0
- package/src/cursor/skills/linear-create-task/SKILL.md +81 -0
- package/src/cursor/skills/liquid-doc-comments/SKILL.md +37 -0
- package/src/cursor/skills/locale-translation-prep/SKILL.md +49 -0
- package/src/cursor/skills/schema-section-change/SKILL.md +39 -0
- package/src/cursor/skills/section-from-spec/SKILL.md +39 -0
- package/src/cursor/skills/theme-check-fix/SKILL.md +47 -0
- package/src/index.js +3 -2
- package/src/lib/commit-tooling.js +0 -44
- package/src/lib/config.js +1 -1
- package/src/lib/cursor-bundle.js +47 -0
- package/src/lib/prompts.js +3 -2
- package/src/workflows/build/reusable-build.yml +1 -1
- package/src/workflows/shared/version-bump.yml +5 -2
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Professional JavaScript standards for Electric Maybe Shopify Theme. All JavaScript files must follow these patterns for consistency, performance, and maintainability.
|
|
3
|
+
globs:
|
|
4
|
+
- "**/*.js"
|
|
5
|
+
- "sections/*.liquid"
|
|
6
|
+
- "snippets/*.liquid"
|
|
7
|
+
alwaysApply: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# JavaScript Development Standards
|
|
11
|
+
|
|
12
|
+
## Core Principles
|
|
13
|
+
|
|
14
|
+
- **Zero external dependencies** - Use native browser APIs
|
|
15
|
+
- **Performance first** - Every operation should complete in < 16ms
|
|
16
|
+
- **Accessibility always** - WCAG 2.1 AA compliance minimum
|
|
17
|
+
- **Memory conscious** - Proper cleanup, no leaks
|
|
18
|
+
- **Error resilient** - Graceful degradation with try-catch boundaries
|
|
19
|
+
- **Documentation complete** - Every class, method, and complex operation documented
|
|
20
|
+
|
|
21
|
+
## Web Component Standards
|
|
22
|
+
|
|
23
|
+
### Class Structure Template
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
/**
|
|
27
|
+
* ComponentName - Brief description of component purpose
|
|
28
|
+
*
|
|
29
|
+
* Detailed description of functionality and usage patterns.
|
|
30
|
+
*
|
|
31
|
+
* @example Basic usage
|
|
32
|
+
* <component-name data-option="value">
|
|
33
|
+
* <div slot="content">Content here</div>
|
|
34
|
+
* </component-name>
|
|
35
|
+
*
|
|
36
|
+
* @example Programmatic usage
|
|
37
|
+
* const component = document.querySelector('component-name');
|
|
38
|
+
* component.methodName(data);
|
|
39
|
+
*/
|
|
40
|
+
class ComponentName extends HTMLElement {
|
|
41
|
+
// Private fields - ALWAYS use # syntax
|
|
42
|
+
#isConnected = false;
|
|
43
|
+
#observer = null;
|
|
44
|
+
#abortController = null;
|
|
45
|
+
#cache = new Map();
|
|
46
|
+
#state = {
|
|
47
|
+
isLoading: false,
|
|
48
|
+
hasError: false
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Constants
|
|
52
|
+
static #ANIMATION_DURATION = 300;
|
|
53
|
+
static #DEBOUNCE_DELAY = 150;
|
|
54
|
+
|
|
55
|
+
// Static observed attributes
|
|
56
|
+
static observedAttributes = ['data-option', 'disabled'];
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
super();
|
|
60
|
+
|
|
61
|
+
// Bind event handlers in constructor
|
|
62
|
+
this.#handleClick = this.#handleClick.bind(this);
|
|
63
|
+
this.#handleScroll = this.#handleScroll.bind(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
// Prevent duplicate initialization
|
|
68
|
+
if (this.#isConnected) return;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
this.#setupDOM();
|
|
72
|
+
this.#setupEventListeners();
|
|
73
|
+
this.#initialize();
|
|
74
|
+
this.#isConnected = true;
|
|
75
|
+
|
|
76
|
+
// Dispatch ready event
|
|
77
|
+
this.dispatchEvent(new CustomEvent('component:ready', {
|
|
78
|
+
detail: { component: this },
|
|
79
|
+
bubbles: true
|
|
80
|
+
}));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('ComponentName: Error in connectedCallback', error);
|
|
83
|
+
this.#handleError(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
disconnectedCallback() {
|
|
88
|
+
if (!this.#isConnected) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
this.#cleanup();
|
|
92
|
+
this.#isConnected = false;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('ComponentName: Error in disconnectedCallback', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
99
|
+
if (oldValue === newValue) return;
|
|
100
|
+
|
|
101
|
+
switch(name) {
|
|
102
|
+
case 'data-option':
|
|
103
|
+
this.#handleOptionChange(newValue);
|
|
104
|
+
break;
|
|
105
|
+
case 'disabled':
|
|
106
|
+
this.#handleDisabledChange(newValue !== null);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Public API methods
|
|
112
|
+
/**
|
|
113
|
+
* Public method description
|
|
114
|
+
* @param {Object} data - Parameter description
|
|
115
|
+
* @returns {Promise<void>}
|
|
116
|
+
* @throws {Error} When validation fails
|
|
117
|
+
*/
|
|
118
|
+
async publicMethod(data) {
|
|
119
|
+
if (!this.#validateData(data)) {
|
|
120
|
+
throw new Error('Invalid data provided');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await this.#performAction(data);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('ComponentName: Error in publicMethod', error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Private methods - ALWAYS use # syntax
|
|
132
|
+
#setupDOM() {
|
|
133
|
+
// Cache DOM references
|
|
134
|
+
this.#elements = {
|
|
135
|
+
button: this.querySelector('[data-action]'),
|
|
136
|
+
content: this.querySelector('[data-content]')
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#setupEventListeners() {
|
|
141
|
+
// Use AbortController for cleanup
|
|
142
|
+
this.#abortController = new AbortController();
|
|
143
|
+
const { signal } = this.#abortController;
|
|
144
|
+
|
|
145
|
+
// Passive listeners for scroll/touch
|
|
146
|
+
window.addEventListener('scroll', this.#handleScroll, {
|
|
147
|
+
passive: true,
|
|
148
|
+
signal
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Regular listeners
|
|
152
|
+
this.addEventListener('click', this.#handleClick, { signal });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#cleanup() {
|
|
156
|
+
// Abort all listeners
|
|
157
|
+
this.#abortController?.abort();
|
|
158
|
+
|
|
159
|
+
// Clear observers
|
|
160
|
+
this.#observer?.disconnect();
|
|
161
|
+
|
|
162
|
+
// Clear timeouts
|
|
163
|
+
clearTimeout(this.#timeout);
|
|
164
|
+
|
|
165
|
+
// Clear cache
|
|
166
|
+
this.#cache.clear();
|
|
167
|
+
|
|
168
|
+
// Reset state
|
|
169
|
+
this.#state = {
|
|
170
|
+
isLoading: false,
|
|
171
|
+
hasError: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Event handlers
|
|
176
|
+
#handleClick(event) {
|
|
177
|
+
// Implementation
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#handleScroll(event) {
|
|
181
|
+
// Throttled/debounced implementation
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Utility methods
|
|
185
|
+
#validateData(data) {
|
|
186
|
+
return data && typeof data === 'object';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#handleError(error) {
|
|
190
|
+
this.#state.hasError = true;
|
|
191
|
+
this.dispatchEvent(new CustomEvent('component:error', {
|
|
192
|
+
detail: { error, component: this },
|
|
193
|
+
bubbles: true
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Static utility methods
|
|
198
|
+
/**
|
|
199
|
+
* Find component by selector
|
|
200
|
+
* @param {string} selector - CSS selector
|
|
201
|
+
* @returns {ComponentName|null}
|
|
202
|
+
*/
|
|
203
|
+
static find(selector) {
|
|
204
|
+
return document.querySelector(selector);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Find all components
|
|
209
|
+
* @returns {NodeList}
|
|
210
|
+
*/
|
|
211
|
+
static findAll() {
|
|
212
|
+
return document.querySelectorAll('component-name');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Register with safety check
|
|
217
|
+
if (!customElements.get('component-name')) {
|
|
218
|
+
customElements.define('component-name', ComponentName);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Module export support
|
|
222
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
223
|
+
module.exports = ComponentName;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Performance Patterns
|
|
228
|
+
|
|
229
|
+
### Debouncing & Throttling
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// Debounce for input/resize
|
|
233
|
+
#debounce(func, delay) {
|
|
234
|
+
let timeout;
|
|
235
|
+
return (...args) => {
|
|
236
|
+
clearTimeout(timeout);
|
|
237
|
+
timeout = setTimeout(() => func.apply(this, args), delay);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Throttle for scroll/mousemove
|
|
242
|
+
#throttle(func, limit) {
|
|
243
|
+
let inThrottle;
|
|
244
|
+
return (...args) => {
|
|
245
|
+
if (!inThrottle) {
|
|
246
|
+
func.apply(this, args);
|
|
247
|
+
inThrottle = true;
|
|
248
|
+
setTimeout(() => inThrottle = false, limit);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Usage
|
|
254
|
+
this.#debouncedSearch = this.#debounce(this.#search.bind(this), 300);
|
|
255
|
+
this.#throttledScroll = this.#throttle(this.#handleScroll.bind(this), 100);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### RequestAnimationFrame
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// Smooth animations
|
|
262
|
+
#animate() {
|
|
263
|
+
if (this.#rafId) {
|
|
264
|
+
cancelAnimationFrame(this.#rafId);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.#rafId = requestAnimationFrame(() => {
|
|
268
|
+
// Perform DOM updates
|
|
269
|
+
this.#updatePosition();
|
|
270
|
+
|
|
271
|
+
// Continue animation if needed
|
|
272
|
+
if (this.#shouldContinue) {
|
|
273
|
+
this.#animate();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Intersection Observer
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
#setupObserver() {
|
|
283
|
+
const options = {
|
|
284
|
+
root: null,
|
|
285
|
+
rootMargin: '50px',
|
|
286
|
+
threshold: [0, 0.5, 1]
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
this.#observer = new IntersectionObserver((entries) => {
|
|
290
|
+
entries.forEach(entry => {
|
|
291
|
+
if (entry.isIntersecting) {
|
|
292
|
+
this.#handleVisible(entry.target);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}, options);
|
|
296
|
+
|
|
297
|
+
this.#observer.observe(this);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Memory Management
|
|
302
|
+
|
|
303
|
+
### AbortController Pattern
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
#setupEventListeners() {
|
|
307
|
+
// Create controller for this component
|
|
308
|
+
this.#abortController = new AbortController();
|
|
309
|
+
const { signal } = this.#abortController;
|
|
310
|
+
|
|
311
|
+
// All listeners use the same signal
|
|
312
|
+
window.addEventListener('resize', this.#handleResize, { signal });
|
|
313
|
+
document.addEventListener('click', this.#handleClick, { signal });
|
|
314
|
+
|
|
315
|
+
// Cleanup in disconnectedCallback
|
|
316
|
+
this.#abortController?.abort();
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### WeakMap for External References
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
class ComponentManager {
|
|
324
|
+
// Use WeakMap for external object references
|
|
325
|
+
#cache = new WeakMap();
|
|
326
|
+
|
|
327
|
+
#storeData(element, data) {
|
|
328
|
+
this.#cache.set(element, data);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#getData(element) {
|
|
332
|
+
return this.#cache.get(element);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Error Handling
|
|
338
|
+
|
|
339
|
+
### Try-Catch Boundaries
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
async #fetchData(url) {
|
|
343
|
+
try {
|
|
344
|
+
const response = await fetch(url);
|
|
345
|
+
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return await response.json();
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error('ComponentName: Fetch error', error);
|
|
353
|
+
|
|
354
|
+
// Dispatch error event
|
|
355
|
+
this.dispatchEvent(new CustomEvent('component:error', {
|
|
356
|
+
detail: { error, operation: 'fetch' },
|
|
357
|
+
bubbles: true
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
// Return fallback
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Validation Guards
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
#processData(data) {
|
|
370
|
+
// Early validation
|
|
371
|
+
if (!data || typeof data !== 'object') {
|
|
372
|
+
console.warn('ComponentName: Invalid data provided');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Type checking
|
|
377
|
+
if (!Array.isArray(data.items)) {
|
|
378
|
+
console.warn('ComponentName: Expected items array');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Safe processing
|
|
383
|
+
data.items.forEach(item => {
|
|
384
|
+
try {
|
|
385
|
+
this.#processItem(item);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error('ComponentName: Error processing item', error);
|
|
388
|
+
// Continue processing other items
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Accessibility Requirements
|
|
395
|
+
|
|
396
|
+
### ARIA Attributes
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
#setupAccessibility() {
|
|
400
|
+
// Set role if needed
|
|
401
|
+
if (!this.hasAttribute('role')) {
|
|
402
|
+
this.setAttribute('role', 'region');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Set aria-label or aria-labelledby
|
|
406
|
+
if (!this.hasAttribute('aria-label')) {
|
|
407
|
+
const label = this.querySelector('[data-label]');
|
|
408
|
+
if (label) {
|
|
409
|
+
label.id = label.id || `label-${Date.now()}`;
|
|
410
|
+
this.setAttribute('aria-labelledby', label.id);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Live regions for updates
|
|
415
|
+
this.#announcer = document.createElement('div');
|
|
416
|
+
this.#announcer.setAttribute('aria-live', 'polite');
|
|
417
|
+
this.#announcer.setAttribute('aria-atomic', 'true');
|
|
418
|
+
this.#announcer.className = 'sr-only';
|
|
419
|
+
this.appendChild(this.#announcer);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#announce(message) {
|
|
423
|
+
if (this.#announcer) {
|
|
424
|
+
this.#announcer.textContent = message;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Focus Management
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
#trapFocus() {
|
|
433
|
+
const focusableElements = this.querySelectorAll(
|
|
434
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const firstElement = focusableElements[0];
|
|
438
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
439
|
+
|
|
440
|
+
this.#focusTrap = (event) => {
|
|
441
|
+
if (event.key !== 'Tab') return;
|
|
442
|
+
|
|
443
|
+
if (event.shiftKey) {
|
|
444
|
+
if (document.activeElement === firstElement) {
|
|
445
|
+
event.preventDefault();
|
|
446
|
+
lastElement.focus();
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
if (document.activeElement === lastElement) {
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
firstElement.focus();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
this.addEventListener('keydown', this.#focusTrap);
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Async Operations
|
|
461
|
+
|
|
462
|
+
### Fetch with AbortController
|
|
463
|
+
|
|
464
|
+
```javascript
|
|
465
|
+
async #fetchWithTimeout(url, timeout = 5000) {
|
|
466
|
+
const controller = new AbortController();
|
|
467
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const response = await fetch(url, {
|
|
471
|
+
signal: controller.signal,
|
|
472
|
+
headers: {
|
|
473
|
+
'Content-Type': 'application/json'
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
clearTimeout(timeoutId);
|
|
478
|
+
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
throw new Error(`HTTP ${response.status}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return await response.json();
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error.name === 'AbortError') {
|
|
486
|
+
throw new Error('Request timeout');
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Promise Queue
|
|
494
|
+
|
|
495
|
+
```javascript
|
|
496
|
+
class QueuedComponent extends HTMLElement {
|
|
497
|
+
#queue = [];
|
|
498
|
+
#processing = false;
|
|
499
|
+
|
|
500
|
+
async #addToQueue(task) {
|
|
501
|
+
this.#queue.push(task);
|
|
502
|
+
|
|
503
|
+
if (!this.#processing) {
|
|
504
|
+
this.#processQueue();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async #processQueue() {
|
|
509
|
+
this.#processing = true;
|
|
510
|
+
|
|
511
|
+
while (this.#queue.length > 0) {
|
|
512
|
+
const task = this.#queue.shift();
|
|
513
|
+
try {
|
|
514
|
+
await task();
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.error('QueuedComponent: Task error', error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.#processing = false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## URL Manipulation
|
|
526
|
+
|
|
527
|
+
### Always use URL and URLSearchParams APIs
|
|
528
|
+
|
|
529
|
+
```javascript
|
|
530
|
+
// Good - Type-safe URL manipulation
|
|
531
|
+
const updateFilters = (filters) => {
|
|
532
|
+
const url = new URL(window.location.href);
|
|
533
|
+
|
|
534
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
535
|
+
if (value) {
|
|
536
|
+
url.searchParams.set(key, value);
|
|
537
|
+
} else {
|
|
538
|
+
url.searchParams.delete(key);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return url;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// Navigation with proper state management
|
|
546
|
+
const navigateToFilters = (filters) => {
|
|
547
|
+
const url = updateFilters(filters);
|
|
548
|
+
const params = url.searchParams.toString();
|
|
549
|
+
|
|
550
|
+
history.pushState({ urlParameters: params }, '', url.toString());
|
|
551
|
+
updateProductGrid(url.searchParams);
|
|
552
|
+
};
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## Event-Driven Architecture
|
|
556
|
+
|
|
557
|
+
### Custom Events with Typed Details
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
/**
|
|
561
|
+
* @typedef {Object} ComponentEventDetail
|
|
562
|
+
* @property {string} action - Action performed
|
|
563
|
+
* @property {Object} data - Associated data
|
|
564
|
+
* @property {number} timestamp - Event timestamp
|
|
565
|
+
*/
|
|
566
|
+
|
|
567
|
+
// Dispatch typed event
|
|
568
|
+
const event = new CustomEvent('component:action', {
|
|
569
|
+
detail: {
|
|
570
|
+
action: 'update',
|
|
571
|
+
data: { id: 123, value: 'test' },
|
|
572
|
+
timestamp: Date.now()
|
|
573
|
+
},
|
|
574
|
+
bubbles: true,
|
|
575
|
+
cancelable: true
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
this.dispatchEvent(event);
|
|
579
|
+
|
|
580
|
+
// Listen with type safety
|
|
581
|
+
element.addEventListener('component:action', (event) => {
|
|
582
|
+
const { action, data, timestamp } = event.detail;
|
|
583
|
+
// Handle event
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Documentation Standards
|
|
588
|
+
|
|
589
|
+
### JSDoc Requirements
|
|
590
|
+
|
|
591
|
+
```javascript
|
|
592
|
+
/**
|
|
593
|
+
* ComponentName - One line description
|
|
594
|
+
*
|
|
595
|
+
* Detailed multi-line description explaining:
|
|
596
|
+
* - Primary purpose
|
|
597
|
+
* - Key features
|
|
598
|
+
* - Usage patterns
|
|
599
|
+
*
|
|
600
|
+
* @fires component:ready - When component initialization completes
|
|
601
|
+
* @fires component:error - When an error occurs
|
|
602
|
+
*
|
|
603
|
+
* @example Basic usage
|
|
604
|
+
* <component-name data-option="value">
|
|
605
|
+
* Content
|
|
606
|
+
* </component-name>
|
|
607
|
+
*
|
|
608
|
+
* @example JavaScript API
|
|
609
|
+
* const component = document.querySelector('component-name');
|
|
610
|
+
* await component.loadData({ id: 123 });
|
|
611
|
+
*/
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Method description
|
|
615
|
+
* @param {string} name - Parameter description
|
|
616
|
+
* @param {Object} [options] - Optional parameter
|
|
617
|
+
* @param {boolean} [options.cache=true] - Option description
|
|
618
|
+
* @returns {Promise<Object>} Return value description
|
|
619
|
+
* @throws {TypeError} When name is not a string
|
|
620
|
+
* @throws {Error} When network request fails
|
|
621
|
+
* @private
|
|
622
|
+
*/
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Browser Compatibility
|
|
626
|
+
|
|
627
|
+
### Feature Detection
|
|
628
|
+
|
|
629
|
+
```javascript
|
|
630
|
+
class CompatibleComponent extends HTMLElement {
|
|
631
|
+
#supportsIntersectionObserver = 'IntersectionObserver' in window;
|
|
632
|
+
#supportsWebAnimations = 'animate' in Element.prototype;
|
|
633
|
+
|
|
634
|
+
#animate(element, keyframes, options) {
|
|
635
|
+
if (this.#supportsWebAnimations) {
|
|
636
|
+
return element.animate(keyframes, options);
|
|
637
|
+
} else {
|
|
638
|
+
// Fallback to CSS transitions
|
|
639
|
+
element.style.transition = `all ${options.duration}ms`;
|
|
640
|
+
Object.assign(element.style, keyframes[keyframes.length - 1]);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## Code Style Guidelines
|
|
647
|
+
|
|
648
|
+
### General Patterns
|
|
649
|
+
|
|
650
|
+
- **Avoid mutation** - Use `const` over `let` unless necessary
|
|
651
|
+
- **Use `for...of`** over `forEach()` for better performance
|
|
652
|
+
- **Early returns** over nested conditionals
|
|
653
|
+
- **Async/await** over Promise chains
|
|
654
|
+
- **Optional chaining** for safe property access
|
|
655
|
+
- **Nullish coalescing** for default values
|
|
656
|
+
|
|
657
|
+
```javascript
|
|
658
|
+
// Good patterns
|
|
659
|
+
const processItems = async (items) => {
|
|
660
|
+
if (!items?.length) return [];
|
|
661
|
+
|
|
662
|
+
const results = [];
|
|
663
|
+
for (const item of items) {
|
|
664
|
+
const processed = await processItem(item);
|
|
665
|
+
results.push(processed);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return results;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Avoid
|
|
672
|
+
const processItems = (items) => {
|
|
673
|
+
return new Promise((resolve) => {
|
|
674
|
+
if (items && items.length > 0) {
|
|
675
|
+
const results = [];
|
|
676
|
+
items.forEach((item) => {
|
|
677
|
+
processItem(item).then((processed) => {
|
|
678
|
+
results.push(processed);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
resolve(results);
|
|
682
|
+
} else {
|
|
683
|
+
resolve([]);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## Testing Considerations
|
|
690
|
+
|
|
691
|
+
### Testable Structure
|
|
692
|
+
|
|
693
|
+
```javascript
|
|
694
|
+
class TestableComponent extends HTMLElement {
|
|
695
|
+
// Expose state for testing (dev only)
|
|
696
|
+
get testState() {
|
|
697
|
+
if (process.env.NODE_ENV === 'development') {
|
|
698
|
+
return { ...this.#state };
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Static test utilities
|
|
704
|
+
static #testUtils = {
|
|
705
|
+
resetAll() {
|
|
706
|
+
document.querySelectorAll('testable-component').forEach(c => c.reset());
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
findByAttribute(attr, value) {
|
|
710
|
+
return document.querySelector(`testable-component[${attr}="${value}"]`);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
static get testUtils() {
|
|
715
|
+
if (process.env.NODE_ENV === 'development') {
|
|
716
|
+
return this.#testUtils;
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Layout/Reflow Performance Optimization
|
|
724
|
+
|
|
725
|
+
### Understanding Layout Thrashing
|
|
726
|
+
|
|
727
|
+
**Layout thrashing (forced synchronous layout) is a major performance bottleneck that occurs when JavaScript repeatedly reads and writes DOM properties that trigger layout recalculation.**
|
|
728
|
+
|
|
729
|
+
When you read certain DOM properties or call specific methods, the browser must synchronously calculate styles and layout if the DOM has been modified. This forces the browser to:
|
|
730
|
+
1. Recalculate styles (if invalidated)
|
|
731
|
+
2. Recompute layout/reflow
|
|
732
|
+
3. Update the render tree
|
|
733
|
+
4. Potentially repaint affected areas
|
|
734
|
+
|
|
735
|
+
### Operations That Force Layout/Reflow
|
|
736
|
+
|
|
737
|
+
**Element Box Metrics (Always trigger reflow when read):**
|
|
738
|
+
```javascript
|
|
739
|
+
// ❌ These properties force layout when read:
|
|
740
|
+
element.offsetLeft, element.offsetTop, element.offsetWidth, element.offsetHeight
|
|
741
|
+
element.offsetParent
|
|
742
|
+
element.clientLeft, element.clientTop, element.clientWidth, element.clientHeight
|
|
743
|
+
element.getClientRects(), element.getBoundingClientRect()
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Scroll Properties:**
|
|
747
|
+
```javascript
|
|
748
|
+
// ❌ Reading or setting these forces layout:
|
|
749
|
+
element.scrollWidth, element.scrollHeight
|
|
750
|
+
element.scrollLeft, element.scrollTop // Also when setting
|
|
751
|
+
element.scrollBy(), element.scrollTo()
|
|
752
|
+
element.scrollIntoView(), element.scrollIntoViewIfNeeded()
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Window Dimensions:**
|
|
756
|
+
```javascript
|
|
757
|
+
// ❌ These force layout:
|
|
758
|
+
window.scrollX, window.scrollY
|
|
759
|
+
window.innerHeight, window.innerWidth
|
|
760
|
+
window.visualViewport.height, window.visualViewport.width
|
|
761
|
+
window.visualViewport.offsetTop, window.visualViewport.offsetLeft
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Other Common Triggers:**
|
|
765
|
+
```javascript
|
|
766
|
+
// ❌ These also force layout:
|
|
767
|
+
element.focus() // Needs to check if element is rendered
|
|
768
|
+
element.innerText // Needs layout for line breaks
|
|
769
|
+
element.computedRole, element.computedName
|
|
770
|
+
document.elementFromPoint()
|
|
771
|
+
|
|
772
|
+
// getComputedStyle() forces layout for:
|
|
773
|
+
// - Elements in shadow DOM
|
|
774
|
+
// - When viewport media queries exist
|
|
775
|
+
// - For layout-dependent properties (width, height, top, left, etc.)
|
|
776
|
+
window.getComputedStyle(element).width // Forces layout
|
|
777
|
+
window.getComputedStyle(element).color // May not force layout
|
|
778
|
+
|
|
779
|
+
// Form elements
|
|
780
|
+
inputElement.select(), textareaElement.select()
|
|
781
|
+
|
|
782
|
+
// Mouse event properties
|
|
783
|
+
mouseEvent.layerX, mouseEvent.layerY
|
|
784
|
+
mouseEvent.offsetX, mouseEvent.offsetY
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### Best Practices to Avoid Layout Thrashing
|
|
788
|
+
|
|
789
|
+
#### 1. Batch DOM Reads and Writes
|
|
790
|
+
|
|
791
|
+
**❌ BAD: Interleaving reads and writes (causes layout thrashing)**
|
|
792
|
+
```javascript
|
|
793
|
+
// This causes 3 layouts!
|
|
794
|
+
elements.forEach(el => {
|
|
795
|
+
el.style.left = el.offsetLeft + 10 + 'px'; // Read then write
|
|
796
|
+
el.style.top = el.offsetTop + 10 + 'px'; // Read then write
|
|
797
|
+
el.style.width = el.offsetWidth + 10 + 'px'; // Read then write
|
|
798
|
+
});
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**✅ GOOD: Batch all reads, then all writes**
|
|
802
|
+
```javascript
|
|
803
|
+
// Read phase - get all measurements first
|
|
804
|
+
const measurements = elements.map(el => ({
|
|
805
|
+
left: el.offsetLeft,
|
|
806
|
+
top: el.offsetTop,
|
|
807
|
+
width: el.offsetWidth
|
|
808
|
+
}));
|
|
809
|
+
|
|
810
|
+
// Write phase - apply all changes
|
|
811
|
+
elements.forEach((el, i) => {
|
|
812
|
+
el.style.left = measurements[i].left + 10 + 'px';
|
|
813
|
+
el.style.top = measurements[i].top + 10 + 'px';
|
|
814
|
+
el.style.width = measurements[i].width + 10 + 'px';
|
|
815
|
+
});
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
#### 2. Use requestAnimationFrame for Visual Updates
|
|
819
|
+
|
|
820
|
+
**✅ Schedule layout-affecting changes at the optimal time:**
|
|
821
|
+
```javascript
|
|
822
|
+
/**
|
|
823
|
+
* Batch DOM updates in animation frame
|
|
824
|
+
* @private
|
|
825
|
+
*/
|
|
826
|
+
#scheduleUpdate() {
|
|
827
|
+
if (this.#rafId) return;
|
|
828
|
+
|
|
829
|
+
this.#rafId = requestAnimationFrame(() => {
|
|
830
|
+
// Read phase - at the very start of rAF
|
|
831
|
+
const scrollTop = window.scrollY;
|
|
832
|
+
const viewportHeight = window.innerHeight;
|
|
833
|
+
const elementRect = this.getBoundingClientRect();
|
|
834
|
+
|
|
835
|
+
// Write phase - after all reads
|
|
836
|
+
this.style.transform = `translateY(${scrollTop * 0.5}px)`;
|
|
837
|
+
this.classList.toggle('in-view', elementRect.top < viewportHeight);
|
|
838
|
+
|
|
839
|
+
this.#rafId = null;
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
#### 3. Use CSS for Animations When Possible
|
|
845
|
+
|
|
846
|
+
**✅ Prefer CSS transforms and opacity (don't trigger layout):**
|
|
847
|
+
```javascript
|
|
848
|
+
// ❌ BAD: Animating layout properties
|
|
849
|
+
element.style.left = x + 'px';
|
|
850
|
+
element.style.top = y + 'px';
|
|
851
|
+
element.style.width = width + 'px';
|
|
852
|
+
|
|
853
|
+
// ✅ GOOD: Use transforms (compositor-only)
|
|
854
|
+
element.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
|
|
855
|
+
element.style.opacity = opacity;
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
#### 4. Cache Layout Values
|
|
859
|
+
|
|
860
|
+
**✅ Store frequently accessed layout values:**
|
|
861
|
+
```javascript
|
|
862
|
+
class Component extends HTMLElement {
|
|
863
|
+
#cachedDimensions = null;
|
|
864
|
+
#resizeObserver = null;
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Get cached dimensions or calculate if needed
|
|
868
|
+
* @returns {Object} Element dimensions
|
|
869
|
+
* @private
|
|
870
|
+
*/
|
|
871
|
+
#getDimensions() {
|
|
872
|
+
if (!this.#cachedDimensions) {
|
|
873
|
+
this.#cachedDimensions = {
|
|
874
|
+
width: this.offsetWidth,
|
|
875
|
+
height: this.offsetHeight,
|
|
876
|
+
top: this.offsetTop,
|
|
877
|
+
left: this.offsetLeft
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return this.#cachedDimensions;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Set up resize observer to invalidate cache
|
|
885
|
+
* @private
|
|
886
|
+
*/
|
|
887
|
+
#setupResizeObserver() {
|
|
888
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
889
|
+
this.#cachedDimensions = null; // Invalidate cache
|
|
890
|
+
});
|
|
891
|
+
this.#resizeObserver.observe(this);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
#### 5. Use Document Fragments for Multiple DOM Insertions
|
|
897
|
+
|
|
898
|
+
**✅ Build DOM off-screen, then insert once:**
|
|
899
|
+
```javascript
|
|
900
|
+
/**
|
|
901
|
+
* Add multiple items efficiently
|
|
902
|
+
* @param {Array} items - Items to add
|
|
903
|
+
* @private
|
|
904
|
+
*/
|
|
905
|
+
#addMultipleItems(items) {
|
|
906
|
+
// ❌ BAD: Multiple reflows
|
|
907
|
+
// items.forEach(item => {
|
|
908
|
+
// const el = document.createElement('div');
|
|
909
|
+
// el.textContent = item;
|
|
910
|
+
// container.appendChild(el); // Triggers reflow each time
|
|
911
|
+
// });
|
|
912
|
+
|
|
913
|
+
// ✅ GOOD: Single reflow
|
|
914
|
+
const fragment = document.createDocumentFragment();
|
|
915
|
+
items.forEach(item => {
|
|
916
|
+
const el = document.createElement('div');
|
|
917
|
+
el.textContent = item;
|
|
918
|
+
fragment.appendChild(el); // No reflow
|
|
919
|
+
});
|
|
920
|
+
container.appendChild(fragment); // Single reflow
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
#### 6. Use CSS contain Property
|
|
925
|
+
|
|
926
|
+
**✅ Limit layout recalculation scope:**
|
|
927
|
+
```css
|
|
928
|
+
/* Limit layout recalculation to this element */
|
|
929
|
+
.component {
|
|
930
|
+
contain: layout style paint;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* For scrollable areas */
|
|
934
|
+
.scroll-container {
|
|
935
|
+
contain: strict;
|
|
936
|
+
content-visibility: auto;
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
#### 7. Avoid Layout in Loops
|
|
941
|
+
|
|
942
|
+
**✅ Move layout queries outside loops:**
|
|
943
|
+
```javascript
|
|
944
|
+
/**
|
|
945
|
+
* Process visible elements efficiently
|
|
946
|
+
* @private
|
|
947
|
+
*/
|
|
948
|
+
#processVisibleElements() {
|
|
949
|
+
// ❌ BAD: Layout query in loop condition
|
|
950
|
+
// for (let i = 0; i < elements.length && window.scrollY < 1000; i++) {
|
|
951
|
+
// // scrollY forces layout each iteration
|
|
952
|
+
// }
|
|
953
|
+
|
|
954
|
+
// ✅ GOOD: Query once before loop
|
|
955
|
+
const scrollY = window.scrollY;
|
|
956
|
+
const viewportHeight = window.innerHeight;
|
|
957
|
+
|
|
958
|
+
for (let i = 0; i < elements.length && scrollY < 1000; i++) {
|
|
959
|
+
// Process without additional layout queries
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
### FastDOM Pattern Implementation
|
|
965
|
+
|
|
966
|
+
**Implement a queue system for DOM operations:**
|
|
967
|
+
```javascript
|
|
968
|
+
/**
|
|
969
|
+
* DOM operation queue to batch reads and writes
|
|
970
|
+
* @private
|
|
971
|
+
*/
|
|
972
|
+
class DOMQueue {
|
|
973
|
+
#reads = [];
|
|
974
|
+
#writes = [];
|
|
975
|
+
#scheduled = false;
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Queue a read operation
|
|
979
|
+
* @param {Function} fn - Read function
|
|
980
|
+
* @returns {Promise}
|
|
981
|
+
*/
|
|
982
|
+
read(fn) {
|
|
983
|
+
return new Promise(resolve => {
|
|
984
|
+
this.#reads.push(() => resolve(fn()));
|
|
985
|
+
this.#scheduleFlush();
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Queue a write operation
|
|
991
|
+
* @param {Function} fn - Write function
|
|
992
|
+
* @returns {Promise}
|
|
993
|
+
*/
|
|
994
|
+
write(fn) {
|
|
995
|
+
return new Promise(resolve => {
|
|
996
|
+
this.#writes.push(() => resolve(fn()));
|
|
997
|
+
this.#scheduleFlush();
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Schedule queue flush
|
|
1003
|
+
* @private
|
|
1004
|
+
*/
|
|
1005
|
+
#scheduleFlush() {
|
|
1006
|
+
if (this.#scheduled) return;
|
|
1007
|
+
this.#scheduled = true;
|
|
1008
|
+
|
|
1009
|
+
requestAnimationFrame(() => {
|
|
1010
|
+
this.#flush();
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Flush the queue - reads first, then writes
|
|
1016
|
+
* @private
|
|
1017
|
+
*/
|
|
1018
|
+
#flush() {
|
|
1019
|
+
const reads = this.#reads.splice(0);
|
|
1020
|
+
const writes = this.#writes.splice(0);
|
|
1021
|
+
|
|
1022
|
+
// Execute all reads
|
|
1023
|
+
reads.forEach(read => read());
|
|
1024
|
+
|
|
1025
|
+
// Then execute all writes
|
|
1026
|
+
writes.forEach(write => write());
|
|
1027
|
+
|
|
1028
|
+
this.#scheduled = false;
|
|
1029
|
+
|
|
1030
|
+
// Schedule another flush if needed
|
|
1031
|
+
if (this.#reads.length || this.#writes.length) {
|
|
1032
|
+
this.#scheduleFlush();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Usage example
|
|
1038
|
+
const domQueue = new DOMQueue();
|
|
1039
|
+
|
|
1040
|
+
// Queue operations
|
|
1041
|
+
await domQueue.read(() => {
|
|
1042
|
+
this.#width = element.offsetWidth;
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
await domQueue.write(() => {
|
|
1046
|
+
element.style.width = this.#width + 10 + 'px';
|
|
1047
|
+
});
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### Performance Monitoring
|
|
1051
|
+
|
|
1052
|
+
**Monitor and log layout thrashing in development:**
|
|
1053
|
+
```javascript
|
|
1054
|
+
/**
|
|
1055
|
+
* Monitor layout performance in development
|
|
1056
|
+
* @private
|
|
1057
|
+
*/
|
|
1058
|
+
#monitorLayoutPerformance() {
|
|
1059
|
+
if (process.env.NODE_ENV === 'development') {
|
|
1060
|
+
const observer = new PerformanceObserver((list) => {
|
|
1061
|
+
for (const entry of list.getEntries()) {
|
|
1062
|
+
if (entry.entryType === 'measure') {
|
|
1063
|
+
if (entry.duration > 16) { // Longer than one frame
|
|
1064
|
+
console.warn(`Slow layout operation: ${entry.name} took ${entry.duration}ms`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
observer.observe({ entryTypes: ['measure'] });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Summary of Layout/Reflow Rules
|
|
1076
|
+
|
|
1077
|
+
1. **Never read layout properties immediately after writing** - This forces synchronous layout
|
|
1078
|
+
2. **Batch all DOM reads before any DOM writes** - Use the read-write-read-write pattern
|
|
1079
|
+
3. **Use requestAnimationFrame** for visual updates that affect layout
|
|
1080
|
+
4. **Cache layout values** when they don't change frequently
|
|
1081
|
+
5. **Prefer CSS transforms and opacity** for animations (compositor-only properties)
|
|
1082
|
+
6. **Use will-change sparingly** to hint at upcoming changes
|
|
1083
|
+
7. **Avoid layout queries in loops** - Query once before the loop
|
|
1084
|
+
8. **Use ResizeObserver/IntersectionObserver** instead of polling dimensions
|
|
1085
|
+
9. **Build complex DOM structures off-screen** using DocumentFragment
|
|
1086
|
+
10. **Monitor performance** using Performance Observer API
|
|
1087
|
+
|
|
1088
|
+
**Remember: The key to avoiding layout thrashing is to minimize the number of times the browser must recalculate layout. Every time you force layout, you're potentially blocking the main thread and degrading user experience.**
|
|
1089
|
+
|
|
1090
|
+
## Code Review Checklist
|
|
1091
|
+
|
|
1092
|
+
When reviewing JavaScript code, ensure:
|
|
1093
|
+
|
|
1094
|
+
- [ ] Class extends HTMLElement with proper lifecycle
|
|
1095
|
+
- [ ] Private fields use # syntax (not _ or private)
|
|
1096
|
+
- [ ] Event handlers bound in constructor
|
|
1097
|
+
- [ ] Connected/disconnected callbacks have guards
|
|
1098
|
+
- [ ] Try-catch blocks around critical operations
|
|
1099
|
+
- [ ] Memory cleanup in disconnectedCallback
|
|
1100
|
+
- [ ] AbortController for event listeners
|
|
1101
|
+
- [ ] Passive option for scroll/touch events
|
|
1102
|
+
- [ ] RequestAnimationFrame for animations
|
|
1103
|
+
- [ ] Debounce/throttle for expensive operations
|
|
1104
|
+
- [ ] JSDoc comments on all public methods
|
|
1105
|
+
- [ ] Custom events with proper detail objects
|
|
1106
|
+
- [ ] ARIA attributes for accessibility
|
|
1107
|
+
- [ ] Error events dispatched to parent
|
|
1108
|
+
- [ ] Static utility methods for querying
|
|
1109
|
+
- [ ] Module export support
|
|
1110
|
+
- [ ] Performance measurements for slow operations
|
|
1111
|
+
- [ ] Validation guards on public methods
|
|
1112
|
+
- [ ] Graceful degradation for missing features
|
|
1113
|
+
- [ ] No console.log in production code
|
|
1114
|
+
|
|
1115
|
+
## Reference Implementation
|
|
1116
|
+
|
|
1117
|
+
Use `_scripts/electric-modal.js` as the gold standard reference for all web component implementations in this project.
|
|
1118
|
+
|
|
1119
|
+
## Performance Targets
|
|
1120
|
+
|
|
1121
|
+
- Component initialization: < 16ms
|
|
1122
|
+
- Event handler execution: < 8ms
|
|
1123
|
+
- Animation frame: < 16ms (60fps)
|
|
1124
|
+
- Memory: No detectable leaks
|
|
1125
|
+
- Network requests: < 3 seconds with timeout
|