@pnx-mixtape/mxds 0.1.0-rc.4 → 0.1.0-rc.6
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/.claude/skills/create-mxds-component/SKILL.md +77 -0
- package/DESIGN.md +595 -0
- package/README.md +2 -3
- package/dist/build/accordion.entry.js +2 -1
- package/dist/build/autocomplete.entry.js +1 -1
- package/dist/build/back-to-top.css +1 -0
- package/dist/build/back-to-top.entry.js +1 -0
- package/dist/build/base.css +1 -1
- package/dist/build/button.css +1 -1
- package/dist/build/card.css +1 -1
- package/dist/build/chunks/Accordion-Bqe8vTNR.js +44 -0
- package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
- package/dist/build/chunks/AccordionMobile-D517oXYZ.js +26 -0
- package/dist/build/chunks/Autocomplete-kRo8RNbC.js +135 -0
- package/dist/build/chunks/BackToTop-4M-Wl7z3.js +64 -0
- package/dist/build/chunks/ClosableAlert-CZ114JsL.js +55 -0
- package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
- package/dist/build/chunks/Dialog-DoYcGqhW.js +48 -0
- package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
- package/dist/build/chunks/FeedbackForm-BsL7fv8s.js +64 -0
- package/dist/build/chunks/Gallery-DqJPujX5.js +165 -0
- package/dist/build/chunks/GlobalToggle-7H3F1Wvc.js +82 -0
- package/dist/build/chunks/InPageNavigation-D7m3IN89.js +86 -0
- package/dist/build/chunks/Navigation-CRModT40.js +142 -0
- package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
- package/dist/build/chunks/Sticky-C1H8MNqK.js +51 -0
- package/dist/build/chunks/Tabs-BJD3kABL.js +160 -0
- package/dist/build/chunks/Translate-yAgySwet.js +46 -0
- package/dist/build/chunks/cookie-compliance-CG6x-TcV.js +119 -0
- package/dist/build/chunks/enums-BrHxPZZ_.js +36 -0
- package/dist/build/chunks/polyfills-D-qh1a_2.js +33 -0
- package/dist/build/chunks/utilities-B9mNRs7V.js +255 -0
- package/dist/build/content-block.css +1 -1
- package/dist/build/cookie-consent.entry.js +1 -1
- package/dist/build/dialog.entry.js +1 -1
- package/dist/build/drop-menu.entry.js +1 -1
- package/dist/build/elements.js +147 -1
- package/dist/build/feedback-form.css +1 -0
- package/dist/build/feedback-form.entry.js +1 -0
- package/dist/build/filters.entry.js +145 -1
- package/dist/build/form.css +1 -1
- package/dist/build/gallery.entry.js +1 -1
- package/dist/build/global-alert.entry.js +1 -1
- package/dist/build/header.css +1 -1
- package/dist/build/header.entry.js +1 -1
- package/dist/build/icon.css +1 -1
- package/dist/build/in-page-navigation.entry.js +1 -1
- package/dist/build/navigation.css +1 -1
- package/dist/build/navigation.entry.js +1 -1
- package/dist/build/pagination.css +1 -1
- package/dist/build/popover.entry.js +1 -1
- package/dist/build/react.js +2291 -1
- package/dist/build/side-navigation.css +1 -1
- package/dist/build/sidebar.css +1 -1
- package/dist/build/sticky.entry.js +1 -1
- package/dist/build/tabs.entry.js +1 -1
- package/dist/build/translate.css +1 -0
- package/dist/build/translate.entry.js +1 -0
- package/dist/build/utility-list.entry.js +53 -1
- package/dist/types/Component/BackToTop/BackToTop.d.ts +20 -0
- package/dist/types/Component/BackToTop/BackToTop.d.ts.map +1 -0
- package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts +27 -0
- package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts.map +1 -0
- package/dist/types/Component/BackToTop/back-to-top.entry.d.ts +2 -0
- package/dist/types/Component/BackToTop/back-to-top.entry.d.ts.map +1 -0
- package/dist/types/Component/DropMenu/DropMenu.d.ts +2 -1
- package/dist/types/Component/DropMenu/DropMenu.d.ts.map +1 -1
- package/dist/types/Component/Navigation/Elements/Navigation.d.ts +2 -3
- package/dist/types/Component/Navigation/Elements/Navigation.d.ts.map +1 -1
- package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts +3 -3
- package/dist/types/Component/SideNavigation/Components/SideNavigationTree.d.ts.map +1 -1
- package/dist/types/Component/SideNavigation/SideNavigation.d.ts +3 -1
- package/dist/types/Component/SideNavigation/SideNavigation.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Components/TabDropMenu.d.ts.map +1 -1
- package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
- package/dist/types/Component/Translate/Elements/Translate.d.ts +23 -0
- package/dist/types/Component/Translate/Elements/Translate.d.ts.map +1 -0
- package/dist/types/Component/Translate/Translate.d.ts +37 -0
- package/dist/types/Component/Translate/Translate.d.ts.map +1 -0
- package/dist/types/Component/Translate/languages.d.ts +11 -0
- package/dist/types/Component/Translate/languages.d.ts.map +1 -0
- package/dist/types/Component/Translate/translate.entry.d.ts +2 -0
- package/dist/types/Component/Translate/translate.entry.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts +37 -0
- package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts +36 -0
- package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts.map +1 -0
- package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts +2 -0
- package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts.map +1 -0
- package/dist/types/Utility/Elements/google-translate-widget.d.ts +91 -0
- package/dist/types/Utility/Elements/google-translate-widget.d.ts.map +1 -0
- package/dist/types/Utility/utilities.d.ts +2 -0
- package/dist/types/Utility/utilities.d.ts.map +1 -1
- package/dist/types/elements.d.ts +3 -0
- package/dist/types/elements.d.ts.map +1 -1
- package/dist/types/enums.d.ts +3 -1
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/react.d.ts +3 -0
- package/dist/types/react.d.ts.map +1 -1
- package/dist/types/tokens.d.ts +1 -0
- package/dist/types/tokens.d.ts.map +1 -1
- package/docs/API.md +1215 -0
- package/docs/CSSGuidelines.md +893 -0
- package/docs/CodeQuality.md +940 -0
- package/docs/ComponentArchitecture.md +714 -0
- package/docs/Contributing.md +626 -0
- package/docs/MemoryManagement.md +598 -0
- package/docs/Migrations.md +6 -0
- package/docs/README.md +100 -0
- package/docs/Security.md +444 -0
- package/docs/Testing.md +739 -0
- package/docs/Troubleshooting.md +674 -0
- package/package.json +9 -6
- package/src/Atom/Button/Button.tsx +1 -1
- package/src/Atom/Button/__snapshots__/Button.stories.tsx.snap +2 -2
- package/src/Atom/Button/_buttons.css +1 -0
- package/src/Atom/Icon/README.md +1 -1
- package/src/Atom/Icon/_icon.css +1 -0
- package/src/Atom/Icon/images/thumb.svg +1 -0
- package/src/Atom/Link/_links.css +1 -0
- package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +4 -9
- package/src/Atom/Media/media.twig +4 -5
- package/src/Component/BackToTop/BackToTop.stories.ts +127 -0
- package/src/Component/BackToTop/BackToTop.stories.tsx +53 -0
- package/src/Component/BackToTop/BackToTop.tsx +120 -0
- package/src/Component/BackToTop/Elements/BackToTop.ts +119 -0
- package/src/Component/BackToTop/README.md +164 -0
- package/src/Component/BackToTop/__snapshots__/BackToTop.stories.ts.snap +24 -0
- package/src/Component/BackToTop/__snapshots__/BackToTop.stories.tsx.snap +3 -0
- package/src/Component/BackToTop/back-to-top.css +70 -0
- package/src/Component/BackToTop/back-to-top.entry.js +1 -0
- package/src/Component/BackToTop/back-to-top.twig +26 -0
- package/src/Component/Card/Components/CardContent.tsx +1 -1
- package/src/Component/Card/Components/CardMedia.tsx +1 -1
- package/src/Component/Card/__snapshots__/Card.stories.tsx.snap +3 -3
- package/src/Component/Card/card.css +1 -1
- package/src/Component/Carousel/Carousel.stories.ts +66 -36
- package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -5
- package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
- package/src/Component/ContentBlock/content-block.css +1 -1
- package/src/Component/DropMenu/DropMenu.stories.ts +21 -2
- package/src/Component/DropMenu/DropMenu.tsx +14 -4
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +3 -3
- package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +2 -2
- package/src/Component/DropMenu/drop-menu.twig +7 -1
- package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.tsx.snap +1 -1
- package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +48 -13
- package/src/Component/InPageNavigation/in-page-navigation.twig +4 -6
- package/src/Component/ListItem/Components/ListItemMedia.tsx +1 -1
- package/src/Component/Navigation/Elements/Navigation.ts +25 -29
- package/src/Component/Navigation/README.md +11 -10
- package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +8 -8
- package/src/Component/Navigation/_navigation-dropdown.css +9 -11
- package/src/Component/Navigation/_navigation-mega.css +5 -11
- package/src/Component/Navigation/_navigation.css +5 -3
- package/src/Component/Navigation/menu.twig +1 -1
- package/src/Component/Navigation/navigation.twig +0 -1
- package/src/Component/Pagination/Pagination.stories.ts +57 -27
- package/src/Component/Pagination/pagination.css +16 -7
- package/src/Component/Popover/__snapshots__/Popover.stories.tsx.snap +3 -3
- package/src/Component/SideNavigation/Components/SideNavigationTree.tsx +68 -24
- package/src/Component/SideNavigation/README.md +78 -32
- package/src/Component/SideNavigation/SideNavigation.stories.ts +41 -2
- package/src/Component/SideNavigation/SideNavigation.stories.tsx +31 -0
- package/src/Component/SideNavigation/SideNavigation.tsx +13 -3
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +87 -3
- package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.tsx.snap +2 -0
- package/src/Component/SideNavigation/side-navigation.css +3 -3
- package/src/Component/SideNavigation/side-navigation.twig +10 -3
- package/src/Component/Tabs/Components/TabDropMenu.tsx +7 -1
- package/src/Component/Tabs/Elements/Tabs.ts +13 -3
- package/src/Component/Tabs/Tabs.stories.ts +33 -12
- package/src/Component/Tabs/Tabs.stories.tsx +28 -14
- package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +3 -3
- package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +2 -2
- package/src/Component/Translate/Elements/Translate.ts +76 -0
- package/src/Component/Translate/README.md +285 -0
- package/src/Component/Translate/Translate.stories.ts +87 -0
- package/src/Component/Translate/Translate.stories.tsx +77 -0
- package/src/Component/Translate/Translate.tsx +213 -0
- package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +18 -0
- package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +3 -0
- package/src/Component/Translate/languages.ts +39 -0
- package/src/Component/Translate/translate.css +62 -0
- package/src/Component/Translate/translate.entry.js +1 -0
- package/src/Component/Translate/translate.twig +44 -0
- package/src/Form/Autocomplete/Autocomplete.stories.tsx +2 -1
- package/src/Form/FeedbackForm/Elements/FeedbackForm.ts +122 -0
- package/src/Form/FeedbackForm/FeedbackForm.stories.ts +99 -0
- package/src/Form/FeedbackForm/FeedbackForm.stories.tsx +73 -0
- package/src/Form/FeedbackForm/FeedbackForm.tsx +196 -0
- package/src/Form/FeedbackForm/README.md +233 -0
- package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.ts.snap +125 -0
- package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.tsx.snap +5 -0
- package/src/Form/FeedbackForm/feedback-form.css +74 -0
- package/src/Form/FeedbackForm/feedback-form.entry.js +1 -0
- package/src/Form/FeedbackForm/feedback-form.twig +80 -0
- package/src/Form/form.css +17 -19
- package/src/Introduction.mdx +1 -1
- package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +3 -3
- package/src/Layout/Header/Components/GlobalToggle.tsx +1 -1
- package/src/Layout/Header/Header.stories.ts +53 -48
- package/src/Layout/Header/Header.stories.tsx +35 -22
- package/src/Layout/Header/Header.tsx +2 -2
- package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +18 -14
- package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
- package/src/Layout/Header/_toggles.css +7 -7
- package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
- package/src/Layout/Sidebar/sidebar.css +10 -3
- package/src/Utility/Elements/google-translate-widget.ts +293 -0
- package/src/Utility/global.d.ts +4 -0
- package/src/Utility/utilities.ts +2 -0
- package/src/_custom-media.css +2 -0
- package/src/elements.ts +3 -0
- package/src/enums.ts +2 -0
- package/src/react.ts +3 -0
- package/src/tokens.js +1 -0
- package/dist/build/chunks/Accordion-GN0sME4W.js +0 -1
- package/dist/build/chunks/AccordionGroup-Bzqp5__q.js +0 -1
- package/dist/build/chunks/AccordionMobile-DG5jSe8Q.js +0 -1
- package/dist/build/chunks/Autocomplete-B4RtkAyh.js +0 -1
- package/dist/build/chunks/ClosableAlert-DEhj8Nxm.js +0 -1
- package/dist/build/chunks/CookieConsent-BkCMRVST.js +0 -1
- package/dist/build/chunks/Dialog-Haahn-Yn.js +0 -1
- package/dist/build/chunks/DropMenu-1baQc8Yc.js +0 -1
- package/dist/build/chunks/Gallery-D8NGXy2Q.js +0 -1
- package/dist/build/chunks/GlobalToggle-B52fdBQu.js +0 -1
- package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +0 -1
- package/dist/build/chunks/Navigation-hOrfLq7h.js +0 -1
- package/dist/build/chunks/Popover-Cxg-s5Jn.js +0 -1
- package/dist/build/chunks/Sticky-BGCuuEJK.js +0 -1
- package/dist/build/chunks/Tabs-DYy1YTdU.js +0 -15
- package/dist/build/chunks/cookie-compliance-Bbm-0vpj.js +0 -1
- package/dist/build/chunks/enums-DLV_1NAf.js +0 -1
- package/dist/build/chunks/polyfills-C7pRt54M.js +0 -1
- package/dist/build/chunks/utilities-_Bq3TxAQ.js +0 -1
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
# Memory Management Guide
|
|
2
|
+
|
|
3
|
+
Prevent memory leaks in Web Components and React applications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Why Memory Leaks Matter](#why-memory-leaks-matter)
|
|
8
|
+
- [Event Listeners](#event-listeners)
|
|
9
|
+
- [Observers](#observers)
|
|
10
|
+
- [Timers](#timers)
|
|
11
|
+
- [React Hooks](#react-hooks)
|
|
12
|
+
- [Testing for Leaks](#testing-for-leaks)
|
|
13
|
+
- [Common Pitfalls](#common-pitfalls)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why Memory Leaks Matter
|
|
18
|
+
|
|
19
|
+
Memory leaks occur when resources aren't cleaned up properly, causing:
|
|
20
|
+
|
|
21
|
+
- **Increased memory usage** over time
|
|
22
|
+
- **Degraded performance** as garbage collection struggles
|
|
23
|
+
- **Browser crashes** in long-running applications
|
|
24
|
+
- **Wasted CPU cycles** from zombie event listeners
|
|
25
|
+
|
|
26
|
+
**In a design system**, memory leaks are especially critical because:
|
|
27
|
+
|
|
28
|
+
- Components may be created/destroyed frequently
|
|
29
|
+
- A leak in one component multiplies across all instances
|
|
30
|
+
- Consumer applications may run for hours/days
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Event Listeners
|
|
35
|
+
|
|
36
|
+
### The Problem
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ❌ BAD - Memory leak
|
|
40
|
+
class MyComponent extends HTMLElement {
|
|
41
|
+
connectedCallback() {
|
|
42
|
+
this.addEventListener("click", this.handleClick)
|
|
43
|
+
document.addEventListener("keydown", this.handleKeyDown)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// No disconnectedCallback = listeners never removed!
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Result:** Every time a component is created and destroyed, listeners accumulate.
|
|
51
|
+
|
|
52
|
+
### The Solution: AbortController
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// ✅ GOOD - Proper cleanup
|
|
56
|
+
class MyComponent extends HTMLElement {
|
|
57
|
+
controller: AbortController
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
super()
|
|
61
|
+
this.controller = new AbortController()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
connectedCallback() {
|
|
65
|
+
const { signal } = this.controller
|
|
66
|
+
|
|
67
|
+
// All listeners use the signal
|
|
68
|
+
this.addEventListener("click", this.handleClick, { signal })
|
|
69
|
+
document.addEventListener("keydown", this.handleKeyDown, { signal })
|
|
70
|
+
window.addEventListener("resize", this.handleResize, { signal })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
disconnectedCallback() {
|
|
74
|
+
// One line removes ALL listeners
|
|
75
|
+
this.controller.abort()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleClick = () => {
|
|
79
|
+
/* ... */
|
|
80
|
+
}
|
|
81
|
+
handleKeyDown = () => {
|
|
82
|
+
/* ... */
|
|
83
|
+
}
|
|
84
|
+
handleResize = () => {
|
|
85
|
+
/* ... */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Benefits of AbortController
|
|
91
|
+
|
|
92
|
+
- ✅ **One line cleanup** - `controller.abort()` removes all listeners
|
|
93
|
+
- ✅ **No manual tracking** - Don't need to remember each listener
|
|
94
|
+
- ✅ **Type-safe** - TypeScript knows about the signal option
|
|
95
|
+
- ✅ **Browser standard** - Supported in all modern browsers
|
|
96
|
+
- ✅ **Impossible to forget** - One controller for all listeners
|
|
97
|
+
|
|
98
|
+
### Pattern for React
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const controller = new AbortController()
|
|
103
|
+
const { signal } = controller
|
|
104
|
+
|
|
105
|
+
element.addEventListener("click", handler, { signal })
|
|
106
|
+
element.addEventListener("focus", otherHandler, { signal })
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
controller.abort() // Cleanup on unmount
|
|
110
|
+
}
|
|
111
|
+
}, [dependencies])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Observers
|
|
117
|
+
|
|
118
|
+
### The Problem
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ BAD - Observer never disconnected
|
|
122
|
+
class MyComponent extends HTMLElement {
|
|
123
|
+
connectedCallback() {
|
|
124
|
+
// Lost reference - can't disconnect later
|
|
125
|
+
const observer = new IntersectionObserver(entries => {
|
|
126
|
+
// ...
|
|
127
|
+
})
|
|
128
|
+
observer.observe(this)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Result:** Observer keeps running even after component is removed.
|
|
134
|
+
|
|
135
|
+
### The Solution: Store as Properties
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// ✅ GOOD - Store observers as properties
|
|
139
|
+
class MyComponent extends HTMLElement {
|
|
140
|
+
iO: IntersectionObserver
|
|
141
|
+
rO: ResizeObserver
|
|
142
|
+
mO: MutationObserver
|
|
143
|
+
|
|
144
|
+
connectedCallback() {
|
|
145
|
+
// Store observers
|
|
146
|
+
this.iO = new IntersectionObserver(entries => {
|
|
147
|
+
entries.forEach(({ isIntersecting, target }) => {
|
|
148
|
+
if (isIntersecting) {
|
|
149
|
+
// Handle intersection
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
this.iO.observe(this)
|
|
154
|
+
|
|
155
|
+
this.rO = new ResizeObserver(entries => {
|
|
156
|
+
entries.forEach(({ contentBoxSize }) => {
|
|
157
|
+
// Handle resize
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
this.rO.observe(this)
|
|
161
|
+
|
|
162
|
+
this.mO = new MutationObserver(mutations => {
|
|
163
|
+
mutations.forEach(({ type }) => {
|
|
164
|
+
// Handle mutation
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
this.mO.observe(this, { childList: true })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
disconnectedCallback() {
|
|
171
|
+
// Disconnect all observers
|
|
172
|
+
this.iO.disconnect()
|
|
173
|
+
this.rO.disconnect()
|
|
174
|
+
this.mO.disconnect()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Optional Observers
|
|
180
|
+
|
|
181
|
+
If an observer might not exist, use optional chaining:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
connectedCallback() {
|
|
185
|
+
// Only create if needed
|
|
186
|
+
if (this.hasAttribute('observe-resize')) {
|
|
187
|
+
this.rO = new ResizeObserver(...)
|
|
188
|
+
this.rO.observe(this)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
disconnectedCallback() {
|
|
193
|
+
// Safe cleanup even if not created
|
|
194
|
+
this.iO?.disconnect()
|
|
195
|
+
this.rO?.disconnect()
|
|
196
|
+
this.mO?.disconnect()
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Pattern for React
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const observer = new IntersectionObserver(entries => {
|
|
205
|
+
// Handle entries
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
observer.observe(elementRef.current)
|
|
209
|
+
|
|
210
|
+
return () => {
|
|
211
|
+
observer.disconnect() // Cleanup on unmount
|
|
212
|
+
}
|
|
213
|
+
}, [])
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Timers
|
|
219
|
+
|
|
220
|
+
### The Problem
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// ❌ BAD - Timeout never cleared
|
|
224
|
+
class MyComponent extends HTMLElement {
|
|
225
|
+
connectedCallback() {
|
|
226
|
+
// Lost reference - can't clear later
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
this.doSomething()
|
|
229
|
+
}, 1000)
|
|
230
|
+
|
|
231
|
+
setInterval(() => {
|
|
232
|
+
this.poll()
|
|
233
|
+
}, 5000) // Runs forever!
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Result:** Timers keep firing even after component is destroyed.
|
|
239
|
+
|
|
240
|
+
### The Solution: Store Timer IDs
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// ✅ GOOD - Store timer IDs
|
|
244
|
+
class MyComponent extends HTMLElement {
|
|
245
|
+
timeout: ReturnType<typeof setTimeout>
|
|
246
|
+
interval: ReturnType<typeof setInterval>
|
|
247
|
+
|
|
248
|
+
connectedCallback() {
|
|
249
|
+
// Store timer IDs
|
|
250
|
+
this.timeout = setTimeout(() => {
|
|
251
|
+
this.doSomething()
|
|
252
|
+
}, 1000)
|
|
253
|
+
|
|
254
|
+
this.interval = setInterval(() => {
|
|
255
|
+
this.poll()
|
|
256
|
+
}, 5000)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
disconnectedCallback() {
|
|
260
|
+
// Clear timers
|
|
261
|
+
clearTimeout(this.timeout)
|
|
262
|
+
clearInterval(this.interval)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Add Timeout Safety to setInterval
|
|
268
|
+
|
|
269
|
+
Prevent infinite loops if a condition is never met:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// ✅ GOOD - Has timeout safety
|
|
273
|
+
interval: ReturnType<typeof setInterval>
|
|
274
|
+
timeout: ReturnType<typeof setTimeout>
|
|
275
|
+
|
|
276
|
+
connectedCallback() {
|
|
277
|
+
// Safety timeout: give up after 2 seconds
|
|
278
|
+
this.timeout = setTimeout(() => {
|
|
279
|
+
clearInterval(this.interval)
|
|
280
|
+
this.interval = null
|
|
281
|
+
console.warn('Timeout: condition never met')
|
|
282
|
+
}, 2000)
|
|
283
|
+
|
|
284
|
+
// Check condition every 5ms
|
|
285
|
+
this.interval = setInterval(() => {
|
|
286
|
+
if (this.checkCondition()) {
|
|
287
|
+
clearTimeout(this.timeout)
|
|
288
|
+
clearInterval(this.interval)
|
|
289
|
+
this.timeout = null
|
|
290
|
+
this.interval = null
|
|
291
|
+
// Success!
|
|
292
|
+
}
|
|
293
|
+
}, 5)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
disconnectedCallback() {
|
|
297
|
+
clearTimeout(this.timeout)
|
|
298
|
+
clearInterval(this.interval)
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Pattern for React
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
doSomething()
|
|
308
|
+
}, 1000)
|
|
309
|
+
|
|
310
|
+
const interval = setInterval(() => {
|
|
311
|
+
poll()
|
|
312
|
+
}, 5000)
|
|
313
|
+
|
|
314
|
+
return () => {
|
|
315
|
+
clearTimeout(timeout)
|
|
316
|
+
clearInterval(interval)
|
|
317
|
+
}
|
|
318
|
+
}, [])
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## React Hooks
|
|
324
|
+
|
|
325
|
+
### The Problem
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// ❌ BAD - No cleanup
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
element.addEventListener('click', handler)
|
|
331
|
+
const observer = new ResizeObserver(...)
|
|
332
|
+
observer.observe(element)
|
|
333
|
+
}, [])
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Result:** Listeners and observers persist after component unmounts.
|
|
337
|
+
|
|
338
|
+
### The Solution: Return Cleanup Functions
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// ✅ GOOD - Proper cleanup
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
const controller = new AbortController()
|
|
344
|
+
element.addEventListener("click", handler, {
|
|
345
|
+
signal: controller.signal,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const observer = new ResizeObserver(entries => {
|
|
349
|
+
// Handle resize
|
|
350
|
+
})
|
|
351
|
+
observer.observe(element)
|
|
352
|
+
|
|
353
|
+
return () => {
|
|
354
|
+
controller.abort() // Remove listeners
|
|
355
|
+
observer.disconnect() // Disconnect observer
|
|
356
|
+
}
|
|
357
|
+
}, [element])
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Separate Controllers per Effect
|
|
361
|
+
|
|
362
|
+
Don't share AbortControllers across multiple effects:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// ❌ BAD - Shared controller causes conflicts
|
|
366
|
+
const Component = () => {
|
|
367
|
+
const controller = new AbortController() // Shared!
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
element.addEventListener("click", handler, { signal: controller.signal })
|
|
371
|
+
return () => controller.abort() // Aborts controller
|
|
372
|
+
}, [])
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
// Uses same signal - will be aborted by first effect!
|
|
376
|
+
element.addEventListener("focus", handler, { signal: controller.signal })
|
|
377
|
+
return () => controller.abort() // Tries to abort again
|
|
378
|
+
}, [])
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ✅ GOOD - Separate controller per effect
|
|
382
|
+
const Component = () => {
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
const controller = new AbortController() // Isolated
|
|
385
|
+
element.addEventListener("click", handler, { signal: controller.signal })
|
|
386
|
+
return () => controller.abort()
|
|
387
|
+
}, [])
|
|
388
|
+
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
const controller = new AbortController() // Another isolated
|
|
391
|
+
element.addEventListener("focus", handler, { signal: controller.signal })
|
|
392
|
+
return () => controller.abort()
|
|
393
|
+
}, [])
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Testing for Leaks
|
|
400
|
+
|
|
401
|
+
### Manual Testing
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
// Test 1: Rapid create/destroy
|
|
405
|
+
for (let i = 0; i < 100; i++) {
|
|
406
|
+
const component = document.createElement("mx-component")
|
|
407
|
+
document.body.append(component)
|
|
408
|
+
component.remove()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Expected: No observers, listeners, or timers remaining
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Chrome DevTools
|
|
415
|
+
|
|
416
|
+
1. **Performance Monitor**
|
|
417
|
+
- Show: Listeners, Nodes, JS Heap
|
|
418
|
+
- Create/destroy components
|
|
419
|
+
- Numbers should return to baseline
|
|
420
|
+
|
|
421
|
+
2. **Memory Profiler**
|
|
422
|
+
- Take heap snapshot before
|
|
423
|
+
- Create 100 components
|
|
424
|
+
- Destroy all components
|
|
425
|
+
- Take heap snapshot after
|
|
426
|
+
- Compare: Detached DOM nodes should be 0
|
|
427
|
+
|
|
428
|
+
3. **Performance Tab**
|
|
429
|
+
- Record timeline
|
|
430
|
+
- Create/destroy components
|
|
431
|
+
- Look for: Continuous timer fires, listener calls
|
|
432
|
+
|
|
433
|
+
### Automated Testing
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
describe("Component Memory Leaks", () => {
|
|
437
|
+
it("should clean up event listeners", () => {
|
|
438
|
+
const component = document.createElement("mx-component")
|
|
439
|
+
document.body.append(component)
|
|
440
|
+
|
|
441
|
+
// Mock addEventListener to track calls
|
|
442
|
+
const addSpy = jest.spyOn(component, "addEventListener")
|
|
443
|
+
const removeSpy = jest.spyOn(component, "removeEventListener")
|
|
444
|
+
|
|
445
|
+
// Trigger connectedCallback
|
|
446
|
+
component.connectedCallback()
|
|
447
|
+
|
|
448
|
+
const listenerCount = addSpy.mock.calls.length
|
|
449
|
+
|
|
450
|
+
// Trigger disconnectedCallback
|
|
451
|
+
component.disconnectedCallback()
|
|
452
|
+
component.remove()
|
|
453
|
+
|
|
454
|
+
// With AbortController, no removeEventListener calls needed
|
|
455
|
+
// Just verify controller.abort() was called
|
|
456
|
+
expect(component.controller).toBeDefined()
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Common Pitfalls
|
|
464
|
+
|
|
465
|
+
### 1. Forgetting disconnectedCallback
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// ❌ BAD
|
|
469
|
+
class MyComponent extends HTMLElement {
|
|
470
|
+
connectedCallback() {
|
|
471
|
+
// Setup...
|
|
472
|
+
}
|
|
473
|
+
// Missing disconnectedCallback!
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ✅ GOOD
|
|
477
|
+
class MyComponent extends HTMLElement {
|
|
478
|
+
connectedCallback() {
|
|
479
|
+
// Setup...
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
disconnectedCallback() {
|
|
483
|
+
// Cleanup...
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### 2. Anonymous Functions
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// ❌ BAD - Can't remove later
|
|
492
|
+
element.addEventListener("click", () => {
|
|
493
|
+
// Can't removeEventListener - anonymous function!
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// ✅ GOOD - Use AbortController instead
|
|
497
|
+
const controller = new AbortController()
|
|
498
|
+
element.addEventListener(
|
|
499
|
+
"click",
|
|
500
|
+
() => {
|
|
501
|
+
// Can abort all listeners at once
|
|
502
|
+
},
|
|
503
|
+
{ signal: controller.signal },
|
|
504
|
+
)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### 3. Conditional Cleanup
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// ❌ BAD - Conditional cleanup can miss things
|
|
511
|
+
disconnectedCallback() {
|
|
512
|
+
if (this.hasObserver) {
|
|
513
|
+
this.observer.disconnect()
|
|
514
|
+
}
|
|
515
|
+
// What if hasObserver is wrong?
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ✅ GOOD - Optional chaining handles it
|
|
519
|
+
disconnectedCallback() {
|
|
520
|
+
this.observer?.disconnect() // Safe even if undefined
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### 4. Nested Components
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// ❌ BAD - Parent doesn't clean up children
|
|
528
|
+
class Parent extends HTMLElement {
|
|
529
|
+
connectedCallback() {
|
|
530
|
+
this.child = document.createElement("mx-child")
|
|
531
|
+
this.append(this.child)
|
|
532
|
+
}
|
|
533
|
+
// Child never removed!
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ✅ GOOD - Clean up children
|
|
537
|
+
class Parent extends HTMLElement {
|
|
538
|
+
child: HTMLElement
|
|
539
|
+
|
|
540
|
+
connectedCallback() {
|
|
541
|
+
this.child = document.createElement("mx-child")
|
|
542
|
+
this.append(this.child)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
disconnectedCallback() {
|
|
546
|
+
this.child?.remove() // Remove child
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 5. Global References
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// ❌ BAD - Global reference never cleared
|
|
555
|
+
class MyComponent extends HTMLElement {
|
|
556
|
+
connectedCallback() {
|
|
557
|
+
window.MyComponent = this // Leaked!
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ✅ GOOD - Clear global references
|
|
562
|
+
class MyComponent extends HTMLElement {
|
|
563
|
+
connectedCallback() {
|
|
564
|
+
window.MyComponent = this
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
disconnectedCallback() {
|
|
568
|
+
if (window.MyComponent === this) {
|
|
569
|
+
window.MyComponent = undefined
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Checklist
|
|
578
|
+
|
|
579
|
+
Before shipping a component, verify:
|
|
580
|
+
|
|
581
|
+
- [ ] **AbortController** used for all event listeners
|
|
582
|
+
- [ ] **disconnectedCallback** implemented
|
|
583
|
+
- [ ] **Observers** stored as properties and disconnected
|
|
584
|
+
- [ ] **Timers** stored and cleared
|
|
585
|
+
- [ ] **Global references** cleaned up
|
|
586
|
+
- [ ] **React effects** return cleanup functions
|
|
587
|
+
- [ ] **Optional chaining** used for nullable cleanup
|
|
588
|
+
- [ ] **Tested** with rapid create/destroy
|
|
589
|
+
- [ ] **DevTools** confirms no leaks
|
|
590
|
+
- [ ] **Documentation** includes cleanup notes
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Related Documentation
|
|
595
|
+
|
|
596
|
+
- [Code Quality Guidelines](./CodeQuality.md)
|
|
597
|
+
- [Component Architecture](./ComponentArchitecture.md)
|
|
598
|
+
- [Security Guidelines](./Security.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Migration Guideline
|
|
2
|
+
|
|
3
|
+
This document provides guides for migrating between major versions of Mixtape. Each guide includes a checklist of steps to follow, as well as links to relevant documentation and resources.
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> This document is a work in progress. We will be adding migration guides for each major version as they are released. If you have any questions or need help with a migration, please reach out to the Mixtape team.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
Welcome to the Mixtape Design System documentation!
|
|
4
|
+
|
|
5
|
+
## 📚 Documentation Structure
|
|
6
|
+
|
|
7
|
+
### Getting Started
|
|
8
|
+
|
|
9
|
+
- [Main README](../README.md) - Installation and basic usage
|
|
10
|
+
- [Introduction](../src/Introduction.mdx) - Design system overview
|
|
11
|
+
|
|
12
|
+
### Developer Guides
|
|
13
|
+
|
|
14
|
+
- **[Code Quality Guidelines](./CodeQuality.md)** - Comprehensive coding standards
|
|
15
|
+
- **[Component Architecture](./ComponentArchitecture.md)** - How to build components
|
|
16
|
+
- **[Memory Management](./MemoryManagement.md)** - Prevent memory leaks
|
|
17
|
+
- **[Security Guidelines](./Security.md)** - Security best practices
|
|
18
|
+
- **[CSS Guidelines](./CSSGuidelines.md)** - Styling conventions
|
|
19
|
+
- **[Testing Guide](./Testing.md)** - Testing strategies
|
|
20
|
+
- **[Contributing Guide](./Contributing.md)** - How to contribute
|
|
21
|
+
|
|
22
|
+
### Reference
|
|
23
|
+
|
|
24
|
+
- **[API Documentation](./API.md)** - Utilities and helpers
|
|
25
|
+
- **[Migration Guides](./Migrations.md)** - Version upgrade guides
|
|
26
|
+
- **[Troubleshooting](./Troubleshooting.md)** - Common issues and solutions
|
|
27
|
+
|
|
28
|
+
### Component Documentation
|
|
29
|
+
|
|
30
|
+
- Component-specific docs are in each component's `README.md`
|
|
31
|
+
- See [src/Atom/README.md](../src/Atom/README.md) for atoms
|
|
32
|
+
- See individual component folders for detailed usage (work in progress)
|
|
33
|
+
|
|
34
|
+
## 🎯 Quick Links
|
|
35
|
+
|
|
36
|
+
### For New Developers
|
|
37
|
+
|
|
38
|
+
1. Read the [Main README](../README.md)
|
|
39
|
+
2. Review [Component Architecture](./ComponentArchitecture.md)
|
|
40
|
+
3. Check [Code Quality Guidelines](./CodeQuality.md)
|
|
41
|
+
4. Browse [Storybook](https://mxds.pnx.io)
|
|
42
|
+
|
|
43
|
+
### For Contributors
|
|
44
|
+
|
|
45
|
+
1. Read [Contributing Guide](./Contributing.md)
|
|
46
|
+
2. Follow [Code Quality Guidelines](./CodeQuality.md)
|
|
47
|
+
3. Run tests: `pnpm test`
|
|
48
|
+
4. Submit PR with conventional commits
|
|
49
|
+
|
|
50
|
+
### For Designers
|
|
51
|
+
|
|
52
|
+
1. Browse [Storybook](https://mxds.pnx.io)
|
|
53
|
+
2. Check [Design Tokens](../src/tokens.js)
|
|
54
|
+
3. Review component READMEs for usage
|
|
55
|
+
|
|
56
|
+
## 📖 External Resources
|
|
57
|
+
|
|
58
|
+
- [Storybook](https://mxds.pnx.io) - Interactive component library
|
|
59
|
+
- [GitHub Repository](https://github.com/previousnext/mxds)
|
|
60
|
+
- [npm Package](https://www.npmjs.com/package/@pnx-mixtape/mxds)
|
|
61
|
+
- [PreviousNext Website](https://www.previousnext.com.au/)
|
|
62
|
+
|
|
63
|
+
## 🔄 Document Status
|
|
64
|
+
|
|
65
|
+
| Document | Status | Last Updated |
|
|
66
|
+
| ------------------------ | ----------- | ------------ |
|
|
67
|
+
| CodeQuality.md | ✅ Complete | 2026-03-05 |
|
|
68
|
+
| ComponentArchitecture.md | ✅ Complete | 2026-03-05 |
|
|
69
|
+
| MemoryManagement.md | ✅ Complete | 2026-02-26 |
|
|
70
|
+
| Security.md | ✅ Complete | 2026-02-26 |
|
|
71
|
+
| CSSGuidelines.md | ✅ Complete | 2026-03-05 |
|
|
72
|
+
| Testing.md | ✅ Complete | 2026-02-26 |
|
|
73
|
+
| Contributing.md | ✅ Complete | 2026-02-26 |
|
|
74
|
+
| API.md | ✅ Complete | 2026-03-05 |
|
|
75
|
+
| Troubleshooting.md | ✅ Complete | 2026-02-26 |
|
|
76
|
+
|
|
77
|
+
## 💡 Contributing to Documentation
|
|
78
|
+
|
|
79
|
+
Found a typo or want to improve our docs? Please submit a PR!
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Clone the repo
|
|
83
|
+
git clone https://github.com/previousnext/mxds.git
|
|
84
|
+
|
|
85
|
+
# Create a branch
|
|
86
|
+
git checkout -b docs/improve-something
|
|
87
|
+
|
|
88
|
+
# Make your changes
|
|
89
|
+
# ...
|
|
90
|
+
|
|
91
|
+
# Commit with conventional commits
|
|
92
|
+
pnpm commit
|
|
93
|
+
|
|
94
|
+
# Push and create PR
|
|
95
|
+
git push origin docs/improve-something
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
**Need help?** Open an issue on [GitHub](https://github.com/previousnext/mxds/issues)
|