@pnx-mixtape/mxds 0.1.0-rc.3 → 0.1.0-rc.5

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 (230) hide show
  1. package/.claude/skills/create-mxds-component/SKILL.md +77 -0
  2. package/DESIGN.md +595 -0
  3. package/README.md +1 -2
  4. package/dist/build/accordion.css +1 -1
  5. package/dist/build/accordion.entry.js +2 -1
  6. package/dist/build/autocomplete.entry.js +1 -1
  7. package/dist/build/back-to-top.css +1 -0
  8. package/dist/build/back-to-top.entry.js +1 -0
  9. package/dist/build/base.css +1 -1
  10. package/dist/build/button.css +1 -1
  11. package/dist/build/card.css +1 -1
  12. package/dist/build/carousel.css +1 -1
  13. package/dist/build/chunks/Accordion-D2wp8hBG.js +44 -0
  14. package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
  15. package/dist/build/chunks/AccordionMobile-D3sTeiM5.js +26 -0
  16. package/dist/build/chunks/Autocomplete-kRo8RNbC.js +135 -0
  17. package/dist/build/chunks/BackToTop-4M-Wl7z3.js +64 -0
  18. package/dist/build/chunks/ClosableAlert-yy84gae6.js +55 -0
  19. package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
  20. package/dist/build/chunks/Dialog-DWoXW-7Z.js +48 -0
  21. package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
  22. package/dist/build/chunks/FeedbackForm-BochXEDx.js +64 -0
  23. package/dist/build/chunks/Gallery-Rkfv7Mbb.js +165 -0
  24. package/dist/build/chunks/GlobalToggle-CzlkpLpc.js +82 -0
  25. package/dist/build/chunks/InPageNavigation-D1Kk-rCm.js +86 -0
  26. package/dist/build/chunks/Navigation-CDQnN61h.js +147 -0
  27. package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
  28. package/dist/build/chunks/Sticky-DoVndENm.js +51 -0
  29. package/dist/build/chunks/Tabs-BDlxCDNh.js +160 -0
  30. package/dist/build/chunks/Translate-bmM38j6z.js +46 -0
  31. package/dist/build/chunks/cookie-compliance-CG6x-TcV.js +119 -0
  32. package/dist/build/chunks/enums-BrHxPZZ_.js +36 -0
  33. package/dist/build/chunks/polyfills-D-qh1a_2.js +33 -0
  34. package/dist/build/chunks/utilities-Dd-08SW6.js +255 -0
  35. package/dist/build/constants.css +1 -1
  36. package/dist/build/content-block.css +1 -1
  37. package/dist/build/cookie-consent.css +1 -1
  38. package/dist/build/cookie-consent.entry.js +1 -1
  39. package/dist/build/dialog.css +1 -1
  40. package/dist/build/dialog.entry.js +1 -1
  41. package/dist/build/drop-menu.entry.js +1 -1
  42. package/dist/build/elements.js +147 -1
  43. package/dist/build/feedback-form.css +1 -0
  44. package/dist/build/feedback-form.entry.js +1 -0
  45. package/dist/build/filters.entry.js +145 -1
  46. package/dist/build/form.css +1 -1
  47. package/dist/build/gallery.entry.js +1 -1
  48. package/dist/build/global-alert.css +1 -1
  49. package/dist/build/global-alert.entry.js +1 -1
  50. package/dist/build/grid.css +1 -1
  51. package/dist/build/header.css +1 -1
  52. package/dist/build/header.entry.js +1 -1
  53. package/dist/build/icon.css +1 -1
  54. package/dist/build/in-page-navigation.entry.js +1 -1
  55. package/dist/build/navigation.css +1 -1
  56. package/dist/build/navigation.entry.js +1 -1
  57. package/dist/build/pagination.css +1 -1
  58. package/dist/build/popover.css +1 -1
  59. package/dist/build/popover.entry.js +1 -1
  60. package/dist/build/react.js +2270 -1
  61. package/dist/build/sticky.entry.js +1 -1
  62. package/dist/build/tabs.css +1 -1
  63. package/dist/build/tabs.entry.js +1 -1
  64. package/dist/build/translate.css +1 -0
  65. package/dist/build/translate.entry.js +1 -0
  66. package/dist/build/utility-list.entry.js +53 -1
  67. package/dist/types/Component/BackToTop/BackToTop.d.ts +20 -0
  68. package/dist/types/Component/BackToTop/BackToTop.d.ts.map +1 -0
  69. package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts +27 -0
  70. package/dist/types/Component/BackToTop/Elements/BackToTop.d.ts.map +1 -0
  71. package/dist/types/Component/BackToTop/back-to-top.entry.d.ts +2 -0
  72. package/dist/types/Component/BackToTop/back-to-top.entry.d.ts.map +1 -0
  73. package/dist/types/Component/DropMenu/DropMenu.d.ts +2 -1
  74. package/dist/types/Component/DropMenu/DropMenu.d.ts.map +1 -1
  75. package/dist/types/Component/Tabs/Components/TabDropMenu.d.ts.map +1 -1
  76. package/dist/types/Component/Tabs/Elements/Tabs.d.ts.map +1 -1
  77. package/dist/types/Component/Translate/Elements/Translate.d.ts +23 -0
  78. package/dist/types/Component/Translate/Elements/Translate.d.ts.map +1 -0
  79. package/dist/types/Component/Translate/Translate.d.ts +37 -0
  80. package/dist/types/Component/Translate/Translate.d.ts.map +1 -0
  81. package/dist/types/Component/Translate/languages.d.ts +11 -0
  82. package/dist/types/Component/Translate/languages.d.ts.map +1 -0
  83. package/dist/types/Component/Translate/translate.entry.d.ts +2 -0
  84. package/dist/types/Component/Translate/translate.entry.d.ts.map +1 -0
  85. package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts +37 -0
  86. package/dist/types/Form/FeedbackForm/Elements/FeedbackForm.d.ts.map +1 -0
  87. package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts +36 -0
  88. package/dist/types/Form/FeedbackForm/FeedbackForm.d.ts.map +1 -0
  89. package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts +2 -0
  90. package/dist/types/Form/FeedbackForm/feedback-form.entry.d.ts.map +1 -0
  91. package/dist/types/Utility/Elements/google-translate-widget.d.ts +91 -0
  92. package/dist/types/Utility/Elements/google-translate-widget.d.ts.map +1 -0
  93. package/dist/types/Utility/utilities.d.ts +2 -0
  94. package/dist/types/Utility/utilities.d.ts.map +1 -1
  95. package/dist/types/elements.d.ts +3 -0
  96. package/dist/types/elements.d.ts.map +1 -1
  97. package/dist/types/enums.d.ts +3 -1
  98. package/dist/types/enums.d.ts.map +1 -1
  99. package/dist/types/react.d.ts +3 -0
  100. package/dist/types/react.d.ts.map +1 -1
  101. package/docs/API.md +1215 -0
  102. package/docs/CSSGuidelines.md +891 -0
  103. package/docs/CodeQuality.md +940 -0
  104. package/docs/ComponentArchitecture.md +714 -0
  105. package/docs/Contributing.md +626 -0
  106. package/docs/MemoryManagement.md +598 -0
  107. package/docs/Migrations.md +6 -0
  108. package/docs/README.md +100 -0
  109. package/docs/Security.md +444 -0
  110. package/docs/Testing.md +739 -0
  111. package/docs/Troubleshooting.md +674 -0
  112. package/package.json +6 -3
  113. package/src/Atom/Button/Button.tsx +1 -1
  114. package/src/Atom/Button/__snapshots__/Button.stories.tsx.snap +2 -2
  115. package/src/Atom/Button/_buttons.css +1 -0
  116. package/src/Atom/Icon/README.md +1 -1
  117. package/src/Atom/Icon/_icon.css +1 -0
  118. package/src/Atom/Icon/images/thumb.svg +1 -0
  119. package/src/Atom/Media/__snapshots__/Media.stories.ts.snap +4 -9
  120. package/src/Atom/Media/media.twig +4 -5
  121. package/src/Atom/_generic.css +0 -10
  122. package/src/Component/Accordion/accordion.css +3 -1
  123. package/src/Component/BackToTop/BackToTop.stories.ts +127 -0
  124. package/src/Component/BackToTop/BackToTop.stories.tsx +53 -0
  125. package/src/Component/BackToTop/BackToTop.tsx +120 -0
  126. package/src/Component/BackToTop/Elements/BackToTop.ts +119 -0
  127. package/src/Component/BackToTop/README.md +164 -0
  128. package/src/Component/BackToTop/__snapshots__/BackToTop.stories.ts.snap +24 -0
  129. package/src/Component/BackToTop/__snapshots__/BackToTop.stories.tsx.snap +3 -0
  130. package/src/Component/BackToTop/back-to-top.css +70 -0
  131. package/src/Component/BackToTop/back-to-top.entry.js +1 -0
  132. package/src/Component/BackToTop/back-to-top.twig +26 -0
  133. package/src/Component/Card/Components/CardContent.tsx +1 -1
  134. package/src/Component/Card/Components/CardMedia.tsx +1 -1
  135. package/src/Component/Card/__snapshots__/Card.stories.tsx.snap +3 -3
  136. package/src/Component/Card/card.css +1 -1
  137. package/src/Component/Carousel/Carousel.stories.ts +66 -36
  138. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -5
  139. package/src/Component/Carousel/carousel.css +1 -1
  140. package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
  141. package/src/Component/ContentBlock/content-block.css +1 -1
  142. package/src/Component/CookieConsent/cookie-consent.css +3 -0
  143. package/src/Component/Dialog/dialog.css +5 -2
  144. package/src/Component/DropMenu/DropMenu.stories.ts +21 -2
  145. package/src/Component/DropMenu/DropMenu.tsx +14 -4
  146. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +3 -3
  147. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +2 -2
  148. package/src/Component/DropMenu/drop-menu.twig +7 -1
  149. package/src/Component/GlobalAlert/global-alert.css +3 -0
  150. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.tsx.snap +1 -1
  151. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +48 -13
  152. package/src/Component/InPageNavigation/in-page-navigation.twig +4 -6
  153. package/src/Component/ListItem/Components/ListItemMedia.tsx +1 -1
  154. package/src/Component/Navigation/_navigation-collapsible.css +3 -1
  155. package/src/Component/Navigation/_navigation-dropdown.css +3 -1
  156. package/src/Component/Pagination/Pagination.stories.ts +57 -27
  157. package/src/Component/Pagination/pagination.css +16 -7
  158. package/src/Component/Popover/__snapshots__/Popover.stories.tsx.snap +3 -3
  159. package/src/Component/Popover/popover.css +3 -1
  160. package/src/Component/Tabs/Components/TabDropMenu.tsx +7 -1
  161. package/src/Component/Tabs/Elements/Tabs.ts +13 -3
  162. package/src/Component/Tabs/Tabs.stories.ts +33 -12
  163. package/src/Component/Tabs/Tabs.stories.tsx +28 -14
  164. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +3 -3
  165. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +2 -2
  166. package/src/Component/Tabs/tabs.css +4 -5
  167. package/src/Component/Translate/Elements/Translate.ts +76 -0
  168. package/src/Component/Translate/README.md +283 -0
  169. package/src/Component/Translate/Translate.stories.ts +87 -0
  170. package/src/Component/Translate/Translate.stories.tsx +77 -0
  171. package/src/Component/Translate/Translate.tsx +213 -0
  172. package/src/Component/Translate/__snapshots__/Translate.stories.ts.snap +18 -0
  173. package/src/Component/Translate/__snapshots__/Translate.stories.tsx.snap +3 -0
  174. package/src/Component/Translate/languages.ts +39 -0
  175. package/src/Component/Translate/translate.css +62 -0
  176. package/src/Component/Translate/translate.entry.js +1 -0
  177. package/src/Component/Translate/translate.twig +44 -0
  178. package/src/Form/Autocomplete/Autocomplete.stories.tsx +2 -1
  179. package/src/Form/FeedbackForm/Elements/FeedbackForm.ts +122 -0
  180. package/src/Form/FeedbackForm/FeedbackForm.stories.ts +99 -0
  181. package/src/Form/FeedbackForm/FeedbackForm.stories.tsx +73 -0
  182. package/src/Form/FeedbackForm/FeedbackForm.tsx +196 -0
  183. package/src/Form/FeedbackForm/README.md +233 -0
  184. package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.ts.snap +125 -0
  185. package/src/Form/FeedbackForm/__snapshots__/FeedbackForm.stories.tsx.snap +5 -0
  186. package/src/Form/FeedbackForm/feedback-form.css +74 -0
  187. package/src/Form/FeedbackForm/feedback-form.entry.js +1 -0
  188. package/src/Form/FeedbackForm/feedback-form.twig +80 -0
  189. package/src/Form/form.css +5 -3
  190. package/src/Layout/Grid/Grid.stories.ts +1 -0
  191. package/src/Layout/Grid/Grid.stories.tsx +26 -0
  192. package/src/Layout/Grid/__snapshots__/Grid.stories.tsx.snap +2 -0
  193. package/src/Layout/Grid/grid.css +12 -0
  194. package/src/Layout/Header/Components/GlobalToggle.tsx +1 -1
  195. package/src/Layout/Header/Header.stories.ts +53 -48
  196. package/src/Layout/Header/Header.stories.tsx +35 -22
  197. package/src/Layout/Header/Header.tsx +2 -2
  198. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +14 -10
  199. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
  200. package/src/Layout/Header/_header.css +4 -2
  201. package/src/Layout/Header/_toggles.css +9 -6
  202. package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
  203. package/src/Utility/Elements/google-translate-widget.ts +293 -0
  204. package/src/Utility/_mixins.css +35 -13
  205. package/src/Utility/global.d.ts +4 -0
  206. package/src/Utility/utilities.ts +2 -0
  207. package/src/_custom-media.css +2 -2
  208. package/src/constants.css +3 -13
  209. package/src/elements.ts +3 -0
  210. package/src/enums.ts +2 -0
  211. package/src/react.ts +3 -0
  212. package/dist/build/chunks/Accordion-GN0sME4W.js +0 -1
  213. package/dist/build/chunks/AccordionGroup-Bzqp5__q.js +0 -1
  214. package/dist/build/chunks/AccordionMobile-DG5jSe8Q.js +0 -1
  215. package/dist/build/chunks/Autocomplete-B4RtkAyh.js +0 -1
  216. package/dist/build/chunks/ClosableAlert-DEhj8Nxm.js +0 -1
  217. package/dist/build/chunks/CookieConsent-BkCMRVST.js +0 -1
  218. package/dist/build/chunks/Dialog-Haahn-Yn.js +0 -1
  219. package/dist/build/chunks/DropMenu-1baQc8Yc.js +0 -1
  220. package/dist/build/chunks/Gallery-D8NGXy2Q.js +0 -1
  221. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +0 -1
  222. package/dist/build/chunks/InPageNavigation-Dyn2TR-N.js +0 -1
  223. package/dist/build/chunks/Navigation-hOrfLq7h.js +0 -1
  224. package/dist/build/chunks/Popover-Cxg-s5Jn.js +0 -1
  225. package/dist/build/chunks/Sticky-BGCuuEJK.js +0 -1
  226. package/dist/build/chunks/Tabs-DYy1YTdU.js +0 -15
  227. package/dist/build/chunks/cookie-compliance-Bbm-0vpj.js +0 -1
  228. package/dist/build/chunks/enums-DLV_1NAf.js +0 -1
  229. package/dist/build/chunks/polyfills-C7pRt54M.js +0 -1
  230. package/dist/build/chunks/utilities-_Bq3TxAQ.js +0 -1
