@comet/agent-features 9.0.0-beta.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 (35) hide show
  1. package/LICENSE +24 -0
  2. package/package.json +19 -0
  3. package/rules/coding-guidelines/api-nestjs.instructions.md +107 -0
  4. package/rules/coding-guidelines/cdn.instructions.md +24 -0
  5. package/rules/coding-guidelines/general.instructions.md +30 -0
  6. package/rules/coding-guidelines/git.instructions.md +37 -0
  7. package/rules/coding-guidelines/kubernetes.instructions.md +59 -0
  8. package/rules/coding-guidelines/libraries.instructions.md +34 -0
  9. package/rules/coding-guidelines/naming.instructions.md +39 -0
  10. package/rules/coding-guidelines/postgresql.instructions.md +40 -0
  11. package/rules/coding-guidelines/react.instructions.md +102 -0
  12. package/rules/coding-guidelines/security.instructions.md +44 -0
  13. package/rules/coding-guidelines/styling.instructions.md +50 -0
  14. package/rules/coding-guidelines/typescript.instructions.md +50 -0
  15. package/skills/.gitkeep +0 -0
  16. package/skills/comet-block/SKILL.md +246 -0
  17. package/skills/comet-block/references/admin-patterns.md +192 -0
  18. package/skills/comet-block/references/api-patterns.md +183 -0
  19. package/skills/comet-block/references/block-loader.md +368 -0
  20. package/skills/comet-block/references/block-types.md +210 -0
  21. package/skills/comet-block/references/custom-block-field.md +266 -0
  22. package/skills/comet-block/references/fixtures.md +436 -0
  23. package/skills/comet-block/references/image.md +341 -0
  24. package/skills/comet-block/references/migration.md +597 -0
  25. package/skills/comet-block/references/registration.md +167 -0
  26. package/skills/comet-block/references/response-summary.md +102 -0
  27. package/skills/comet-block/references/rich-text.md +309 -0
  28. package/skills/comet-block/references/select.md +176 -0
  29. package/skills/comet-block/references/site-patterns.md +202 -0
  30. package/skills/comet-mail-react/SKILL.md +541 -0
  31. package/skills/comet-mail-react/references/components-and-theme.md +441 -0
  32. package/skills/comet-mail-react/references/layout-patterns.md +315 -0
  33. package/skills/comet-mail-react/references/styling-and-customization.md +306 -0
  34. package/skills/comet-minor-update/SKILL.md +191 -0
  35. package/skills/dev-pm/SKILL.md +100 -0
