@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.
- package/.claude/skills/create-mxds-component/SKILL.md +77 -0
- package/DESIGN.md +595 -0
- package/README.md +1 -2
- package/dist/build/accordion.css +1 -1
- 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/carousel.css +1 -1
- package/dist/build/chunks/Accordion-D2wp8hBG.js +44 -0
- package/dist/build/chunks/AccordionGroup-BYtoAFxg.js +47 -0
- package/dist/build/chunks/AccordionMobile-D3sTeiM5.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-yy84gae6.js +55 -0
- package/dist/build/chunks/CookieConsent-wd8gQ2Oe.js +18 -0
- package/dist/build/chunks/Dialog-DWoXW-7Z.js +48 -0
- package/dist/build/chunks/DropMenu-CSTZq0Zt.js +68 -0
- package/dist/build/chunks/FeedbackForm-BochXEDx.js +64 -0
- package/dist/build/chunks/Gallery-Rkfv7Mbb.js +165 -0
- package/dist/build/chunks/GlobalToggle-CzlkpLpc.js +82 -0
- package/dist/build/chunks/InPageNavigation-D1Kk-rCm.js +86 -0
- package/dist/build/chunks/Navigation-CDQnN61h.js +147 -0
- package/dist/build/chunks/Popover-CiTAftNU.js +30 -0
- package/dist/build/chunks/Sticky-DoVndENm.js +51 -0
- package/dist/build/chunks/Tabs-BDlxCDNh.js +160 -0
- package/dist/build/chunks/Translate-bmM38j6z.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-Dd-08SW6.js +255 -0
- package/dist/build/constants.css +1 -1
- package/dist/build/content-block.css +1 -1
- package/dist/build/cookie-consent.css +1 -1
- package/dist/build/cookie-consent.entry.js +1 -1
- package/dist/build/dialog.css +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.css +1 -1
- package/dist/build/global-alert.entry.js +1 -1
- package/dist/build/grid.css +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.css +1 -1
- package/dist/build/popover.entry.js +1 -1
- package/dist/build/react.js +2270 -1
- package/dist/build/sticky.entry.js +1 -1
- package/dist/build/tabs.css +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/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/docs/API.md +1215 -0
- package/docs/CSSGuidelines.md +891 -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 +6 -3
- 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/Media/__snapshots__/Media.stories.ts.snap +4 -9
- package/src/Atom/Media/media.twig +4 -5
- package/src/Atom/_generic.css +0 -10
- package/src/Component/Accordion/accordion.css +3 -1
- 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/Carousel/carousel.css +1 -1
- package/src/Component/ContentBlock/Components/ContentBlockMedia.tsx +1 -1
- package/src/Component/ContentBlock/content-block.css +1 -1
- package/src/Component/CookieConsent/cookie-consent.css +3 -0
- package/src/Component/Dialog/dialog.css +5 -2
- 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/GlobalAlert/global-alert.css +3 -0
- 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/_navigation-collapsible.css +3 -1
- package/src/Component/Navigation/_navigation-dropdown.css +3 -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/Popover/popover.css +3 -1
- 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/Tabs/tabs.css +4 -5
- package/src/Component/Translate/Elements/Translate.ts +76 -0
- package/src/Component/Translate/README.md +283 -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 +5 -3
- package/src/Layout/Grid/Grid.stories.ts +1 -0
- package/src/Layout/Grid/Grid.stories.tsx +26 -0
- package/src/Layout/Grid/__snapshots__/Grid.stories.tsx.snap +2 -0
- package/src/Layout/Grid/grid.css +12 -0
- 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 +14 -10
- package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -2
- package/src/Layout/Header/_header.css +4 -2
- package/src/Layout/Header/_toggles.css +9 -6
- package/src/Layout/Page/__snapshots__/Page.stories.tsx.snap +1 -1
- package/src/Utility/Elements/google-translate-widget.ts +293 -0
- package/src/Utility/_mixins.css +35 -13
- package/src/Utility/global.d.ts +4 -0
- package/src/Utility/utilities.ts +2 -0
- package/src/_custom-media.css +2 -2
- package/src/constants.css +3 -13
- package/src/elements.ts +3 -0
- package/src/enums.ts +2 -0
- package/src/react.ts +3 -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,940 @@
|
|
|
1
|
+
# Code Quality Guidelines
|
|
2
|
+
|
|
3
|
+
This document provides a quick reference for code quality standards. For detailed guides, see the focused documentation below.
|
|
4
|
+
|
|
5
|
+
## 📚 Focused Documentation
|
|
6
|
+
|
|
7
|
+
- **[Component Architecture](./ComponentArchitecture.md)** - How to build Web Components and React components
|
|
8
|
+
- **[Memory Management](./MemoryManagement.md)** - Prevent memory leaks (essential reading!)
|
|
9
|
+
- **[Security Guidelines](./Security.md)** - XSS prevention, input validation, secure practices
|
|
10
|
+
- **[CSS Guidelines](./CSSGuidelines.md)** - BEM, tokens, logical properties, custom media
|
|
11
|
+
- **[Testing Guide](./Testing.md)** - Storybook, snapshots, accessibility testing
|
|
12
|
+
- **[Contributing Guide](./Contributing.md)** - Git workflow, commit conventions
|
|
13
|
+
- **[API Documentation](./API.md)** - Validators utility and helper functions
|
|
14
|
+
- **[Troubleshooting](./Troubleshooting.md)** - Common issues and solutions
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Quick Reference](#quick-reference)
|
|
19
|
+
- [Memory Management](#memory-management)
|
|
20
|
+
- [Security](#security)
|
|
21
|
+
- [TypeScript](#typescript)
|
|
22
|
+
- [Component Architecture](#component-architecture)
|
|
23
|
+
- [CSS & Styling](#css--styling)
|
|
24
|
+
- [Testing](#testing)
|
|
25
|
+
- [Code Formatting](#code-formatting)
|
|
26
|
+
- [Git & Commits](#git--commits)
|
|
27
|
+
- [Validation](#validation)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Reference
|
|
32
|
+
|
|
33
|
+
### Essential Rules
|
|
34
|
+
|
|
35
|
+
1. ✅ **Always use AbortController** for event listeners → [Details](./MemoryManagement.md#event-listeners)
|
|
36
|
+
2. ✅ **Always implement disconnectedCallback()** → [Details](./MemoryManagement.md#the-solution-store-as-properties)
|
|
37
|
+
3. ✅ **Never use dangerouslySetInnerHTML or innerHTML** → [Details](./Security.md#xss-prevention)
|
|
38
|
+
4. ✅ **Always validate input** with validators utility → [Details](./Security.md#input-validation)
|
|
39
|
+
5. ✅ **Use BEM naming** for all CSS → [Details](./CSSGuidelines.md#bem-naming)
|
|
40
|
+
6. ✅ **Use logical properties** not physical → [Details](./CSSGuidelines.md#logical-properties)
|
|
41
|
+
7. ✅ **Import custom media** in every CSS file → [Details](./CSSGuidelines.md#custom-media)
|
|
42
|
+
8. ✅ **Follow TypeScript types** strictly → [Details](#typescript)
|
|
43
|
+
9. ✅ **Write Storybook stories** for all components → [Details](./Testing.md)
|
|
44
|
+
10. ✅ **Use conventional commits** → [Details](#git--commits)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Memory Management
|
|
49
|
+
|
|
50
|
+
> 📖 **[Full Memory Management Guide](./MemoryManagement.md)**
|
|
51
|
+
|
|
52
|
+
### Critical Rules
|
|
53
|
+
|
|
54
|
+
1. ✅ **Use AbortController** for event listeners
|
|
55
|
+
2. ✅ **Store observers** as properties (iO, rO, mO)
|
|
56
|
+
3. ✅ **Store timer IDs** and clear them
|
|
57
|
+
4. ✅ **Implement disconnectedCallback()** always
|
|
58
|
+
5. ✅ **Return cleanup** from React useEffect
|
|
59
|
+
|
|
60
|
+
### Quick Pattern
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
class Component extends HTMLElement {
|
|
64
|
+
controller: AbortController
|
|
65
|
+
iO: IntersectionObserver
|
|
66
|
+
timeout: ReturnType<typeof setTimeout>
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
super()
|
|
70
|
+
this.controller = new AbortController()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
connectedCallback() {
|
|
74
|
+
// Setup with signal
|
|
75
|
+
const { signal } = this.controller
|
|
76
|
+
this.addEventListener('click', this.handleClick, { signal })
|
|
77
|
+
|
|
78
|
+
this.iO = new IntersectionObserver(...)
|
|
79
|
+
this.timeout = setTimeout(...)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
disconnectedCallback() {
|
|
83
|
+
// Cleanup everything
|
|
84
|
+
this.controller.abort()
|
|
85
|
+
this.iO?.disconnect()
|
|
86
|
+
clearTimeout(this.timeout)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**→ See [Memory Management Guide](./MemoryManagement.md) for complete details**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Security
|
|
96
|
+
|
|
97
|
+
> 📖 **[Full Security Guidelines](./Security.md)**
|
|
98
|
+
|
|
99
|
+
### Critical Rules
|
|
100
|
+
|
|
101
|
+
1. ❌ **Never** use `dangerouslySetInnerHTML`
|
|
102
|
+
2. ❌ **Never** use `innerHTML`
|
|
103
|
+
3. ✅ **Always** validate URLs with `validators.url()`
|
|
104
|
+
4. ✅ **Always** validate numbers with `validators.integer()`
|
|
105
|
+
5. ✅ **Always** wrap JSON.parse with `validators.json()`
|
|
106
|
+
6. ✅ Use environment-specific logging
|
|
107
|
+
7. ✅ Add `rel="noopener noreferrer"` to external `target="_blank"` links
|
|
108
|
+
|
|
109
|
+
### Quick Patterns
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// XSS Prevention
|
|
113
|
+
import { createElement } from "../Utility/utilities"
|
|
114
|
+
const el = createElement(htmlString) // Safe, not innerHTML
|
|
115
|
+
|
|
116
|
+
// URL Validation
|
|
117
|
+
import { validators } from "../Utility/utilities"
|
|
118
|
+
if (!validators.url(url)) {
|
|
119
|
+
url = window.location.href // Safe fallback
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Number Validation
|
|
123
|
+
if (!validators.integer(page, 1, lastPage)) {
|
|
124
|
+
return // Reject invalid input
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// JSON Validation
|
|
128
|
+
if (!validators.json(stored)) {
|
|
129
|
+
return defaultValue
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**→ See [Security Guidelines](./Security.md) for OWASP Top 10 coverage and complete details**
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## TypeScript
|
|
138
|
+
|
|
139
|
+
### Type Safety Best Practices
|
|
140
|
+
|
|
141
|
+
1. **Use strict TypeScript settings** (already configured in tsconfig.json)
|
|
142
|
+
- `isolatedModules: true`
|
|
143
|
+
- `skipLibCheck: true`
|
|
144
|
+
- `esModuleInterop: true`
|
|
145
|
+
|
|
146
|
+
2. **Use explicit types for component props**
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// ✅ Good
|
|
150
|
+
type CardProps = PropsWithChildren & {
|
|
151
|
+
reversed?: boolean
|
|
152
|
+
block?: boolean
|
|
153
|
+
as?: AsTypes
|
|
154
|
+
background?: BackgroundStyles
|
|
155
|
+
className?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const Card = ({ children, reversed = null, ... }: CardProps): JSX.Element => {
|
|
159
|
+
// ...
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
3. **Use proper return types for functions**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// ✅ Good
|
|
167
|
+
export const makeAnchor = (string: string, length: number = 20): string => {
|
|
168
|
+
// ...
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
4. **Use `@ts-expect-error` sparingly and document why**
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// ✅ Good - Explains why
|
|
176
|
+
// @ts-expect-error inert is allowed but not in TS yet
|
|
177
|
+
element.inert = true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
5. **Use oxlint disable comments when necessary** and document why
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// For user-facing errors where logging is intentional
|
|
184
|
+
console.error("Could not copy URL") // eslint-disable-line no-console
|
|
185
|
+
|
|
186
|
+
// For React patterns that need exceptions
|
|
187
|
+
/* eslint-disable consistent-return, @eslint-react/hooks-extra/no-direct-set-state-in-use-effect */
|
|
188
|
+
|
|
189
|
+
// At file level for specific cases
|
|
190
|
+
/* eslint-disable no-console */
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
6. **Avoid `any` - use `unknown` or proper types**
|
|
194
|
+
```typescript
|
|
195
|
+
// ⚠️ Acceptable for utility functions with generic types
|
|
196
|
+
validators: {
|
|
197
|
+
oneOf: <T>(value: T, allowedValues: T[]): boolean => {
|
|
198
|
+
return allowedValues.includes(value)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Component Architecture
|
|
206
|
+
|
|
207
|
+
> 📖 **[Full Component Architecture Guide](./ComponentArchitecture.md)**
|
|
208
|
+
|
|
209
|
+
### Web Components (Custom Elements)
|
|
210
|
+
|
|
211
|
+
1. **Component structure**
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
export default class MyComponent extends HTMLElement {
|
|
215
|
+
// 1. Properties
|
|
216
|
+
internals_: ElementInternals
|
|
217
|
+
controller: AbortController
|
|
218
|
+
|
|
219
|
+
// 2. Constructor
|
|
220
|
+
constructor() {
|
|
221
|
+
super()
|
|
222
|
+
this.internals_ = this.attachInternals()
|
|
223
|
+
this.controller = new AbortController()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 3. Lifecycle
|
|
227
|
+
connectedCallback(): void {
|
|
228
|
+
// Setup
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
disconnectedCallback(): void {
|
|
232
|
+
// Cleanup
|
|
233
|
+
this.controller.abort()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 4. Handlers
|
|
237
|
+
handleClick = (): void => {
|
|
238
|
+
// ...
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 5. Getters
|
|
242
|
+
get trigger(): HTMLButtonElement {
|
|
243
|
+
const trigger = this.querySelector("button")
|
|
244
|
+
if (!trigger) {
|
|
245
|
+
throw new Error(`${this.localName} must contain a <button>`)
|
|
246
|
+
}
|
|
247
|
+
return trigger
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 6. Registration
|
|
252
|
+
if (!customElements.get("mx-component")) {
|
|
253
|
+
customElements.define("mx-component", MyComponent)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 7. TypeScript declaration
|
|
257
|
+
declare global {
|
|
258
|
+
interface HTMLElementTagNameMap {
|
|
259
|
+
"mx-component": MyComponent
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
2. **Use getters for DOM queries** with validation
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
get details(): HTMLDetailsElement | null {
|
|
268
|
+
const details = this.querySelector("details")
|
|
269
|
+
if (!details) {
|
|
270
|
+
throw new Error(`${this.localName} must contain a <details> element.`)
|
|
271
|
+
}
|
|
272
|
+
return details
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
3. **Generate IDs when needed**
|
|
277
|
+
```typescript
|
|
278
|
+
details.id = details.id || this.generatedId()
|
|
279
|
+
```
|
|
280
|
+
> `generatedId()` is a private method on Web Component classes. See [API Documentation → generatedId()](./API.md#generatedid) for details.
|
|
281
|
+
|
|
282
|
+
### React Components
|
|
283
|
+
|
|
284
|
+
1. **Component structure**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { JSX, PropsWithChildren } from "react"
|
|
288
|
+
import classNames from "classnames"
|
|
289
|
+
|
|
290
|
+
type ComponentProps = PropsWithChildren & {
|
|
291
|
+
// Props with defaults
|
|
292
|
+
reversed?: boolean
|
|
293
|
+
// Required props
|
|
294
|
+
className?: string
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const Component = ({
|
|
298
|
+
children,
|
|
299
|
+
reversed = false,
|
|
300
|
+
className = null,
|
|
301
|
+
}: ComponentProps): JSX.Element => {
|
|
302
|
+
return (
|
|
303
|
+
<div className={classNames("mx-component", className, {
|
|
304
|
+
"mx-component--reversed": reversed,
|
|
305
|
+
})}>
|
|
306
|
+
{children}
|
|
307
|
+
</div>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default Component
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
2. **Use proper React hooks**
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// State
|
|
318
|
+
const [activePage, setActivePage] = useState(0)
|
|
319
|
+
|
|
320
|
+
// Refs
|
|
321
|
+
const polyfillsInstance = useRef<any>(null)
|
|
322
|
+
|
|
323
|
+
// Effects with cleanup
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
const controller = new AbortController()
|
|
326
|
+
// Setup
|
|
327
|
+
|
|
328
|
+
return () => {
|
|
329
|
+
controller.abort() // Cleanup
|
|
330
|
+
}
|
|
331
|
+
}, [dependencies])
|
|
332
|
+
|
|
333
|
+
// Memoization
|
|
334
|
+
const value = useMemo(() => {
|
|
335
|
+
return expensiveCalculation(dep)
|
|
336
|
+
}, [dep])
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
3. **Use classNames for conditional classes**
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import classNames from "classnames"
|
|
343
|
+
|
|
344
|
+
<div className={classNames("mx-card", className, {
|
|
345
|
+
"mx-card--reversed": reversed,
|
|
346
|
+
"mx-card--block": block,
|
|
347
|
+
[`mx-background--${background}`]: background,
|
|
348
|
+
})} />
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## CSS & Styling
|
|
354
|
+
|
|
355
|
+
> 📖 **[Full CSS Guidelines](./CSSGuidelines.md)**
|
|
356
|
+
|
|
357
|
+
> ⚙️ **[PostCSS Pipeline](./CSSGuidelines.md#postcss-pipeline)** — explains non-standard syntax: `token()`, `@media (--medium-up)`, and px→rem conversion
|
|
358
|
+
|
|
359
|
+
### BEM Naming Convention
|
|
360
|
+
|
|
361
|
+
Follow BEM (Block Element Modifier) strictly:
|
|
362
|
+
|
|
363
|
+
```css
|
|
364
|
+
/* ✅ Good - BEM structure */
|
|
365
|
+
.mx-card {
|
|
366
|
+
}
|
|
367
|
+
.mx-card__header {
|
|
368
|
+
}
|
|
369
|
+
.mx-card__body {
|
|
370
|
+
}
|
|
371
|
+
.mx-card--reversed {
|
|
372
|
+
}
|
|
373
|
+
.mx-card--block {
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ❌ Bad - Not BEM */
|
|
377
|
+
.mx-card-header {
|
|
378
|
+
}
|
|
379
|
+
.mx-card.reversed {
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### CSS Organization
|
|
384
|
+
|
|
385
|
+
1. **File structure**
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
Component/
|
|
389
|
+
_component-partial.css (internal styles)
|
|
390
|
+
component.css (public API)
|
|
391
|
+
Component.tsx
|
|
392
|
+
component.twig
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
2. **Import custom media in every CSS file**
|
|
396
|
+
|
|
397
|
+
This is only required if you're not using something like [@csstools/postcss-global-data](https://www.npmjs.com/package/@csstools/postcss-global-data).
|
|
398
|
+
|
|
399
|
+
```css
|
|
400
|
+
@import "../../_custom-media.css";
|
|
401
|
+
|
|
402
|
+
.mx-component {
|
|
403
|
+
/* Mobile first */
|
|
404
|
+
display: block;
|
|
405
|
+
|
|
406
|
+
/* Responsive */
|
|
407
|
+
@media (--medium-up) {
|
|
408
|
+
display: flex;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
3. **Use CSS custom properties from constants.css**
|
|
414
|
+
|
|
415
|
+
Design tokens (in `src/tokens.js`) are used with PostCSS to generate `src/constants.css` which defines all CSS custom properties. Load `constants.css` once at the page level, then all components can use the custom properties:
|
|
416
|
+
|
|
417
|
+
```css
|
|
418
|
+
/* Component CSS - NO import needed */
|
|
419
|
+
.mx-component {
|
|
420
|
+
color: var(--colour-foreground);
|
|
421
|
+
padding: var(--spacing-m);
|
|
422
|
+
font-size: var(--font-size-m);
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The custom properties are available because `constants.css` is loaded at the page level, preventing duplication.
|
|
427
|
+
|
|
428
|
+
4. **Use logical properties**
|
|
429
|
+
|
|
430
|
+
```css
|
|
431
|
+
/* ✅ Good - Logical properties */
|
|
432
|
+
padding-inline: 1rem;
|
|
433
|
+
margin-block-start: 1rem;
|
|
434
|
+
inset-inline-start: 0;
|
|
435
|
+
|
|
436
|
+
/* ❌ Avoid - Physical properties */
|
|
437
|
+
padding-left: 1rem;
|
|
438
|
+
margin-top: 1rem;
|
|
439
|
+
left: 0;
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
5. **Nest selectors properly**
|
|
443
|
+
|
|
444
|
+
```css
|
|
445
|
+
/* ✅ Good - Use & for nesting */
|
|
446
|
+
.mx-component {
|
|
447
|
+
& button {
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.mx-component--modifier {
|
|
452
|
+
& button {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
.mx-component__element {
|
|
456
|
+
& button {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* Stylelint enforces this pattern */
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
6. **Use cascade layers**
|
|
464
|
+
```css
|
|
465
|
+
/* All component styles are wrapped in a layer */
|
|
466
|
+
@layer design-system.components {
|
|
467
|
+
.mx-component {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Validation
|
|
475
|
+
|
|
476
|
+
> 📖 **[Full API Documentation](./API.md)**
|
|
477
|
+
|
|
478
|
+
### Use the validators utility
|
|
479
|
+
|
|
480
|
+
Always use the centralized `validators` utility for input validation:
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { validators } from "../Utility/utilities"
|
|
484
|
+
|
|
485
|
+
// URL validation
|
|
486
|
+
if (!validators.url(url)) {
|
|
487
|
+
// Handle invalid URL
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Integer with bounds
|
|
491
|
+
if (!validators.integer(page, 1, lastPage)) {
|
|
492
|
+
// Handle invalid page
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// JSON validation
|
|
496
|
+
if (!validators.json(jsonString)) {
|
|
497
|
+
return fallbackValue
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Email validation
|
|
501
|
+
if (!validators.email(email)) {
|
|
502
|
+
// Handle invalid email
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Non-empty string
|
|
506
|
+
if (!validators.notEmpty(input)) {
|
|
507
|
+
return ""
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Available validators:
|
|
512
|
+
|
|
513
|
+
- `url(value, allowedProtocols)` - URL with protocol checking
|
|
514
|
+
- `email(value)` - Email format
|
|
515
|
+
- `number(value, min, max)` - Number with bounds
|
|
516
|
+
- `integer(value, min, max)` - Integer with bounds
|
|
517
|
+
- `string(value, minLength, maxLength)` - String with length
|
|
518
|
+
- `notEmpty(value)` - Non-empty after trim
|
|
519
|
+
- `pattern(value, regex)` - Regex matching
|
|
520
|
+
- `oneOf(value, allowedValues)` - Enum validation
|
|
521
|
+
- `array(value, minLength, maxLength)` - Array validation
|
|
522
|
+
- `json(value)` - JSON string validation
|
|
523
|
+
- `date(value, minDate, maxDate)` - Date validation
|
|
524
|
+
- `hexColor(value)` - Hex color validation
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Testing
|
|
529
|
+
|
|
530
|
+
> 📖 **[Full Testing Guide](./Testing.md)**
|
|
531
|
+
|
|
532
|
+
### Storybook Stories
|
|
533
|
+
|
|
534
|
+
1. **Create stories for all components**
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
import type { Meta, StoryObj } from "@storybook/html"
|
|
538
|
+
|
|
539
|
+
const meta: Meta = {
|
|
540
|
+
title: "Component/MyComponent",
|
|
541
|
+
tags: ["autodocs"],
|
|
542
|
+
render: args => {
|
|
543
|
+
return `<mx-component>${args.content}</mx-component>`
|
|
544
|
+
},
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export default meta
|
|
548
|
+
type Story = StoryObj
|
|
549
|
+
|
|
550
|
+
export const Default: Story = {
|
|
551
|
+
args: {
|
|
552
|
+
content: "Default content",
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
2. **Use snapshots for visual regression**
|
|
558
|
+
- Snapshots are automatically generated from stories
|
|
559
|
+
- Located in `__snapshots__/` directories
|
|
560
|
+
- Run `pnpm test` to verify
|
|
561
|
+
|
|
562
|
+
3. **Tag stories appropriately**
|
|
563
|
+
```typescript
|
|
564
|
+
export const NoSnapshotExample: Story = {
|
|
565
|
+
tags: ["no-snapshot"], // Skip snapshot testing
|
|
566
|
+
args: { ... }
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Testing Commands
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
# Run all tests
|
|
574
|
+
pnpm test
|
|
575
|
+
|
|
576
|
+
# Run HTML component tests
|
|
577
|
+
pnpm test-html
|
|
578
|
+
|
|
579
|
+
# Run React component tests
|
|
580
|
+
pnpm test-react
|
|
581
|
+
|
|
582
|
+
# Lint everything
|
|
583
|
+
pnpm lint
|
|
584
|
+
|
|
585
|
+
# Fix linting issues
|
|
586
|
+
pnpm format
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## Code Formatting
|
|
592
|
+
|
|
593
|
+
### oxfmt Configuration
|
|
594
|
+
|
|
595
|
+
oxfmt is configured via `.oxfmtrc.json`:
|
|
596
|
+
|
|
597
|
+
- **Print width**: 100 characters
|
|
598
|
+
- **Arrow parens**: avoid
|
|
599
|
+
- **Semi**: false (no semicolons)
|
|
600
|
+
- **Sort package.json**: false
|
|
601
|
+
- **Ignore patterns**: node_modules, package files, storybook output, markdown docs
|
|
602
|
+
|
|
603
|
+
**Commands:**
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
# Format all files
|
|
607
|
+
pnpm format
|
|
608
|
+
|
|
609
|
+
# Check formatting without making changes
|
|
610
|
+
pnpm lint-format
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
> **Why oxfmt?** oxfmt is a Rust-based formatter that's significantly faster than Prettier while maintaining compatibility with Prettier configurations.
|
|
614
|
+
|
|
615
|
+
### Oxlint
|
|
616
|
+
|
|
617
|
+
Using `oxlint` for fast, performant linting (configured in `.oxlintrc.json`):
|
|
618
|
+
|
|
619
|
+
**Configuration highlights:**
|
|
620
|
+
|
|
621
|
+
- **Plugins**: unicorn, typescript, oxc, import, react, react-perf, jsx-a11y, jest, jsdoc
|
|
622
|
+
- **Categories**: correctness (error), suspicious (warn), perf (warn)
|
|
623
|
+
- **Globals defined**: Drupal, once, Mixtape
|
|
624
|
+
- **Key rules**:
|
|
625
|
+
- `no-console: error` (use `// eslint-disable-line no-console` for exceptions)
|
|
626
|
+
- `import/no-unused-imports: error`
|
|
627
|
+
- `consistent-return: warn`
|
|
628
|
+
- `no-unused-vars: warn`
|
|
629
|
+
|
|
630
|
+
**Globals:**
|
|
631
|
+
|
|
632
|
+
These are injected by the host environment and declared in `.oxlintrc.json`. See [API Documentation → Global Variables](./API.md#global-variables) for full details.
|
|
633
|
+
|
|
634
|
+
| Global | Source | Purpose |
|
|
635
|
+
|---|---|---|
|
|
636
|
+
| `Drupal` | Drupal CMS | CMS integration — use `loadOnReady()` rather than accessing directly |
|
|
637
|
+
| `once` / `window.once` | [`@drupal/once`](https://www.npmjs.com/package/@drupal/once) (standalone or Drupal core) | DOM initialisation guard — assign to `window.once` in non-Drupal projects |
|
|
638
|
+
| `Mixtape` | Internal CMS layer | App-level config and feature flags |
|
|
639
|
+
|
|
640
|
+
**Commands:**
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
# Lint TypeScript files
|
|
644
|
+
pnpm lint-js
|
|
645
|
+
|
|
646
|
+
# Fix auto-fixable issues
|
|
647
|
+
pnpm fix-js
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**React/JSX overrides:**
|
|
651
|
+
|
|
652
|
+
- `consistent-return: off` (React components don't always need explicit returns)
|
|
653
|
+
- `react/react-in-jsx-scope: off` (React 17+ JSX transform)
|
|
654
|
+
- `jsx-a11y/anchor-is-valid: off` (design system uses custom Link components)
|
|
655
|
+
|
|
656
|
+
### Stylelint
|
|
657
|
+
|
|
658
|
+
Enforces:
|
|
659
|
+
|
|
660
|
+
- BEM naming convention
|
|
661
|
+
- Logical properties preferred
|
|
662
|
+
- Standard CSS rules
|
|
663
|
+
- No descending specificity issues (disabled for flexibility)
|
|
664
|
+
|
|
665
|
+
```bash
|
|
666
|
+
# Lint CSS
|
|
667
|
+
pnpm lint-css
|
|
668
|
+
|
|
669
|
+
# Fix CSS issues
|
|
670
|
+
pnpm fix-css
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### EditorConfig
|
|
674
|
+
|
|
675
|
+
Use consistent settings:
|
|
676
|
+
|
|
677
|
+
- UTF-8 encoding
|
|
678
|
+
- LF line endings
|
|
679
|
+
- 2 spaces for indentation
|
|
680
|
+
- Trim trailing whitespace
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Git & Commits
|
|
685
|
+
|
|
686
|
+
> 📖 **[Full Contributing Guide](./Contributing.md)**
|
|
687
|
+
|
|
688
|
+
### Conventional Commits
|
|
689
|
+
|
|
690
|
+
Use Commitizen for standardized commit messages:
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
pnpm commit
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
Commit types:
|
|
697
|
+
|
|
698
|
+
- `feat`: New feature
|
|
699
|
+
- `fix`: Bug fix
|
|
700
|
+
- `docs`: Documentation only
|
|
701
|
+
- `style`: Code style (formatting, missing semicolons, etc)
|
|
702
|
+
- `refactor`: Code change that neither fixes a bug nor adds a feature
|
|
703
|
+
- `perf`: Performance improvement
|
|
704
|
+
- `test`: Adding or updating tests
|
|
705
|
+
- `chore`: Maintenance tasks, dependencies, etc
|
|
706
|
+
|
|
707
|
+
### Example Commits
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
feat: add validation utility for form inputs
|
|
711
|
+
fix: prevent memory leak in Carousel component
|
|
712
|
+
docs: update CodeQuality.md with validation patterns
|
|
713
|
+
refactor: use validators utility in Pagination
|
|
714
|
+
perf: optimize image loading with intersection observer
|
|
715
|
+
test: add snapshot tests for Button variants
|
|
716
|
+
chore: update dependencies to latest versions
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## File Organization
|
|
722
|
+
|
|
723
|
+
### Component Structure
|
|
724
|
+
|
|
725
|
+
```
|
|
726
|
+
Component/
|
|
727
|
+
README.md # Component documentation (optional)
|
|
728
|
+
Component.tsx # React component
|
|
729
|
+
Component.stories.tsx # React stories
|
|
730
|
+
component.twig # Twig template
|
|
731
|
+
Component.stories.ts # Twig stories
|
|
732
|
+
component.css # Public styles
|
|
733
|
+
_component-partial.css # Internal styles
|
|
734
|
+
Elements/ # Web Components
|
|
735
|
+
Component.ts
|
|
736
|
+
Components/ # React Sub-components
|
|
737
|
+
ComponentPart.tsx
|
|
738
|
+
__snapshots__/ # Snapshot regression tests
|
|
739
|
+
Component.stories.ts.snap
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Import Order
|
|
743
|
+
|
|
744
|
+
1. External dependencies
|
|
745
|
+
2. Internal utilities
|
|
746
|
+
3. Types/enums
|
|
747
|
+
4. Components
|
|
748
|
+
5. Styles (in CSS/Twig files)
|
|
749
|
+
|
|
750
|
+
```typescript
|
|
751
|
+
// 1. External
|
|
752
|
+
import { JSX, PropsWithChildren } from "react"
|
|
753
|
+
import classNames from "classnames"
|
|
754
|
+
|
|
755
|
+
// 2. Internal utilities
|
|
756
|
+
import { validators } from "../../Utility/utilities"
|
|
757
|
+
|
|
758
|
+
// 3. Types/enums
|
|
759
|
+
import { AsTypes } from "@pnx-mixtape/ids-shape"
|
|
760
|
+
import { BackgroundStyles } from "../../enums"
|
|
761
|
+
|
|
762
|
+
// 4. Components
|
|
763
|
+
import Button from "../Button/Button"
|
|
764
|
+
|
|
765
|
+
// Component code
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## Performance
|
|
771
|
+
|
|
772
|
+
> 📖 **[Full Component Architecture Guide - Performance](./ComponentArchitecture.md)**
|
|
773
|
+
|
|
774
|
+
1. **Lazy load components when appropriate**
|
|
775
|
+
- Use `IntersectionLoader` for below-fold content
|
|
776
|
+
- Use `BreakpointLoader` for responsive loading
|
|
777
|
+
|
|
778
|
+
2. **Use memoization for expensive calculations**
|
|
779
|
+
|
|
780
|
+
```typescript
|
|
781
|
+
const value = useMemo(() => expensiveCalculation(dep), [dep])
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
3. **Debounce/throttle frequent events**
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
clearTimeout(this.debounced)
|
|
788
|
+
this.debounced = setTimeout(this.handler, 200)
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
4. **Use CSS containment where appropriate**
|
|
792
|
+
```css
|
|
793
|
+
.mx-component {
|
|
794
|
+
contain: layout style paint;
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## Accessibility
|
|
801
|
+
|
|
802
|
+
> 📖 **[Full Testing Guide - Accessibility](./Testing.md#accessibility-testing)**
|
|
803
|
+
|
|
804
|
+
1. **Always include ARIA labels**
|
|
805
|
+
|
|
806
|
+
```html
|
|
807
|
+
<nav aria-label="Primary navigation">
|
|
808
|
+
<button aria-expanded="false" aria-controls="menu">
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
2. **Use semantic HTML**
|
|
812
|
+
|
|
813
|
+
```html
|
|
814
|
+
<!-- ✅ Good -->
|
|
815
|
+
<nav>, <main>, <article>, <section>, <aside>
|
|
816
|
+
|
|
817
|
+
<!-- ❌ Avoid -->
|
|
818
|
+
<div class="nav">, <div class="main">
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
3. **Support keyboard navigation**
|
|
822
|
+
- Tab order must be logical
|
|
823
|
+
- Escape key closes modals/menus
|
|
824
|
+
- Arrow keys navigate menus
|
|
825
|
+
|
|
826
|
+
4. **Include skip links**
|
|
827
|
+
|
|
828
|
+
```html
|
|
829
|
+
<a href="#main-content" class="sr-only">Skip to main content</a>
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
5. **Test with Storybook a11y addon**
|
|
833
|
+
- Addon runs automatically
|
|
834
|
+
- Check for WCAG violations
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Documentation
|
|
839
|
+
|
|
840
|
+
1. **Add JSDoc to public APIs**
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
/**
|
|
844
|
+
* Validates a URL string and optionally checks protocol.
|
|
845
|
+
* @param value - URL string to validate
|
|
846
|
+
* @param allowedProtocols - Array of allowed protocols
|
|
847
|
+
* @returns true if valid URL with allowed protocol
|
|
848
|
+
* @example
|
|
849
|
+
* validators.url('https://example.com') // true
|
|
850
|
+
*/
|
|
851
|
+
url: (value: string, allowedProtocols: string[] = ["http:", "https:"]): boolean => {
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
2. **Create README files for complex components**
|
|
855
|
+
- Usage examples
|
|
856
|
+
- Props documentation
|
|
857
|
+
- Common patterns
|
|
858
|
+
|
|
859
|
+
3. **Use MDX for Storybook documentation**
|
|
860
|
+
|
|
861
|
+
```mdx
|
|
862
|
+
import { Meta } from "@storybook/blocks"
|
|
863
|
+
|
|
864
|
+
<Meta title="Component/MyComponent/Docs" />
|
|
865
|
+
|
|
866
|
+
# MyComponent
|
|
867
|
+
|
|
868
|
+
Description and usage guide...
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## Common Patterns
|
|
874
|
+
|
|
875
|
+
### Singleton Pattern (CookieCompliance)
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
static create({ context = null } = {}): void {
|
|
879
|
+
const [el] = window.once("cookie-compliance", "#cookie-compliance", context)
|
|
880
|
+
if (el) {
|
|
881
|
+
const instance = new CookieCompliance(el)
|
|
882
|
+
instance.init()
|
|
883
|
+
window.CookieCompliance = instance // Global reference
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
> `window.once` is a DOM initialisation guard from [`@drupal/once`](https://www.npmjs.com/package/@drupal/once) — works standalone (assign to `window.once` at your entry point) or automatically inside Drupal. See [API Documentation → Global Variables](./API.md#global-variables) for full details.
|
|
889
|
+
|
|
890
|
+
### Disclosure Pattern (Accordion, Tabs)
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
import { DisclosureWidget } from "../../../Utility/utilities"
|
|
894
|
+
|
|
895
|
+
this.widget = new DisclosureWidget(this.trigger, this.target)
|
|
896
|
+
this.widget.init()
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### Keyboard Navigation Pattern
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
import { Keyboard } from "../../../Utility/utilities"
|
|
903
|
+
|
|
904
|
+
const keyboard = new Keyboard()
|
|
905
|
+
keyboard.attachMenu(container, items)
|
|
906
|
+
keyboard.attachEventListeners(signal)
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## Checklist for New Components
|
|
912
|
+
|
|
913
|
+
- [ ] TypeScript types defined
|
|
914
|
+
- [ ] Props have defaults where appropriate
|
|
915
|
+
- [ ] Memory cleanup implemented (disconnectedCallback/useEffect cleanup)
|
|
916
|
+
- [ ] Input validation using `validators` utility
|
|
917
|
+
- [ ] Error handling with try-catch
|
|
918
|
+
- [ ] Environment-specific logging
|
|
919
|
+
- [ ] BEM class names
|
|
920
|
+
- [ ] Responsive styles with custom media
|
|
921
|
+
- [ ] Logical CSS properties
|
|
922
|
+
- [ ] ARIA labels and roles
|
|
923
|
+
- [ ] Keyboard navigation support
|
|
924
|
+
- [ ] Storybook stories created
|
|
925
|
+
- [ ] README documentation
|
|
926
|
+
- [ ] Snapshot tests passing
|
|
927
|
+
- [ ] No TypeScript errors
|
|
928
|
+
- [ ] Linting passes (`pnpm lint`)
|
|
929
|
+
- [ ] Manual testing in Storybook
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## Resources
|
|
934
|
+
|
|
935
|
+
- [Project README](../README.md)
|
|
936
|
+
- [Storybook](https://mxds.pnx.io)
|
|
937
|
+
- [Conventional Commits](https://www.conventionalcommits.org/)
|
|
938
|
+
- [BEM Methodology](https://getbem.com/)
|
|
939
|
+
- [MDN Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components)
|
|
940
|
+
- [React Documentation](https://react.dev/)
|