@@ -0,0 +1,714 @@
1
+ # Component Architecture
2
+
3
+ This guide explains how to structure and build components in the Mixtape Design System.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Component Types](#component-types)
8
+ - [Web Components](#web-components)
9
+ - [React Components](#react-components)
10
+ - [Twig Templates](#twig-templates)
11
+ - [Figma Code Connect](#figma-code-connect)
12
+ - [File Organization](#file-organization)
13
+ - [Props & Types](#props--types)
14
+ - [Common Patterns](#common-patterns)
15
+ - [Best Practices](#best-practices)
16
+
17
+ ---
18
+
19
+ ## Component Types
20
+
21
+ The design system supports three component formats:
22
+
23
+ 1. **Web Components** (Custom Elements) - For framework-agnostic JavaScript
24
+ 2. **React Components** - For React applications
25
+ 3. **Twig Templates** - For Drupal integration
26
+
27
+ Most components provide all three formats for maximum flexibility.
28
+
29
+ ---
30
+
31
+ ## Web Components
32
+
33
+ ### Structure Pattern
34
+
35
+ ```typescript
36
+ /**
37
+ * ComponentName
38
+ * @file Brief description of what the component does
39
+ */
40
+
41
+ import { makeAnchor } from "../../../Utility/utilities"
42
+
43
+ export default class ComponentName extends HTMLElement {
44
+ // 1. Properties
45
+ internals_: ElementInternals
46
+ controller: AbortController
47
+ // ... other properties
48
+
49
+ // 2. Constructor
50
+ constructor() {
51
+ super()
52
+ this.internals_ = this.attachInternals()
53
+ this.controller = new AbortController()
54
+ }
55
+
56
+ // 3. Lifecycle Methods
57
+ connectedCallback(): void {
58
+ if (!this.trigger || !this.target) return
59
+
60
+ const { signal } = this.controller
61
+ // Setup event listeners with signal
62
+ this.trigger.addEventListener("click", this.handleClick, { signal })
63
+ }
64
+
65
+ disconnectedCallback(): void {
66
+ // Cleanup - ALWAYS implement this
67
+ this.controller.abort()
68
+ this.observer?.disconnect()
69
+ clearTimeout(this.timeout)
70
+ }
71
+
72
+ // 4. Event Handlers (use arrow functions)
73
+ handleClick = (): void => {
74
+ // Handler logic
75
+ }
76
+
77
+ // 5. Getters (with validation)
78
+ get trigger(): HTMLButtonElement {
79
+ const trigger = this.querySelector("button")
80
+ if (!trigger) {
81
+ throw new Error(`${this.localName} must contain a <button>`)
82
+ }
83
+ return trigger
84
+ }
85
+
86
+ // 6. Helper Methods
87
+ private generatedId(): string {
88
+ return makeAnchor(this.textContent || "component")
89
+ }
90
+ // generatedId() wraps makeAnchor() to produce a stable HTML-safe ID.
91
+ // See API.md → generatedId() for details.
92
+ }
93
+
94
+ // 7. Register the component
95
+ if (!customElements.get("mx-component")) {
96
+ customElements.define("mx-component", ComponentName)
97
+ }
98
+
99
+ // 8. TypeScript declaration
100
+ declare global {
101
+ interface HTMLElementTagNameMap {
102
+ "mx-component": ComponentName
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### Key Requirements
108
+
109
+ 1. **Always extend HTMLElement**
110
+ 2. **Use ElementInternals** for form-associated components
111
+ 3. **Use AbortController** for event listener cleanup
112
+ 4. **Store observers** as properties (iO, rO, mO)
113
+ 5. **Implement disconnectedCallback()** for cleanup
114
+ 6. **Validate DOM queries** in getters with error messages
115
+ 7. **Use arrow functions** for event handlers (preserves `this`)
116
+ 8. **Register with custom name** (mx-prefix)
117
+ 9. **Add TypeScript declarations**
118
+
119
+ ### Example: Accordion Component
120
+
121
+ ```typescript
122
+ export default class Accordion extends HTMLElement {
123
+ internals_: ElementInternals
124
+ controller: AbortController
125
+
126
+ constructor() {
127
+ super()
128
+ this.internals_ = this.attachInternals()
129
+ this.controller = new AbortController()
130
+ }
131
+
132
+ connectedCallback(): void {
133
+ if (!this.details || !this.trigger) return
134
+
135
+ const { signal } = this.controller
136
+ document.addEventListener("beforeprint", this.handleOpen, { signal })
137
+ document.addEventListener("afterprint", this.handleClose, { signal })
138
+ this.handleHash()
139
+ document.addEventListener("hashchange", this.handleHash, { signal })
140
+ }
141
+
142
+ disconnectedCallback(): void {
143
+ this.controller.abort()
144
+ }
145
+
146
+ handleOpen = (): void => {
147
+ if (!this.details) return
148
+ this.details.open = true
149
+ }
150
+
151
+ handleClose = (): void => {
152
+ if (!this.details) return
153
+ this.details.open = false
154
+ }
155
+
156
+ handleHash = (): void => {
157
+ const { hash } = window.location
158
+ if (hash && hash === `#${this.details?.id}`) {
159
+ this.handleOpen()
160
+ }
161
+ }
162
+
163
+ get details(): HTMLDetailsElement | null {
164
+ const details = this.querySelector("details")
165
+ if (!details) {
166
+ throw new Error(`${this.localName} must contain a <details> element.`)
167
+ }
168
+ details.id = details.id || this.generatedId()
169
+ return details
170
+ }
171
+
172
+ get trigger(): HTMLElement | null {
173
+ return this.details?.querySelector("summary")
174
+ }
175
+
176
+ private generatedId(): string {
177
+ const text = this.trigger?.textContent || this.textContent || "accordion"
178
+ return makeAnchor(text)
179
+ }
180
+ }
181
+ ```
182
+
183
+ ---
184
+
185
+ ## React Components
186
+
187
+ ### Structure Pattern
188
+
189
+ ```typescript
190
+ import { JSX, PropsWithChildren } from "react"
191
+ import classNames from "classnames"
192
+ import { AsTypes } from "@pnx-mixtape/ids-shape"
193
+ import { BackgroundStyles } from "../../enums"
194
+
195
+ // 1. Type Definition
196
+ type ComponentProps = PropsWithChildren & {
197
+ // Optional props with defaults
198
+ reversed?: boolean
199
+ block?: boolean
200
+ // Props with type constraints
201
+ as?: AsTypes
202
+ background?: BackgroundStyles
203
+ // Optional string props
204
+ className?: string
205
+ }
206
+
207
+ // 2. Component with destructured props
208
+ const Component = ({
209
+ children,
210
+ reversed = false, // Default values
211
+ block = false,
212
+ as = AsTypes.DIV,
213
+ background = null,
214
+ className = null,
215
+ }: ComponentProps): JSX.Element => {
216
+ // 3. Dynamic element type
217
+ const Tag: JSX.ElementType = `${as}` as unknown as JSX.ElementType
218
+
219
+ return (
220
+ <Tag className="mx-container">
221
+ <article
222
+ className={classNames("mx-component", className, {
223
+ "mx-component--reversed": reversed,
224
+ "mx-component--block": block,
225
+ [`mx-background--${background}`]: background,
226
+ })}
227
+ >
228
+ {children}
229
+ </article>
230
+ </Tag>
231
+ )
232
+ }
233
+
234
+ export default Component
235
+ ```
236
+
237
+ ### Key Requirements
238
+
239
+ 1. **Use TypeScript types** for all props
240
+ 2. **Extend PropsWithChildren** if accepting children
241
+ 3. **Provide default values** in destructuring
242
+ 4. **Use classNames library** for conditional classes
243
+ 5. **Return JSX.Element** type explicitly
244
+ 6. **Export as default**
245
+ 7. **Use semantic HTML** elements
246
+
247
+ ### Example: Card Component
248
+
249
+ ```typescript
250
+ import { JSX, PropsWithChildren } from "react"
251
+ import classNames from "classnames"
252
+ import { BackgroundStyles } from "../../enums"
253
+ import { AsTypes } from "@pnx-mixtape/ids-shape"
254
+
255
+ type CardProps = PropsWithChildren & {
256
+ reversed?: boolean
257
+ block?: boolean
258
+ as?: AsTypes
259
+ background?: BackgroundStyles
260
+ className?: string
261
+ }
262
+
263
+ const Card = ({
264
+ children,
265
+ reversed = null,
266
+ block = false,
267
+ as = AsTypes.DIV,
268
+ background = null,
269
+ className = null,
270
+ }: CardProps): JSX.Element => {
271
+ const Tag: JSX.ElementType = `${as}` as unknown as JSX.ElementType
272
+
273
+ return (
274
+ <Tag className="mx-container">
275
+ <article
276
+ className={classNames("mx-card", className, {
277
+ "mx-card--reversed": reversed,
278
+ "mx-card--block": block,
279
+ [`mx-background--${background}`]: background,
280
+ })}
281
+ >
282
+ {children}
283
+ </article>
284
+ </Tag>
285
+ )
286
+ }
287
+
288
+ export default Card
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Twig Templates
294
+
295
+ ### Structure Pattern
296
+
297
+ ```twig
298
+ {% set baseClass = 'mx-component' %}
299
+ {% set classes = [baseClass] %}
300
+ {% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
301
+
302
+ <div{{ attributes }}>
303
+ {{ content }}
304
+ </div>
305
+ ```
306
+
307
+ ### Key Requirements
308
+
309
+ 1. **Use `create_attribute()`** for the root element's attributes, merging any passed-in `attributes`
310
+ 2. **Prefer semantic HTML** elements, matching the equivalent React/Web Component markup
311
+ 3. **Be selective with `{% for %}` loops** — see below, this is the most common way a Twig template breaks Drupal Canvas compatibility
312
+
313
+ ### Avoid `{% for %}` loops over repeatable content
314
+
315
+ These Twig templates are the source that gets wrapped as Drupal Canvas (Experience Builder) components. Canvas's slot model hands a template **one pre-rendered value per slot**, never an array to iterate — an editor drops multiple blocks into a region, and Drupal concatenates their rendered output before the template ever sees it. A `{% for item in items %}` loop assumes the opposite (an array prop it can iterate), which breaks under Canvas: printing an array directly (`{{ items }}` without joining first) renders as the literal word `Array` in real Drupal/PHP Twig, or inserts stray commas in the Twig.js engine Storybook uses.
316
+
317
+ - **Never accept an array prop for repeatable content.** Take a single pre-joined string instead, and print it directly — `{{ items }}`, no `for`, no `join`, no `raw`. Build that string at the call site: `.stories.ts` files join an array of already-rendered items before passing them as an arg, e.g. `items: [AccordionItem({...}), AccordionItem({...})].join("")`.
318
+ - **If each item needs its own wrapper element** (e.g. `<li>`, a slide container), give that wrapper its own sibling sub-component rather than generating it from a loop in the parent. See `accordion-item.twig`, `tab-item.twig`, and `tag-item.twig`: the parent template is just `{{ items }}`, and each item component owns its complete root element, wrapper included.
319
+ - **`{% for %}` loops over a scalar array are fine** — e.g. building CSS modifier classes from a `modifiers: string[]` prop. This rule is about repeatable *content* (things an editor would add/remove/reorder as blocks), not plain data.
320
+ - **Exceptions**: native/technical markup that isn't a Canvas-editable "list" — `<option>` in a `<select>`, `<source>` in a `<picture>` — can keep a loop; these aren't things an editor rearranges as blocks.
321
+
322
+ ```twig
323
+ {# accordion.twig — parent, no loop #}
324
+ <div class="mx-accordions">
325
+ {{ items }}
326
+ </div>
327
+ ```
328
+
329
+ ```twig
330
+ {# accordion-item.twig — owns its own <details> root, including any wrapper #}
331
+ <mx-accordion>
332
+ <details{{ attributes }}>
333
+ <summary class="mx-accordion__toggle">{{ title }}</summary>
334
+ <div class="mx-accordion__content">{{ content }}</div>
335
+ </details>
336
+ </mx-accordion>
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Figma Code Connect
342
+
343
+ Every component with a React implementation has a matching `Component.figma.ts` — a [Figma Code Connect](https://www.figma.com/code-connect-docs/) mapping that lets designers copy real component code straight from the Figma frame. It maps the React component only (`source` always points at the `.tsx` file); Twig/Web-Component variants aren't represented in Figma.
344
+
345
+ ### Structure Pattern
346
+
347
+ ```typescript
348
+ // url=https://www.figma.com/design/aGH90DVE6lrFVdLfDCHnFK/mxds---Design-System?node-id=65-234
349
+ // source=src/Component/Tag/Tag.tsx
350
+ // component=Tag
351
+ import figma from "figma"
352
+
353
+ const instance = figma.selectedInstance
354
+
355
+ const dismissible = instance.getEnum("Dismissible", { Off: false, On: true })
356
+
357
+ // Real children — a proper Content SLOT (not a fixed text layer)
358
+ const content = instance.getSlot("Content")
359
+
360
+ export default {
361
+ example: figma.code`<Tag>${content}</Tag>`,
362
+ imports: ['import { Tag } from "@pnx-mixtape/mxds/react"'],
363
+ id: "tag",
364
+ metadata: {
365
+ nestable: true,
366
+ },
367
+ }
368
+ ```
369
+
370
+ - **Header comments** (`url`/`source`/`component`) are read by the Figma Code Connect CLI — `url` must point at the real frame's node, not a placeholder.
371
+ - **`figma.selectedInstance`** reads the actual Figma layer's variant properties (`getEnum`, `getBoolean`, `getSlot`, `getInstanceSwap`, …) so the exported snippet reflects whatever variant a designer has selected.
372
+ - **A slot must be interpolated directly inside the `figma.code` tagged template**, never assigned through an untagged intermediate string first — doing so passes `tsc` but silently renders as `[object Object]` in the real published snippet.
373
+ - **`id`** is the kebab-case component id used by the Figma library; `metadata.nestable` marks whether the component can be nested inside another mapped component's slot.
374
+
375
+ `tools/scaffold-component.mjs` generates a **shell only** — `url=TODO`, no real `getEnum`/`getSlot` calls — since those depend on the actual Figma frame. Populate it for real using the `code-to-figma-code-connect` skill rather than guessing the shape; it also covers validating (`figma connect parse`) and publishing.
376
+
377
+ ---
378
+
379
+ ## File Organization
380
+
381
+ ### Component Directory Structure
382
+
383
+ ```
384
+ Component/
385
+ ├── README.md # Component documentation
386
+ ├── Component.tsx # React component
387
+ ├── Component.stories.tsx # React Storybook stories
388
+ ├── Component.figma.ts # Figma Code Connect mapping (React only)
389
+ ├── component.twig # Twig template
390
+ ├── Component.stories.ts # Twig Storybook stories
391
+ ├── component.css # Public CSS API
392
+ ├── _component-partial.css # Internal CSS
393
+ ├── Elements/ # Web Components (if needed)
394
+ │ └── ComponentElement.ts
395
+ ├── Components/ # Sub-components (if needed)
396
+ │ └── ComponentPart.tsx
397
+ └── __snapshots__/ # Visual regression tests
398
+ ├── Component.stories.ts.snap
399
+ └── Component.stories.tsx.snap
400
+ ```
401
+
402
+ ### File Naming Conventions
403
+
404
+ - **React**: `PascalCase.tsx` (Component.tsx)
405
+ - **Web Components**: `PascalCase.ts` (Accordion.ts)
406
+ - **Twig**: `kebab-case.twig` (accordion.twig)
407
+ - **CSS**: `kebab-case.css` (accordion.css)
408
+ - **Partials**: `_kebab-case.css` (_accordion-base.css)
409
+ - **Stories**: Match component name + `.stories.ts/tsx`
410
+ - **Figma Code Connect**: `PascalCase.figma.ts` (Component.figma.ts) — only for components with a React implementation
411
+
412
+ ---
413
+
414
+ ## Props & Types
415
+
416
+ ### Type Definitions
417
+
418
+ ```typescript
419
+ import { PropsWithChildren } from "react"
420
+ import { AsTypes } from "@pnx-mixtape/ids-shape"
421
+
422
+ // Simple props
423
+ type SimpleProps = {
424
+ title: string
425
+ active: boolean
426
+ }
427
+
428
+ // With children
429
+ type WithChildrenProps = PropsWithChildren & {
430
+ className?: string
431
+ }
432
+
433
+ // With element type
434
+ type FlexibleProps = PropsWithChildren & {
435
+ as?: AsTypes
436
+ }
437
+
438
+ // With union types
439
+ type VariantProps = {
440
+ variant: "primary" | "secondary" | "tertiary"
441
+ size: "small" | "medium" | "large"
442
+ }
443
+
444
+ // With optional complex types
445
+ type ComplexProps = PropsWithChildren & {
446
+ background?: BackgroundStyles
447
+ modifiers?: ButtonModifiers[]
448
+ onClick?: (event: MouseEvent) => void
449
+ }
450
+ ```
451
+
452
+ ### Default Values
453
+
454
+ ```typescript
455
+ // ✅ Good - Defaults in destructuring
456
+ const Component = ({
457
+ variant = "primary",
458
+ size = "medium",
459
+ active = false,
460
+ }: Props) => {
461
+ // ...
462
+ }
463
+
464
+ // ❌ Avoid - Defaults in type definition (TypeScript only, not runtime)
465
+ type Props = {
466
+ variant?: "primary" | "secondary" // No default here
467
+ }
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Common Patterns
473
+
474
+ ### Disclosure Pattern
475
+
476
+ Used in: AccordionDiv, GlobalToggle, Navigation submenus (web component); Accordion, Header, Navigation DropdownLevel (React)
477
+
478
+ **Web component** — `DisclosureWidget` handles the toggle/attribute bookkeeping:
479
+
480
+ ```typescript
481
+ import { DisclosureWidget } from "../../../Utility/utilities"
482
+
483
+ connectedCallback(): void {
484
+ this.widget = new DisclosureWidget(this.trigger, this.target)
485
+ this.widget.init()
486
+
487
+ const { signal } = this.controller
488
+ this.trigger.addEventListener('disclosure-toggle', this.handleToggle, { signal })
489
+ }
490
+
491
+ handleToggle = ({ detail }: CustomEvent<DisclosureWidgetEvent>): void => {
492
+ const { isOpen } = detail
493
+ // Handle state change
494
+ }
495
+ ```
496
+
497
+ **React** — `useDisclosureState` (react-stately) tracks the boolean; we hand-write the button/panel markup and `aria-expanded`/`inert` wiring ourselves rather than using the full `useDisclosure` hook (see [React Aria / React Stately](#react-aria--react-stately) below for why):
498
+
499
+ ```typescript
500
+ import { useDisclosureState } from "react-stately"
501
+
502
+ const { isExpanded, toggle } = useDisclosureState({ defaultExpanded: false })
503
+
504
+ return (
505
+ <>
506
+ <button aria-expanded={isExpanded} onClick={() => toggle()}>...</button>
507
+ <div inert={!isExpanded}>...</div>
508
+ </>
509
+ )
510
+ ```
511
+
512
+ ### Keyboard Navigation Pattern
513
+
514
+ Used in: Navigation flyout menus (`Keyboard` utility), DropMenu (`moveFocus`) — both web component; DropMenu, Tabs (React)
515
+
516
+ **Web component** — the shared `Keyboard` utility handles roving focus between links. `Navigation/Elements/Navigation.ts` is currently its only consumer — don't reach for it in new web components without a specific reason, prefer a small dedicated implementation instead. `DropMenu/Elements/DropMenu.ts`'s `moveFocus` is an example of that smaller alternative: a few lines of index math scoped to its own item list, rather than a shared class:
517
+
518
+ ```typescript
519
+ import { Keyboard } from "../../../Utility/utilities"
520
+
521
+ const keyboard = new Keyboard()
522
+
523
+ useEffect(() => {
524
+ if (!containerRef.current || !open) return
525
+
526
+ const items = containerRef.current.querySelectorAll("button")
527
+ keyboard.attachMenu(containerRef.current, items)
528
+
529
+ const controller = new AbortController()
530
+ keyboard.attachEventListeners(controller.signal)
531
+
532
+ return () => {
533
+ controller.abort()
534
+ keyboard.detachMenu()
535
+ }
536
+ }, [open])
537
+ ```
538
+
539
+ **React** — wrap the container in a `FocusScope`, then call `useFocusManager()` from each item to move focus in response to arrow keys:
540
+
541
+ ```typescript
542
+ import { FocusScope, useFocusManager } from "react-aria"
543
+
544
+ // Container:
545
+ <FocusScope>{children}</FocusScope>
546
+
547
+ // Each item:
548
+ const focusManager = useFocusManager()
549
+ const onKeyDown = (event: KeyboardEvent) => {
550
+ switch (event.key) {
551
+ case "ArrowDown":
552
+ focusManager?.focusNext({ wrap: true, tabbable: true })
553
+ break
554
+ case "ArrowUp":
555
+ focusManager?.focusPrevious({ wrap: true, tabbable: true })
556
+ break
557
+ }
558
+ }
559
+ ```
560
+
561
+ > `tabbable: true` matters if any item can have `tabindex="-1"` (e.g. hidden/disabled) — the default "focusable" mode would otherwise still stop on them. Omit it for roving-tabindex widgets like Tabs, where non-active items are `tabindex="-1"` by design and *should* still be reachable by arrow keys.
562
+
563
+ ### React Aria / React Stately
564
+
565
+ Before reaching for a react-aria/react-stately hook, check whether it does more than you asked for. Several of the higher-level "behavior" hooks assume a floating-overlay UI and carry side effects that don't fit this design system's Twig-parity, own-your-markup model:
566
+
567
+ - **Prefer the *state* hook (react-stately) over the matching *behavior* hook (react-aria) when the behavior hook assumes conventions we don't use.** `useDisclosureState` just tracks a boolean; the full `useDisclosure` hook imperatively toggles `hidden="until-found"` and injects `--disclosure-panel-width`/`--disclosure-panel-height` inline styles for its own animation model — which fights our CSS-driven `[data-open]`/`inert` collapse. `useComboBoxState` builds the collection/selection bookkeeping; `useComboBox` additionally calls `ariaHideOutside()` whenever open, `aria-hide`-ing the *entire rest of the page* — correct for a floating popover, wrong for an inline listbox like `Autocomplete`'s.
568
+ - **Check what a hook's returned methods actually alias to before using them** — a method's name doesn't always match its documented behavior. `useComboBoxState`'s `close` is aliased to `commitValue`, which resets the input to whatever the internal selection-key system thinks is "selected" — surprising if you handle selection yourself and never populate that system (as `Autocomplete` does). Read the hook's source, not just its `.d.ts`, when something behaves unexpectedly.
569
+ - **Verify empirically, not just by types.** Several of the surprises above only show up in rendered DOM or test output, not the type signatures — run the component's play-function tests (or open it in Storybook) after adopting a new hook.
570
+
571
+ ### Observer Pattern
572
+
573
+ Used in: Carousel, Sticky, IntersectionLoader
574
+
575
+ ```typescript
576
+ connectedCallback(): void {
577
+ // IntersectionObserver
578
+ this.iO = new IntersectionObserver(entries => {
579
+ entries.forEach(({ isIntersecting }) => {
580
+ if (isIntersecting) this.load()
581
+ })
582
+ })
583
+ this.iO.observe(this.element)
584
+
585
+ // ResizeObserver
586
+ this.rO = new ResizeObserver(entries => {
587
+ entries.forEach(() => this.handleResize())
588
+ })
589
+ this.rO.observe(this.element)
590
+ }
591
+
592
+ disconnectedCallback(): void {
593
+ this.iO?.disconnect()
594
+ this.rO?.disconnect()
595
+ }
596
+ ```
597
+
598
+ ### Lazy Loading Pattern
599
+
600
+ Used in: IntersectionLoader, BreakpointLoader
601
+
602
+ ```typescript
603
+ import { IntersectionLoader } from "../../../Utility/utilities"
604
+
605
+ // Load component on intersection
606
+ IntersectionLoader.create("[data-lazy]", async () => {
607
+ const { default: Component } = await import("./Component")
608
+ // Initialize component
609
+ })
610
+
611
+ // Load component at breakpoint
612
+ import { BreakpointLoader } from "../../../Utility/utilities"
613
+
614
+ BreakpointLoader.create(
615
+ "[data-responsive]",
616
+ async () => {
617
+ const { default: Component } = await import("./Component")
618
+ // Initialize component
619
+ },
620
+ { mediaQuery: "(min-width: 768px)" },
621
+ )
622
+ ```
623
+
624
+ ### Effect Timing Pattern (`useEffect` vs `useLayoutEffect`)
625
+
626
+ Used in: Filters (right-variant resize-reopen), Tabs (URL `?tab=` sync), Accordion (URL `#hash` deep-link), PaginationContext (URL `?page=` sync on load), Dialog + Gallery (imperative `showModal()`/`close()` + scroll-to-slide), Autocomplete (scroll the active option into view) — all React.
627
+
628
+ If a `useEffect` sets state (or calls an imperative DOM API) whose result is part of the *same render's* visible output, the browser can paint the stale value for one frame before the effect corrects it — a real, if brief, flash of wrong content: a tab that briefly shows the wrong panel before a `?tab=` URL param corrects it, an accordion that renders closed before a `#hash` deep-link expands it, a dialog that pops open at the wrong scroll position for a frame before jumping to the right slide. Use `useIsomorphicLayoutEffect` (`src/Utility/Hooks/useIsomorphicLayoutEffect.ts`) instead of a bare `useEffect` for these — it's `useLayoutEffect` in the browser (runs before paint) and falls back to `useEffect` during server rendering. The isomorphic fallback matters here specifically: a bare `useLayoutEffect` logs a React warning if it ever actually executes on the server, and SSR/hydration is a real, documented consumption pattern for this library (see [Troubleshooting.md](./Troubleshooting.md)'s "React hydration errors" section) — don't import `useLayoutEffect` from `"react"` directly in a component.
629
+
630
+ ```typescript
631
+ import useIsomorphicLayoutEffect from "../../Utility/Hooks/useIsomorphicLayoutEffect"
632
+
633
+ useIsomorphicLayoutEffect(() => {
634
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
635
+ // oxlint-disable-next-line react/set-state-in-effect -- synchronizes state with the URL, an external system
636
+ setActiveTab(tabParam)
637
+ }, [tabParam])
638
+ ```
639
+
640
+ Web Components don't need this distinction — `connectedCallback`/`attributeChangedCallback` already run synchronously as part of the DOM update, with no passive-vs-layout effect split to get wrong.
641
+
642
+ **Only convert effects with a genuine flash risk.** Most effects — data fetching, subscribing to events, async measurement, cleanup-only — are correctly `useEffect` and gain nothing from running earlier. A cautionary tale: an attempt to fix Carousel's initial nav-button visibility this way backfired. Carousel already measures overflow via a `ResizeObserver`, whose first callback fires close to paint time on its own; adding a *second*, layout-effect-driven call to the same measurement duplicated that work, and for the pagination/counter variants it duplicated an untracked `setTimeout` per mount too — which measurably delayed unrelated stories' tests when several ran together in the same file. That change was reverted. Don't reach for `useIsomorphicLayoutEffect` where a native API (an observer, a browser default) already does the same measurement early enough on its own — only add it where nothing else does.
643
+
644
+ ---
645
+
646
+ ## Best Practices
647
+
648
+ ### Do's ✅
649
+
650
+ 1. **Use semantic HTML** - `<nav>`, `<main>`, `<article>`, `<section>`
651
+ 2. **Validate DOM queries** - Throw errors with helpful messages
652
+ 3. **Generate IDs when missing** - Use makeAnchor utility
653
+ 4. **Clean up resources** - Always implement cleanup
654
+ 5. **Use arrow functions** for handlers - Preserves `this` binding
655
+ 6. **Add ARIA attributes** - Support screen readers
656
+ 7. **Support keyboard navigation** - Tab, Enter, Escape, Arrows
657
+ 8. **Document props** - JSDoc comments for complex props
658
+ 9. **Write stories** - For every component variant
659
+ 10. **Test accessibility** - Use Storybook a11y addon
660
+
661
+ ### Don'ts ❌
662
+
663
+ 1. **Don't forget cleanup** - Always disconnect observers
664
+ 2. **Don't use inline styles** - Use CSS classes
665
+ 3. **Don't hardcode IDs** - Generate or allow customization
666
+ 4. **Don't ignore errors** - Log in development, handle in production
667
+ 5. **Don't use `any` type** - Use proper TypeScript types
668
+ 6. **Don't nest BEM classes** - Keep selectors flat
669
+ 7. **Don't mutate props** - Props are read-only
670
+ 8. **Don't use `innerHTML`** - XSS vulnerability
671
+ 9. **Don't forget mobile** - Mobile-first responsive design
672
+ 10. **Don't skip documentation** - Update README with changes
673
+ 11. **Don't loop over repeatable content in Twig** - Pass a pre-joined string instead; see [Twig Templates](#twig-templates)
674
+ 12. **Don't use a bare `useEffect` when it sets state that's part of the same render's visible output** - use `useIsomorphicLayoutEffect` instead, and don't duplicate a measurement a native observer/API already does early on its own; see [Effect Timing Pattern](#effect-timing-pattern-useeffect-vs-uselayouteffect)
675
+
676
+ ---
677
+
678
+ ## Component Checklist
679
+
680
+ Before submitting a new component, verify:
681
+
682
+ - [ ] TypeScript types defined
683
+ - [ ] Props have defaults where appropriate
684
+ - [ ] Cleanup implemented (disconnectedCallback/useEffect cleanup)
685
+ - [ ] Input validation using validators utility
686
+ - [ ] Error handling with try-catch
687
+ - [ ] Environment-specific logging
688
+ - [ ] BEM class names
689
+ - [ ] Responsive styles with custom media
690
+ - [ ] Logical CSS properties
691
+ - [ ] ARIA labels and roles
692
+ - [ ] Keyboard navigation support
693
+ - [ ] React component (if applicable)
694
+ - [ ] Web Component (if applicable)
695
+ - [ ] Twig template (if applicable) — repeatable content passed as a joined string, not an array/`{% for %}` loop
696
+ - [ ] Storybook stories created
697
+ - [ ] README documentation
698
+ - [ ] Snapshot tests passing
699
+ - [ ] No TypeScript errors
700
+ - [ ] Linting passes (`pnpm lint`)
701
+ - [ ] Manual testing in Storybook
702
+ - [ ] Accessibility tested (a11y addon)
703
+ - [ ] Mobile tested
704
+ - [ ] Cross-browser tested
705
+
706
+ ---
707
+
708
+ ## Related Documentation
709
+
710
+ - [Code Quality Guidelines](./CodeQuality.md)
711
+ - [Memory Management](./MemoryManagement.md)
712
+ - [CSS Guidelines](./CSSGuidelines.md)
713
+ - [Testing Guide](./Testing.md)
714
+ - [API Documentation](./API.md)