@@ -0,0 +1,315 @@
1
+ # Layout Patterns
2
+
3
+ ## How Column Gaps Work
4
+
5
+ MJML has no `gap` property. Column padding reduces the content area _inside_ the column — it doesn't create space between column cells. To create a visible gap, apply padding to the inner edges of adjacent columns: `paddingRight` on the left column and `paddingLeft` on the right column. Their sum becomes the gap.
6
+
7
+ **Do not** apply equal padding on all sides of every column. This adds extra outer-edge spacing that compounds with `indent`/`contentIndentation`, pushing content inward.
8
+
9
+ ### CSS Targeting Rules
10
+
11
+ - **Column padding** compiles to an inner `<td>`, not the outer `<div>` that receives `className`. Override padding via `.className > table > tbody > tr > td`.
12
+ - **`margin-bottom`/`margin-top`** goes on the column wrapper itself (the outer `<div>`), so use the plain `.className` selector without the table path.
13
+ - **`!important`** is required on all responsive overrides — MJML inlines styles that take precedence over `<style>` block rules.
14
+ - Prefer `theme.breakpoints.mobile.belowMediaQuery` and `theme.breakpoints.default.belowMediaQuery` over hardcoded media queries.
15
+
16
+ ### BEM Class Naming
17
+
18
+ Follow the BEM convention with camelCase blocks. Adapt the block name to the component context:
19
+
20
+ | Element | Example class name |
21
+ | ---------------------- | ---------------------------------------------------------------- |
22
+ | Section/layout wrapper | `twoColumnsSection`, `imageTextLayout` |
23
+ | Left/small column | `twoColumnsSection__leftColumn`, `imageTextLayout__smallColumn` |
24
+ | Right/fluid column | `twoColumnsSection__rightColumn`, `imageTextLayout__fluidColumn` |
25
+
26
+ ---
27
+
28
+ ## Symmetric Two-Column Layout (Equal Width)
29
+
30
+ Two equal-width columns with a gap between them, stacking vertically on mobile.
31
+
32
+ ### Pattern
33
+
34
+ Apply half the gap to each column's inner edge. Both columns have the same total padding, so MJML's default equal-width split produces equal content areas without explicit `width` props:
35
+
36
+ ```tsx
37
+ const TwoColumnsSection = () => {
38
+ const columnGap = 20;
39
+ const halfGap = columnGap / 2;
40
+
41
+ return (
42
+ <MjmlSection indent className="twoColumnsSection">
43
+ <MjmlColumn className="twoColumnsSection__leftColumn" paddingRight={halfGap}>
44
+ <MjmlText>Left column content.</MjmlText>
45
+ </MjmlColumn>
46
+ <MjmlColumn className="twoColumnsSection__rightColumn" paddingLeft={halfGap}>
47
+ <MjmlText>Right column content.</MjmlText>
48
+ </MjmlColumn>
49
+ </MjmlSection>
50
+ );
51
+ };
52
+ ```
53
+
54
+ ### Responsive Stacking
55
+
56
+ On mobile, columns stack vertically. Reset the gap padding so content stretches full-width, and add a vertical margin between the stacked columns:
57
+
58
+ ```ts
59
+ registerStyles(
60
+ (theme) => css`
61
+ ${theme.breakpoints.mobile.belowMediaQuery} {
62
+ .twoColumnsSection__leftColumn > table > tbody > tr > td {
63
+ padding-right: 0 !important;
64
+ }
65
+
66
+ .twoColumnsSection__rightColumn > table > tbody > tr > td {
67
+ padding-left: 0 !important;
68
+ }
69
+
70
+ .twoColumnsSection__leftColumn {
71
+ margin-bottom: 20px;
72
+ }
73
+ }
74
+ `,
75
+ );
76
+ ```
77
+
78
+ For three or more equal-width columns, see [Multi-Column Symmetric Layouts](#multi-column-symmetric-layouts-3-columns).
79
+
80
+ ---
81
+
82
+ ## Multi-Column Symmetric Layouts (3+ columns)
83
+
84
+ Three or more equal-width columns need explicit `width` props because inner columns carry padding on both sides and outer columns on only one. Without compensation, content areas are unequal.
85
+
86
+ ### N-Column Width Formula
87
+
88
+ ```ts
89
+ const columnGap = 20;
90
+ const halfColumnGap = columnGap / 2;
91
+ const availableContentWidth = theme.sizes.bodyWidth - 2 * getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
92
+ const contentWidthPerColumn = (availableContentWidth - (numberOfColumns - 1) * columnGap) / numberOfColumns;
93
+
94
+ const outerColumnWidth = `${((contentWidthPerColumn + halfColumnGap) / availableContentWidth) * 100}%`;
95
+ const innerColumnWidth = `${((contentWidthPerColumn + columnGap) / availableContentWidth) * 100}%`;
96
+ ```
97
+
98
+ - Outermost columns → `outerColumnWidth`, padding on their inner side only
99
+ - All middle columns → `innerColumnWidth`, padding on both sides (`halfColumnGap` each)
100
+
101
+ Scales to any N; 3 and 4 differ only in how many middle columns you repeat. Percentages — not pixels — keep MJML's fallback math predictable.
102
+
103
+ ### Pattern
104
+
105
+ Three columns shown; for four or more, repeat the middle-column.
106
+
107
+ ```tsx
108
+ <MjmlSection indent className="multiColumnSection">
109
+ <MjmlColumn className="multiColumnSection__column" width={outerColumnWidth} paddingRight={halfColumnGap}>
110
+
111
+ </MjmlColumn>
112
+ <MjmlColumn className="multiColumnSection__column" width={innerColumnWidth} paddingLeft={halfColumnGap} paddingRight={halfColumnGap}>
113
+
114
+ </MjmlColumn>
115
+ <MjmlColumn className="multiColumnSection__column" width={outerColumnWidth} paddingLeft={halfColumnGap}>
116
+
117
+ </MjmlColumn>
118
+ </MjmlSection>
119
+ ```
120
+
121
+ ### Responsive Stacking — Pick Per Layout
122
+
123
+ Stacking is a **design decision per component**, not a function of column count. Do not assume the user wants the storybook default — these are starting points, not rules. If it is unclear which strategy fits, infer from the content (dense text vs. short labels vs. fixed-width icons/numbers) or ask.
124
+
125
+ | Strategy | When to pick it | How it's implemented |
126
+ | ------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
127
+ | A. Stack at mobile | Columns stay readable while narrowing below `bodyWidth` (e.g., 3-col storybook default) | Flex reset at `default` + stack at `mobile` |
128
+ | B. Stack at default | Columns would get too cramped below `bodyWidth` (e.g., 4-col storybook default) | Flex reset that stacks at `default` |
129
+ | C. Never stack | Short fixed content that must remain horizontal (numeric rows, icon strips) | `disableResponsiveBehavior` + flex reset one level deeper (targets `… > td > div`) |
130
+
131
+ All three strategies share the same flex reset on the element that directly contains the columns — it neutralises the compensated inline widths so content areas stay equal at every viewport (without it, percentage widths and pixel paddings drift apart below `bodyWidth`, making columns unequal by a few pixels). For A and B the container is the section's inner `<td>`; for C it is the `MjmlGroup` wrapper `<div>` that `disableResponsiveBehavior` inserts between the `<td>` and the columns, so the selector goes one level deeper (`… > td > div`). They also differ at `mobile.belowMediaQuery`: A adds a stack override, B collapses into the `default` block and stacks immediately, C adds nothing. Strategy C additionally needs `disableResponsiveBehavior` on the section — MJML auto-stacks below its own mobile breakpoint by default, and this prop wraps the columns in an `MjmlGroup` internally so the auto-stack is suppressed even in clients that ignore the flex CSS.
132
+
133
+ **Strategy A** — keep the horizontal intermediate state, stack at mobile:
134
+
135
+ ```ts
136
+ ${theme.breakpoints.default.belowMediaQuery} {
137
+ .multiColumnSection > table > tbody > tr > td {
138
+ display: flex !important;
139
+ gap: 20px !important;
140
+ }
141
+ .multiColumnSection__column {
142
+ flex: 1 1 0% !important;
143
+ width: auto !important;
144
+ max-width: none !important;
145
+ display: block !important;
146
+ }
147
+ .multiColumnSection__column > table > tbody > tr > td {
148
+ padding-left: 0 !important;
149
+ padding-right: 0 !important;
150
+ }
151
+ }
152
+
153
+ ${theme.breakpoints.mobile.belowMediaQuery} {
154
+ .multiColumnSection > table > tbody > tr > td {
155
+ flex-direction: column !important;
156
+ }
157
+ .multiColumnSection__column {
158
+ flex: none !important;
159
+ width: 100% !important;
160
+ max-width: 100% !important;
161
+ }
162
+ }
163
+ ```
164
+
165
+ **Strategy B** — collapse the two blocks: put `flex-direction: column` and `width: 100%` in the `default.belowMediaQuery` block and drop the `mobile.belowMediaQuery` block.
166
+
167
+ **Strategy C** — set `disableResponsiveBehavior` on the section and apply Strategy A's flex reset one level deeper (the group wrapper), dropping the `mobile.belowMediaQuery` block:
168
+
169
+ ```tsx
170
+ <MjmlSection indent disableResponsiveBehavior className="multiColumnSection">
171
+ {/* …columns as in the pattern above… */}
172
+ </MjmlSection>
173
+ ```
174
+
175
+ ```ts
176
+ ${theme.breakpoints.default.belowMediaQuery} {
177
+ .multiColumnSection > table > tbody > tr > td > div {
178
+ display: flex !important;
179
+ gap: 20px !important;
180
+ }
181
+ .multiColumnSection__column {
182
+ flex: 1 1 0% !important;
183
+ width: auto !important;
184
+ max-width: none !important;
185
+ display: block !important;
186
+ }
187
+ .multiColumnSection__column > table > tbody > tr > td {
188
+ padding-left: 0 !important;
189
+ padding-right: 0 !important;
190
+ }
191
+ }
192
+ ```
193
+
194
+ The only difference from Strategy A's `default.belowMediaQuery` block is the `> div` in the first selector — `disableResponsiveBehavior` wraps the columns in an `MjmlGroup` `<div>` inside the `<td>`, so the flex container has to target that wrapper instead of the `<td>` to make the columns its direct flex children. Applying flex to the `<td>` instead would give it a single flex item (the wrapper), and the block-display rule on the columns below would make them stack vertically inside that wrapper.
195
+
196
+ ---
197
+
198
+ ## Asymmetric Two-Column Layout (Fixed + Fluid)
199
+
200
+ A fixed-width column paired with a fluid column that takes the remaining space.
201
+
202
+ ### Width Computation
203
+
204
+ MJML does not give columns "remaining space" — it always divides equally (`containerWidth / numberOfColumns`). Set explicit widths on **both** columns. Derive the fluid width from the theme:
205
+
206
+ ```tsx
207
+ const SMALL_COLUMN_WIDTH = 120;
208
+ const COLUMN_GAP = 20;
209
+
210
+ const sectionIndent = getDefaultFromResponsiveValue(theme.sizes.contentIndentation);
211
+ const sectionInnerWidth = theme.sizes.bodyWidth - 2 * sectionIndent;
212
+ const fluidColumnWidth = sectionInnerWidth - SMALL_COLUMN_WIDTH;
213
+ ```
214
+
215
+ `getDefaultFromResponsiveValue` extracts the default (desktop/inline) value from a responsive theme property like `contentIndentation`.
216
+
217
+ ### Pattern
218
+
219
+ Gap is created by padding on the fluid column's inner edge:
220
+
221
+ ```tsx
222
+ <MjmlSection indent>
223
+ <MjmlColumn className="imageTextLayout__smallColumn" width={`${SMALL_COLUMN_WIDTH}px`} verticalAlign="middle">
224
+ <MjmlImage src="..." alt="..." width={SMALL_COLUMN_WIDTH} />
225
+ </MjmlColumn>
226
+ <MjmlColumn className="imageTextLayout__fluidColumn" width={`${fluidColumnWidth}px`} paddingLeft={`${COLUMN_GAP}px`} verticalAlign="middle">
227
+ <MjmlText>Content that fills the remaining space.</MjmlText>
228
+ </MjmlColumn>
229
+ </MjmlSection>
230
+ ```
231
+
232
+ To place the small column on the right, swap column order and move padding to `paddingRight` on the fluid column.
233
+
234
+ ### Two-Breakpoint Responsive Behavior
235
+
236
+ Fixed-width columns overflow between `bodyWidth` and the mobile stacking breakpoint. Use two stacked `belowMediaQuery` blocks — the later one overrides the earlier via cascade order:
237
+
238
+ ```ts
239
+ registerStyles(
240
+ (theme) => css`
241
+ ${theme.breakpoints.default.belowMediaQuery} {
242
+ .imageTextLayout__fluidColumn {
243
+ width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
244
+ max-width: calc(100% - ${SMALL_COLUMN_WIDTH}px) !important;
245
+ }
246
+ }
247
+
248
+ ${theme.breakpoints.mobile.belowMediaQuery} {
249
+ .imageTextLayout__fluidColumn {
250
+ width: 100% !important;
251
+ max-width: 100% !important;
252
+ }
253
+
254
+ .imageTextLayout__smallColumn {
255
+ margin-bottom: 10px;
256
+ }
257
+
258
+ .imageTextLayout__fluidColumn > table > tbody > tr > td {
259
+ padding-left: 0 !important;
260
+ }
261
+ }
262
+ `,
263
+ );
264
+ ```
265
+
266
+ This cascade-based approach is the idiomatic pattern. Never use hardcoded `@media (min-width: X) and (max-width: Y)` range queries — stacking `belowMediaQuery` blocks achieves the same result while staying in sync with the theme.
267
+
268
+ ### Controlling Mobile Stack Order with `direction="rtl"`
269
+
270
+ MJML stacks columns in source order on mobile. To make a right-side column stack on top, use `direction="rtl"` on the section to flip the desktop visual order while keeping the source (and mobile stacking) order:
271
+
272
+ ```tsx
273
+ <MjmlWrapper padding={`0 ${sectionIndent}px`} backgroundColor={theme.colors.background.content}>
274
+ <MjmlSection direction="rtl">
275
+ <MjmlColumn className="layout__smallColumn" width={`${SMALL_COLUMN_WIDTH}px`}>
276
+ <MjmlImage src="..." alt="..." width={SMALL_COLUMN_WIDTH} />
277
+ </MjmlColumn>
278
+ <MjmlColumn className="layout__fluidColumn" width={`${fluidColumnWidth}px`} paddingRight={`${COLUMN_GAP}px`}>
279
+ <MjmlText>Appears on the left on desktop, below the image on mobile.</MjmlText>
280
+ </MjmlColumn>
281
+ </MjmlSection>
282
+ </MjmlWrapper>
283
+ ```
284
+
285
+ When using `direction="rtl"`:
286
+
287
+ - **Use `MjmlWrapper` instead of `indent`** — applying `indent` on a `direction="rtl"` section causes a 1px line artifact in Outlook. Wrap the section in `MjmlWrapper` with `padding={`0 ${sectionIndent}px`}` and set `backgroundColor` to match.
288
+ - **Source order = mobile stack order** — the small column is first in the JSX, so it stacks on top on mobile. `direction="rtl"` only affects the visual left-to-right order on desktop.
289
+
290
+ ---
291
+
292
+ ## Grouping Sections with a Shared Background
293
+
294
+ When multiple sections need to share a background — for example, a multi-row footer with its own color — wrap them in `MjmlWrapper`. The wrapper owns the background; inner `MjmlSection`s suppress their own theme-default `backgroundColor` so the wrapper's color shows through.
295
+
296
+ ```tsx
297
+ <MjmlWrapper backgroundColor="#2d4a6e">
298
+ <MjmlSection indent>
299
+ <MjmlColumn>
300
+ <MjmlText color="#ffffff">Footer row 1</MjmlText>
301
+ </MjmlColumn>
302
+ </MjmlSection>
303
+ <MjmlSection indent>
304
+ <MjmlColumn>
305
+ <MjmlText color="#ffffff">Footer row 2</MjmlText>
306
+ </MjmlColumn>
307
+ </MjmlSection>
308
+ </MjmlWrapper>
309
+ ```
310
+
311
+ Key behaviors:
312
+
313
+ - `MjmlWrapper` applies `theme.colors.background.content` as its default background when a theme is present, so the `backgroundColor` prop is only needed when the wrapper should differ from the theme default.
314
+ - An explicit `backgroundColor` on an inner `MjmlSection` still wins — use that only when a single section inside the wrapper needs to stand out.
315
+ - For a region that also needs different default text color or variants, combine `MjmlWrapper` with a scoped `ThemeProvider` (see [`components-and-theme.md`](components-and-theme.md) → Scoped Theming). Text components pick up the scoped theme while the wrapper provides the background.
@@ -0,0 +1,306 @@
1
+ # Styling & Customization Reference
2
+
3
+ Deep dive into the desktop-first styling model, `registerStyles`, BEM naming, custom component patterns, and overriding built-in components.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Desktop-First Styling Model](#desktop-first-styling-model)
8
+ 2. [The `css` Helper](#the-css-helper)
9
+ 3. [Registering Responsive Styles](#registering-responsive-styles)
10
+ 4. [BEM Class Naming Convention](#bem-class-naming-convention)
11
+ 5. [Custom Component Pattern](#custom-component-pattern)
12
+ 6. [The `belowMediaQuery` Pattern](#the-belowmediaquery-pattern)
13
+ 7. [Overriding Built-In Components](#overriding-built-in-components)
14
+ 8. [Forwarding Props via `slotProps`](#forwarding-props-via-slotprops)
15
+ 9. [MJML Table Structure](#mjml-table-structure)
16
+
17
+ ---
18
+
19
+ ## Desktop-First Styling Model
20
+
21
+ Email clients that lack CSS support are almost exclusively desktop clients (Outlook 2007–2019, older Lotus Notes). Mobile email clients — Apple Mail, Gmail app, Outlook mobile — all support `<style>` blocks and media queries.
22
+
23
+ This means:
24
+
25
+ - **Desktop/default rendering** → inline styles only (MJML props compile to inline styles)
26
+ - **Mobile/responsive overrides** → `<style>` blocks with media queries via `registerStyles`
27
+
28
+ The base email must look correct with zero CSS from `<style>` blocks. Media queries are layered on top as progressive enhancement for mobile viewports.
29
+
30
+ ### Why `!important`?
31
+
32
+ Email clients inline all styles during processing. Since inline styles have higher CSS specificity than `<style>` block rules, responsive overrides must use `!important` to win. Every property inside a media query override should have `!important`.
33
+
34
+ ---
35
+
36
+ ## The `css` Helper
37
+
38
+ `css` is a tagged template literal that returns a plain string. Its only purpose is enabling CSS syntax highlighting and auto-formatting in editors (e.g., the styled-components VS Code extension):
39
+
40
+ ```ts
41
+ import { css } from "@comet/mail-react";
42
+
43
+ const styles = css`
44
+ @media (max-width: 419px) {
45
+ .myComponent {
46
+ padding: 12px !important;
47
+ }
48
+ }
49
+ `;
50
+ ```
51
+
52
+ No runtime styling logic — purely a developer experience improvement.
53
+
54
+ ---
55
+
56
+ ## Registering Responsive Styles
57
+
58
+ `registerStyles` adds CSS to the email's `<head>` as `<style>` blocks. Call it at the **module level** (outside component functions) so styles are registered once when the module is first imported.
59
+
60
+ Since `<style>` blocks are ignored by some desktop clients, `registerStyles` is intended for responsive overrides inside media queries — not for base styles, which must be inline.
61
+
62
+ ### Theme-Aware CSS (Preferred)
63
+
64
+ When you need access to theme tokens (breakpoints, colors, sizes) — which is **most of the time**. Always prefer theme-aware styles over static CSS so that breakpoints and tokens stay in sync with the theme. If a breakpoint value is needed repeatedly but doesn't exist in the theme, add it via `createBreakpoint` and module augmentation rather than hardcoding media queries.
65
+
66
+ ```ts
67
+ registerStyles(
68
+ (theme) => css`
69
+ ${theme.breakpoints.mobile.belowMediaQuery} {
70
+ .calloutBox {
71
+ padding: 12px !important;
72
+ border-color: ${theme.colors.background.body} !important;
73
+ }
74
+ }
75
+ `,
76
+ );
77
+ ```
78
+
79
+ Theme-aware entries are called at render time with the theme from `MjmlMailRoot`. Using `theme.breakpoints.mobile.belowMediaQuery` keeps styles in sync with the theme's breakpoint configuration.
80
+
81
+ ### Static CSS
82
+
83
+ When you genuinely don't need any theme values (rare — most responsive styles benefit from theme breakpoints):
84
+
85
+ ```ts
86
+ import { css, registerStyles } from "@comet/mail-react";
87
+
88
+ registerStyles(css`
89
+ @media (max-width: 419px) {
90
+ .calloutBox {
91
+ padding: 12px !important;
92
+ }
93
+ }
94
+ `);
95
+ ```
96
+
97
+ ### Important Details
98
+
99
+ - Call at **module level**, not inside component functions
100
+ - `MjmlMailRoot` renders all registered styles automatically
101
+ - Duplicate registrations with the same function/string reference overwrite (stored in a `Map`)
102
+ - Theme-aware entries always resolve against the **root theme** from `MjmlMailRoot` — nested `ThemeProvider` scopes do not affect them
103
+ - Optional second argument: partial `MjmlStyle` props (forwarded to the underlying `<mj-style>`)
104
+
105
+ ---
106
+
107
+ ## BEM Class Naming Convention
108
+
109
+ Use BEM with camelCase blocks for custom component CSS classes:
110
+
111
+ | BEM Part | Pattern | Example |
112
+ | -------- | ----------------------------- | ------------------------- |
113
+ | Block | `componentName` | `calloutBox` |
114
+ | Element | `componentName__elementName` | `calloutBox__title` |
115
+ | Modifier | `componentName--modifierName` | `calloutBox--highlighted` |
116
+
117
+ Built-in components follow this convention:
118
+
119
+ - `mjmlSection`, `mjmlSection--indented`
120
+ - `mjmlText`, `mjmlText--heading`, `mjmlText--bottomSpacing`
121
+ - `htmlText`, `htmlText--body`
122
+ - `htmlInlineLink`
123
+
124
+ ---
125
+
126
+ ## Custom Component Pattern
127
+
128
+ The complete pattern for building email-safe custom components:
129
+
130
+ 1. **Inline styles** for base/desktop rendering — set via `style` props on HTML elements
131
+ 2. **BEM class names** on elements that need responsive overrides
132
+ 3. **`registerStyles`** at module level with media queries targeting those classes
133
+ 4. **`!important`** on all responsive overrides
134
+
135
+ ```tsx
136
+ import { css, MjmlColumn, MjmlRaw, MjmlSection, registerStyles } from "@comet/mail-react";
137
+
138
+ interface FeatureCardProps {
139
+ title: string;
140
+ description: string;
141
+ highlighted?: boolean;
142
+ }
143
+
144
+ function FeatureCard({ title, description, highlighted = false }: FeatureCardProps) {
145
+ return (
146
+ <MjmlSection>
147
+ <MjmlColumn>
148
+ <MjmlRaw>
149
+ <tr>
150
+ <td
151
+ className={`featureCard${highlighted ? " featureCard--highlighted" : ""}`}
152
+ style={{
153
+ padding: "24px",
154
+ border: "1px solid #E0E0E0",
155
+ borderRadius: "8px",
156
+ backgroundColor: highlighted ? "#F0F7FF" : "#FFFFFF",
157
+ }}
158
+ >
159
+ <span
160
+ className="featureCard__title"
161
+ style={{
162
+ display: "block",
163
+ fontSize: "20px",
164
+ fontWeight: 700,
165
+ lineHeight: "28px",
166
+ msoLineHeightRule: "exactly",
167
+ margin: "0 0 8px 0",
168
+ }}
169
+ >
170
+ {title}
171
+ </span>
172
+ <span
173
+ className="featureCard__description"
174
+ style={{
175
+ display: "block",
176
+ fontSize: "14px",
177
+ lineHeight: "22px",
178
+ msoLineHeightRule: "exactly",
179
+ color: "#555555",
180
+ }}
181
+ >
182
+ {description}
183
+ </span>
184
+ </td>
185
+ </tr>
186
+ </MjmlRaw>
187
+ </MjmlColumn>
188
+ </MjmlSection>
189
+ );
190
+ }
191
+
192
+ registerStyles(
193
+ (theme) => css`
194
+ ${theme.breakpoints.mobile.belowMediaQuery} {
195
+ .featureCard {
196
+ padding: 16px !important;
197
+ }
198
+ .featureCard__title {
199
+ font-size: 18px !important;
200
+ line-height: 24px !important;
201
+ }
202
+ .featureCard__description {
203
+ font-size: 13px !important;
204
+ }
205
+ }
206
+ `,
207
+ );
208
+ ```
209
+
210
+ ### Key Reminders
211
+
212
+ - Inside `MjmlRaw` and other ending tags, you're in HTML-land — only HTML elements, no MJML components
213
+ - Always reset margins on block-level elements: `style={{ margin: 0 }}`
214
+ - **Every element with a manual `line-height`** must also have `mso-line-height-rule: exactly` as an inline style — Outlook ignores standard line-height calculations and produces unexpected vertical spacing without it. This is easy to forget on `<span>` and `<td>` elements inside custom components.
215
+ - `borderRadius` won't render in Outlook — provide a visually acceptable fallback
216
+
217
+ ---
218
+
219
+ ## The `belowMediaQuery` Pattern
220
+
221
+ Every breakpoint in the theme has a `belowMediaQuery` property — a ready-to-use CSS media query string targeting viewports below that breakpoint:
222
+
223
+ ```ts
224
+ theme.breakpoints.mobile.belowMediaQuery;
225
+ // → "@media (max-width: 419px)" (for mobile breakpoint at 420px)
226
+
227
+ theme.breakpoints.tablet.belowMediaQuery;
228
+ // → "@media (max-width: 539px)" (for tablet breakpoint at 540px, if augmented)
229
+ ```
230
+
231
+ Always use `belowMediaQuery` instead of hardcoding media query values:
232
+
233
+ ```ts
234
+ registerStyles(
235
+ (theme) => css`
236
+ ${theme.breakpoints.mobile.belowMediaQuery} {
237
+ .myComponent {
238
+ font-size: 14px !important;
239
+ padding: 10px !important;
240
+ }
241
+ }
242
+ `,
243
+ );
244
+ ```
245
+
246
+ This keeps responsive styles in sync with the theme's breakpoint configuration.
247
+
248
+ ---
249
+
250
+ ## Overriding Built-In Components
251
+
252
+ Target built-in CSS class names in `registerStyles` to add responsive overrides to library components:
253
+
254
+ ```ts
255
+ registerStyles(
256
+ (theme) => css`
257
+ ${theme.breakpoints.mobile.belowMediaQuery} {
258
+ .mjmlSection--indented > table > tbody > tr > td {
259
+ background-color: ${theme.colors.background.body} !important;
260
+ }
261
+ }
262
+ `,
263
+ );
264
+ ```
265
+
266
+ ### Built-In CSS Class Names
267
+
268
+ | Component | Classes |
269
+ | ---------------- | --------------------------------------------------------------- |
270
+ | `MjmlSection` | `.mjmlSection`, `.mjmlSection--indented` |
271
+ | `MjmlText` | `.mjmlText`, `.mjmlText--{variant}`, `.mjmlText--bottomSpacing` |
272
+ | `HtmlText` | `.htmlText`, `.htmlText--{variant}`, `.htmlText--bottomSpacing` |
273
+ | `HtmlInlineLink` | `.htmlInlineLink` |
274
+
275
+ ---
276
+
277
+ ## Forwarding Props via `slotProps`
278
+
279
+ Some components use internal sub-components not directly accessible through the main API. These expose a `slotProps` prop for forwarding:
280
+
281
+ ```tsx
282
+ <MjmlSection disableResponsiveBehavior slotProps={{ group: { width: "100%" } }}>
283
+ <MjmlColumn>
284
+ <MjmlText>Column 1</MjmlText>
285
+ </MjmlColumn>
286
+ <MjmlColumn>
287
+ <MjmlText>Column 2</MjmlText>
288
+ </MjmlColumn>
289
+ </MjmlSection>
290
+ ```
291
+
292
+ Currently, `MjmlSection` exposes `slotProps.group` for forwarding props to the internal `MjmlGroup` when `disableResponsiveBehavior` is enabled.
293
+
294
+ ---
295
+
296
+ ## MJML Table Structure
297
+
298
+ MJML generates table-based HTML. When targeting nested elements inside MJML components via CSS, you may need to traverse the generated table structure:
299
+
300
+ ```css
301
+ .mjmlSection--indented > table > tbody > tr > td {
302
+ /* targets the actual content cell */
303
+ }
304
+ ```
305
+
306
+ Use browser dev tools in Storybook to inspect the generated HTML structure when writing CSS selectors for built-in components. The exact nesting varies by component and shouldn't be assumed — always inspect first.