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.
Files changed (40) hide show
  1. package/README.md +8 -6
  2. package/bin/version.txt +1 -1
  3. package/package.json +1 -1
  4. package/src/commands/add-cursor-skill.js +12 -7
  5. package/src/commands/init.js +10 -5
  6. package/src/cursor/rules/00-rule-index.mdc +24 -0
  7. package/src/cursor/rules/accessibility-rules.mdc +527 -0
  8. package/src/cursor/rules/commit-rules.mdc +286 -0
  9. package/src/cursor/rules/cursor-rule-template.mdc +66 -0
  10. package/src/cursor/rules/examples/section-example.liquid +52 -0
  11. package/src/cursor/rules/examples/snippet-example.liquid +83 -0
  12. package/src/cursor/rules/figma-design-system.mdc +182 -0
  13. package/src/cursor/rules/global-rules-reference.mdc +62 -0
  14. package/src/cursor/rules/javascript-standards.mdc +1125 -0
  15. package/src/cursor/rules/js-refactor-tasks.mdc +123 -0
  16. package/src/cursor/rules/linear-task-creation.mdc +105 -0
  17. package/src/cursor/rules/liquid-doc-rules.mdc +595 -0
  18. package/src/cursor/rules/liquid.mdc +228 -0
  19. package/src/cursor/rules/project-overview.mdc +81 -0
  20. package/src/cursor/rules/schemas.mdc +150 -0
  21. package/src/cursor/rules/sections.mdc +25 -0
  22. package/src/cursor/rules/snippets.mdc +134 -0
  23. package/src/cursor/rules/tailwindcss-rules.mdc +410 -0
  24. package/src/cursor/skills/accessibility-pass/SKILL.md +54 -0
  25. package/src/cursor/skills/changelog-release/SKILL.md +50 -0
  26. package/src/cursor/skills/commit/SKILL.md +27 -0
  27. package/src/cursor/skills/commit-in-groups/SKILL.md +55 -0
  28. package/src/cursor/skills/linear-create-task/SKILL.md +81 -0
  29. package/src/cursor/skills/liquid-doc-comments/SKILL.md +37 -0
  30. package/src/cursor/skills/locale-translation-prep/SKILL.md +49 -0
  31. package/src/cursor/skills/schema-section-change/SKILL.md +39 -0
  32. package/src/cursor/skills/section-from-spec/SKILL.md +39 -0
  33. package/src/cursor/skills/theme-check-fix/SKILL.md +47 -0
  34. package/src/index.js +3 -2
  35. package/src/lib/commit-tooling.js +0 -44
  36. package/src/lib/config.js +1 -1
  37. package/src/lib/cursor-bundle.js +47 -0
  38. package/src/lib/prompts.js +3 -2
  39. package/src/workflows/build/reusable-build.yml +1 -1
  40. 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