--- SECTION: INTRO --- # Email Builder — Introduction & LLM Reference Overview This document is the entry point for understanding the email builder system. Read it before consulting any individual component reference. --- ## What This Builder Does This is a **code-driven email builder** that generates HTML email templates compatible with all major email clients, including Outlook Classic (Word rendering engine), Gmail, Apple Mail, and mobile clients. Templates are defined as a JSON `DocumentTree` — a tree of `ComponentNode` objects — which the builder compiles to table-based HTML with inline styles, MSO conditional comments, and responsive media queries. --- ## Document Model Every email is a tree of **ComponentNodes**. Each node has: ```json { "id": "unique-string", "type": "ComponentType", "config": { /* component-specific properties */ }, "bindings": { /* optional: data layer connections */ }, "children": [ /* optional: child ComponentNodes */ ] } ``` The tree always starts with an `Html` root and follows a strict hierarchy. See **SECTION: TEMPLATE** for the full structural contract. > **`id` uniqueness is required.** Every `ComponentNode` in the entire document tree must have a globally unique `id`. Duplicate `id` values anywhere in the tree — across sections, containers, or nested components — will cause undefined rendering behavior. Always use distinct, descriptive identifiers for every node. --- ## Component Types ### Structural (document skeleton) | Component | Role | |---|---| | `Html` | Document root. Contains `Head` and `Body`. | | `Head` | Email `
`: meta tags, fonts, global CSS resets. | | `Body` | Email canvas: sets width, background, and centers content. | | `Section` | Full-width horizontal band. Exactly 3 per template (`header`, `content`, `footer`). | ### Layout (arrange content) | Component | Role | |---|---| | `Container` | Primary layout unit. Direct child of `Section`. Distributes children horizontally into columns. Cannot nest inside another Container. | | `Column` | Vertical stacking primitive inside a Container cell. Freely nestable. | | `Row` | Horizontal arrangement primitive. Freely nestable. | ### Leaf (content) | Component | Role | |---|---| | `Text` | Styled HTML text. Primary typography primitive. | | `Heading` | Semantic headings (`h1`–`h6`) with full text styling. | | `Image` | Responsive, email-compatible image with optional link. | | `Button` | Clickable CTA button with Outlook VML support. | | `Icon` | Iconify-powered icon with background and link support. | | `Divider` | Horizontal rule for separating content. | | `Spacer` | Fixed-height vertical whitespace. | | `Font` | Web font declaration for the `Head`. Not a content node. | --- ## Hierarchy at a Glance ``` Html └── Body └── Section (×3, fixed: header / content / footer) └── Container (stacks vertically; no nesting) └── Column / Row (freely nestable) └── Text / Image / Button / Heading / Icon / Divider / Spacer ``` **Key constraints:** - There are **always exactly 3 Sections** per template — never more, never fewer. - A **Container cannot be nested inside another Container** under any circumstance. - **Column and Row can nest freely** inside each other and inside Container cells. - **Leaf nodes are terminal** — they have no structural children. For the full nesting rule set, see **SECTION: TEMPLATE**. --- ## Data Layer & Bindings Any `ComponentNode` from `Section` downward can carry a `bindings` object to connect it to the Data Layer — a structured JSON object provided at render time. ```json "bindings": { "dataList": "products", "itemAlias": "product", "visible": "user.isPremium === true", "propertyMap": { "config.src": "product.image.src", "config.alt": "product.image.alt" } } ``` See **SECTION: DATA-BINDING** for the full reference. --- ## Reference Sections | Section | Contents | |---|---| | `SECTION: INTRO` | This file. System overview and orientation. | | `SECTION: TEMPLATE` | Full structural contract: nesting rules, hierarchy, the 3-section model. | | `SECTION: DATA-BINDING` | Data Layer, `bindings` object, `propertyMap`, `dataList`, `visible`. | | `SECTION: HTML` | `Html` component reference. | | `SECTION: HEAD` | `Head` component reference. | | `SECTION: BODY` | `Body` component reference. | | `SECTION: SECTION` | `Section` component reference. | | `SECTION: CONTAINER` | `Container` component reference. | | `SECTION: COLUMN` | `Column` component reference. | | `SECTION: ROW` | `Row` component reference. | | `SECTION: TEXT` | `Text` component reference. | | `SECTION: HEADING` | `Heading` component reference. | | `SECTION: IMAGE` | `Image` component reference. | | `SECTION: BUTTON` | `Button` component reference. | | `SECTION: ICON` | `Icon` component reference. | | `SECTION: DIVIDER` | `Divider` component reference. | | `SECTION: SPACER` | `Spacer` component reference. | | `SECTION: FONT` | `Font` component reference. | --- SECTION: TEMPLATE --- # Template — LLM Reference A **template** is the complete structure of an email document. It is the top-level JSON object that the builder reads, renders, and exports. Every template is a plain object with three top-level fields: `bodyStyle`, `tree`, and an optional `dataLayer`. The `tree` is an array of exactly three `Section` nodes — `header`, `content`, and `footer` — in that order, always. > **Note:** `Html`, `Head`, and `Body` are not part of the document object. They are internal rendering concerns handled by the builder. The document you author and store contains only `bodyStyle`, `dataLayer`, and `tree`. --- ## Document Shape ``` { bodyStyle: { … } ← Body styling: width, background, padding, font dataLayer: { … } ← (optional) data for bindings tree: [ Section (sectionType: "header") ← always index 0, never moves Section (sectionType: "content") ← always index 1, never moves Section (sectionType: "footer") ← always index 2, never moves ] } ``` The three Sections are **fixed structural slots**. They are not added or removed — they are always present. Their order is immutable. The builder uses `sectionType` to identify each slot; the slots themselves are not interchangeable. --- ## Full Document JSON Structure ```json { "bodyStyle": { "backgroundColor": "#ffffff", "fontFamily": "sans-serif" }, "dataLayer": {}, "tree": [ { "id": "section-header", "type": "SectionComponent", "config": { "sectionType": "header", "backgroundColor": "#ffffff", "padding": "0" }, "children": [] }, { "id": "section-content", "type": "SectionComponent", "config": { "sectionType": "content", "backgroundColor": "#ffffff", "padding": "0" }, "children": [] }, { "id": "section-footer", "type": "SectionComponent", "config": { "sectionType": "footer", "backgroundColor": "#f9fafb", "padding": "0" }, "children": [] } ] } ``` --- ## Nesting Rules These rules are absolute. Violating them produces invalid templates. ### Section - There are **exactly three** Sections per template: `header`, `content`, `footer`. - **`sectionType` is mandatory** — every Section must declare `"sectionType": "header" | "content" | "footer"` in its `config`. - Sections **cannot be nested** inside each other or inside any other component. - Sections **cannot be added or removed** — they are always present. - Sections **cannot be reordered** — `header` is always first, `content` second, `footer` last. - The only **allowed children** of a Section are `Container` nodes. ### Container - A `Container` is always a **direct child of a Section**. - Containers **cannot be nested** inside another Container, directly or indirectly. - Multiple Containers can exist inside one Section — they stack **vertically**. - The only **allowed parent** of a Container is a Section. ### Column and Row - `Column` and `Row` live **inside a Container** cell, below the Container level. - `Column` and `Row` **can nest freely and infinitely** inside each other. - Neither `Column` nor `Row` can be a direct child of a Section. ### Leaf components - `Text`, `Image`, `Button`, `Heading`, `Icon`, `Divider`, `Spacer` are leaf nodes. - They live inside `Column`, `Row`, or directly in a `Container` cell. - They cannot have structural children. --- ## Component Hierarchy Summary ``` tree (array of 3) └── Section ← exactly 3; fixed positions └── Container ← direct child of Section only; no nesting └── Column / Row ← freely nestable └── Text / Image / Button / Heading / Icon / Divider / Spacer ``` | Component | Allowed parent | Can nest itself? | Children | |---|---|---|---| | `Section` | `tree` (root array) | ✗ No | `Container` only | | `Container` | `Section` only | ✗ No | `Column`, `Row`, leaf nodes | | `Column` | `Container`, `Column`, `Row` | ✓ Yes | `Column`, `Row`, leaf nodes | | `Row` | `Container`, `Column`, `Row` | ✓ Yes | `Column`, `Row`, leaf nodes | | Leaf nodes | `Column`, `Row`, `Container` | ✗ No | None | --- ## The Three Sections ### `header` - Always the first Section in `tree` (index 0). - Intended for: logo, brand bar, navigation, pre-header text. - Contains one or more `Container` children stacked vertically. ### `content` - Always the second Section in `tree` (index 1). - Intended for: all email body content — hero, product rows, articles, CTAs, dividers. - Multiple `Container` children stack vertically to build up the content area. - This is where the bulk of the email lives. ### `footer` - Always the third Section in `tree` (index 2). - Intended for: unsubscribe link, legal text, social icons, mailing address. - Contains one or more `Container` children stacked vertically. --- ## Top-Level Fields ### `bodyStyle` Controls the email canvas appearance. All properties are optional. | Property | Type | Description | |---|---|---| | `color` | `string` | Default text color | | `fontSize` | `string` | Default font size | | `backgroundColor` | `string` | Background color of the email canvas | | `lineHeight` | `string` | Default line height | | `fontFamily` | `string` | Default font family | | `backgroundImage.src` | `string` | Background image URL | | `backgroundImage.repeat` | `string` | CSS `background-repeat` value | | `backgroundImage.size` | `string` | CSS `background-size` value | | `backgroundImage.position` | `string` | CSS `background-position` value | ### `dataLayer` An optional plain JSON object passed to the renderer at render time. It is the single source of truth for all dynamic values used by `bindings`. See **SECTION: DATA-BINDING** for the full reference. Omit or set to `{}` when the template has no dynamic content. ### `tree` An array of exactly three `Section` nodes in fixed order: `header`, `content`, `footer`. This is the entire component tree of the email body. Each `Section` holds `Container` children which in turn hold the layout and leaf components. --- ## Rules & Constraints - **One document = one flat object.** There is no `Html`, `Head`, or `Body` node in the document — those are internal rendering details handled by the builder. - **`tree` has exactly three items.** The three Sections are the only entries in `tree`. They cannot be added, removed, or reordered. - **Sections are immovable.** Their `sectionType` values and order are fixed: index 0 = `header`, index 1 = `content`, index 2 = `footer`. - **Containers do not nest.** Any container-inside-container structure is invalid regardless of depth. - **`Column` and `Row` are the only freely nestable structural components.** Use them to build any layout complexity below the Container level. - **Leaf nodes are terminal.** They cannot contain other components. - **`bodyStyle` is not a `ComponentNode`.** It is a plain config object, not a node with `id`, `type`, or `children`. --- SECTION: DATA-BINDING --- # Data Binding — LLM Reference Data binding connects a `ComponentNode` to the **Data Layer** — a structured JSON object provided at render time. It enables dynamic content, conditional visibility, and repeating components without duplicating template structure. --- ## The `bindings` Object Any `ComponentNode` from `Section` downward can carry a `bindings` property at the root of the node, alongside `id`, `type`, and `config`. ```json { "id": "product-image", "type": "ImageComponent", "bindings": { "dataList": "products", "itemAlias": "product", "visible": "user.isPremium === true", "propertyMap": { "config.src": "product.image.src", "config.alt": "product.image.alt" } }, "config": { "src": "", "alt": "", "width": "100%" } } ``` > **`Body`, `Head`, and `Html` do not support bindings.** The binding system begins at `Section` level and applies to all descendants. --- ## `bindings` Properties ### `dataList` Makes the component a **repeater**. The component (and all its descendants) is rendered once per item in the referenced array. ```json "bindings": { "dataList": "job_listings", "itemAlias": "job" } ``` - Value is a dot-path to an array in the Data Layer (e.g. `"products"`, `"company.open_roles"`). - **Only components with structural children support `dataList`**: `Section`, `Container`, `Column`, `Row`. - Leaf nodes (`Text`, `Image`, `Button`, etc.) do not support `dataList` — place `dataList` on a parent `Container`, `Column`, or `Row` to repeat them. - `Section` supports `dataList` in principle, but since there are exactly 3 fixed Sections per template, using `dataList` there is rarely appropriate. Prefer `dataList` on a `Container` inside the `content` Section. --- ### `itemAlias` Names the current iteration item so descendant components can reference it in their own `propertyMap` and `visible` expressions. ```json "bindings": { "dataList": "products", "itemAlias": "product" } ``` - Required whenever `dataList` is set. - Descendant nodes reference the item using dot-notation: `"product.name"`, `"product.image.src"`, `"product.price"`. - The alias is scoped to the subtree of the repeating component — it is not accessible outside it. --- ### `visible` Controls conditional rendering. The component is hidden when the expression evaluates to falsy. ```json "bindings": { "visible": "user.isPremium === true" } ``` - Value is a JavaScript expression evaluated against the Data Layer. - Supported by all components from `Section` downward, including leaf nodes. - Can be combined with `dataList` to filter items: `"product.inStock === true"`. - When a component is hidden, it produces no HTML output — it does not occupy space. --- ### `propertyMap` Maps specific `config` properties of the component to Data Layer paths. This is the primary mechanism for injecting dynamic values into a component. ```json "bindings": { "propertyMap": { "config.src": "product.image.src", "config.alt": "product.image.alt", "config.backgroundColor": "product.brandColor" } } ``` **Key format:** Keys must be prefixed with `config.` followed by the exact property name as it appears in the component's `config` object. ``` "config.Hello world
", "padding": "16px 24px", "color": "#111827", "textAlign": "left", "fontFamily": "Arial, sans-serif", "fontSize": "16px", "fontWeight": "400", "fontStyle": "normal", "lineHeight": "1.6", "letterSpacing": "0px", "textTransform": "none", "textDecoration": "none", "backgroundColor": "#ffffff", "opacity": 1, "whiteSpace": "normal", "wordBreak": "break-all", "maxWidth": "480px", "direction": "ltr" } } ``` --- ## `TextConfig` — Full Property Reference ### Content | Property | Type | Description | |---|---|---| | `text` | `string` | HTML string rendered via `dangerouslySetInnerHTML`. Link styles are automatically injected to match the text's color and decoration. If omitted, `children` (React nodes) are rendered instead. | ### Layout | Property | Type | Example | Description | |---|---|---|---| | `padding` | `string` | `"16px 24px"` | Applied to the containing `` — paragraph - `
` — quoted text ### Permitted inline tag: `` only The **only permitted inline tag** is ``. It may carry a `style` attribute, but only with the CSS properties listed in the allowed inline styles below. **All semantic inline HTML tags are forbidden.** This includes, but is not limited to: | Forbidden tag | Reason | |---|---| | `` | Use `` instead | | `` | Use `` instead | | `` | Use `` instead | | `` | Use `` instead | | `` | Use `` instead | | ``, ``, `` | Use `` instead | | `` | Use `` instead | | `` | Use `` instead | | ``, `` | Not supported | | `
` | Use separate `` elements for line breaks | | `
` | Images are not permitted inside `text`; use a dedicated image component | | Any `
`, ``, structural tags | Not permitted inside `text` | ### Permitted inline tag: `` for links The `` tag is permitted for hyperlinks. See **Link Style Injection** below for detailed behavior. ### Allowed inline styles on `` and `` When using `` or ``, only the following CSS properties are permitted. These correspond to the properties the parent `TextConfig` already manages — using them inline overrides those values for the spanned content only. | CSS property | Example value | |---|---| | `font-weight` | `"700"`, `"400"`, `"bold"` | | `font-style` | `"italic"`, `"normal"` | | `font-size` | `"14px"`, `"1.2em"` | | `color` | `"#6366f1"` | | `background-color` | `"#fef9c3"` | | `text-decoration` | `"underline"`, `"line-through"`, `"none"` | | `letter-spacing` | `"1px"` | | `text-transform` | `"uppercase"`, `"capitalize"` | | `vertical-align` | `"super"`, `"sub"` | | `font-family` | `"Georgia, serif"` | **Do not use** layout-related properties (`margin`, `padding`, `display`, `width`, `float`, etc.) on inline elements inside `text`. These are not meaningful in this context and will produce unpredictable results in email clients. ### Correct and incorrect examples **✅ Correct — use `` with `font-weight` for bold text:** ```json "text": " Your order is confirmed.
" ``` **❌ Incorrect — do not use ``:** ```json "text": "Your order is confirmed.
" ``` **✅ Correct — use `` with `font-style` for italic text:** ```json "text": "Terms apply. See terms and conditions.
" ``` **❌ Incorrect — do not use `` or ``:** ```json "text": "Terms apply. See terms and conditions.
" ``` **✅ Correct — use `` with `color` for inline color changes:** ```json "text": "Price: $0.00
" ``` **✅ Correct — use multiple `` tags instead of `
`:** ```json "text": "Line one
Line two
" ``` **❌ Incorrect — do not use `
`:** ```json "text": "Line one
" ``` **✅ Correct — link with manual color override:** ```json "text": "
Line twoVisit our site
" ``` --- ## `text` vs `children` `Text` accepts content in two ways: **`config.text` (string)** — an HTML string rendered via `dangerouslySetInnerHTML`. Link styles are automatically injected into `` tags inside the HTML via `injectLinkStyles`. This is the standard mode used by the builder. ```json "text": "Visit our website for details.
" ``` **`children` (React nodes)** — rendered directly inside the styled ``. Used when composing `Text` programmatically in React. `injectLinkStyles` is not applied in this mode. If both `config.text` and `children` are provided, `config.text` takes precedence. --- ## `maxWidth` Behavior `maxWidth` uses the same Outlook Classic compatibility pattern as `Column.maxWidth`: - The outer `` stays at `width: 100%` — it always fills its parent cell - Inside it, ` ` handles horizontal centering in Outlook Classic (Word engine) - Inside ` `, an inner table with `width={maxWidth}` as an HTML attribute — Outlook Classic respects this as a hard pixel cap - Modern clients receive `max-width: {maxWidth}` as CSS on the same table > **Important:** `maxWidth` is **not** applied to the inner ` ` — CSS `max-width` on a `` is ignored by Outlook Classic. The constraint is enforced at the table level only. **When to use:** When a Text block sits in a wide column but you want to limit the line length for readability. For example, a 600px Section containing a single text column where lines should cap at 480px. ```json "config": { "maxWidth": "480px", "textAlign": "center" } ``` --- ## Link Style Injection (`injectLinkStyles`) The `Text` component uses `injectLinkStyles` to automatically manage link styles. Understanding how it works is critical for correct AI-generated content. ### What `injectLinkStyles` Does When `config.text` is a string, the component runs the HTML through `injectLinkStyles`, which: 1. Parses the HTML and finds all `` tags 2. For each `` tag, examines its existing inline `style` attributes 3. **Conditionally injects** missing styles for `color` and `text-decoration` only when: - The property is **not already defined** on the `` tag's inline style, AND - The property is **not inheriting** a valid value from parent elements ### The Algorithm (Simplified) For each `` tag, `injectLinkStyles` checks two properties: `color` and `text-decoration`. **For each property:** - If the `` tag already has this property defined in its inline `style` attribute → **DO NOTHING** (keep the manual value) - If the `` tag does NOT have this property defined: - Try to inherit from parent elements (like a surrounding ``) - If no inherited value exists, use the `fallback` from the text component's config (e.g., `color`, `textDecoration`) - If no fallback exists for `text-decoration`, default to `"none"` **Key takeaway:** Manual inline styles on `` tags are **never overwritten**. They always take precedence. ### Link Style Resolution Examples **Example 1: No manual styles on ``** ```html Click here ``` Result: Receives `color` and `text-decoration` from text config (fallback). If config has `color: "#3b82f6"` and `textDecoration: "underline"`, the link gets those. **Example 2: Manual `color` only** ```html Click here ``` Result: - `color: #ff0000` → **preserved** (manual wins) - `text-decoration` → injected from text config (or defaults to `"none"`) **Example 3: Manual `text-decoration` only** ```html Click here ``` Result: - `text-decoration: none` → **preserved** (manual wins) - `color` → injected from text config **Example 4: Both properties manually defined** ```html Click here ``` Result: Both properties preserved. Nothing is injected. Your manual styles win completely. **Example 5: Inherited from parent ``** ```html Click here ``` Result: - No manual styles on `` - `color` is inherited from the parent `` → `#00ff00` is used - `text-decoration` injected from text config (or defaults to `"none"`) ### Summary: What `injectLinkStyles` Does NOT Do - ❌ It does **NOT** remove or override any existing inline styles on `` tags - ❌ It does **NOT** prevent you from adding any supported style property to `` tags - ❌ It does **NOT** restrict what colors, decorations, or weights you can apply to links - ❌ It does **NOT** inject styles for properties already defined on the `` tag ### Best Practices for AI-Generated Content **✅ You can safely add any supported inline style to `` tags** — they will never be stripped or overridden: ```html Custom styled link ``` **✅ You can rely on automatic injection** — if you don't specify styles, the component ensures links look consistent with your text config: ```html Link that inherits text styles ``` **✅ You can mix both approaches** — let the component handle some properties while manually controlling others: ```html Bold link that inherits color from text config ``` --- ## `wordBreak` Default Unlike all other properties, `wordBreak` has a **non-`undefined` default**: it defaults to `"break-all"` when not specified. This prevents long URLs or unbroken strings from overflowing narrow email columns in most clients. Set `wordBreak: "normal"` explicitly if you need standard word-break behavior (e.g., for CJK text or controlled layouts where overflow is handled elsewhere). --- ## React Usage ```tsx import Text, { TextConfig } from './Text'; // String HTML content (standard) const config: TextConfig = { text: 'Hello world
', fontSize: '16px', color: '#111827', lineHeight: '1.6', padding: '16px 24px', };// React node children (programmatic) Custom content ``` The component is memoized via `arePropsEqual`. Pass stable `config` objects to avoid re-renders. --- ## Common Patterns & Examples ### 1. Body paragraph ```json { "id": "body-text", "type": "TextComponent", "config": { "text": "Your order has been confirmed and will ship within 2 business days.
", "fontSize": "16px", "fontFamily": "Arial, sans-serif", "color": "#374151", "lineHeight": "1.6", "padding": "0 0 16px 0" } } ``` ### 2. Section heading within Text component ```json { "id": "text-section-heading", "type": "TextComponent", "config": { "text": "Welcome back, Alex
", "fontSize": "28px", "fontWeight": "700", "color": "#111827", "lineHeight": "1.2", "textAlign": "center", "padding": "0 0 8px 0" } } ``` ### 3. Link with automatic style injection (no manual styles) ```json { "id": "footer-legal", "type": "TextComponent", "config": { "text": "You received this email because you signed up at example.com. Unsubscribe
", "fontSize": "12px", "color": "#9ca3af", "textAlign": "center", "lineHeight": "1.5", "padding": "16px 24px" } } ``` The unsubscribe link automatically receives `color: #9ca3af` via `injectLinkStyles`. ### 4. Link with manual style override ```json { "id": "promo-link", "type": "TextComponent", "config": { "text": "Special offer ends soon
", "fontSize": "16px", "color": "#3b82f6", "textDecoration": "none" } } ``` The link keeps `color: #ff0000` and `font-weight: 800` (manual wins). `text-decoration` is NOT injected because the text config has `textDecoration: "none"` which becomes the fallback. ### 5. Link inheriting from parent span ```json { "id": "inherited-link", "type": "TextComponent", "config": { "text": "Check this deal
", "fontSize": "16px", "color": "#3b82f6", "textDecoration": "underline" } } ``` The link inherits `color: #00aa00` from the parent ``. `text-decoration: underline` is injected from the text config since the link has no manual decoration. ### 6. Mixed inline styles (bold + colored span) ```json { "id": "mixed-inline", "type": "TextComponent", "config": { "text": "Save 30% on your next order.
", "fontSize": "16px", "color": "#111827", "lineHeight": "1.5" } } ``` ### 7. Constrained reading-width text ```json { "id": "article-body", "type": "TextComponent", "config": { "text": "Long-form article content goes here...
", "maxWidth": "480px", "textAlign": "left", "fontSize": "16px", "color": "#374151", "lineHeight": "1.7", "padding": "0" } } ``` ### 8. RTL text block ```json { "id": "rtl-text", "type": "TextComponent", "config": { "text": "مرحباً بك في متجرنا
", "direction": "rtl", "textAlign": "right", "fontFamily": "Arial, sans-serif", "fontSize": "16px", "color": "#111827", "lineHeight": "1.8" } } ``` ### 9. Highlighted callout text with background ```json { "id": "highlight-text", "type": "TextComponent", "config": { "text": "Limited time offer: Free shipping on all orders over $50.
", "fontSize": "15px", "color": "#713f12", "backgroundColor": "#fef9c3", "padding": "12px 16px", "lineHeight": "1.5", "wordBreak": "break-word" } } ``` --- ## Data Bindings `Text` supports the `bindings` property on the `ComponentNode`, alongside `id`, `type`, and `config`. Leaf components do not support `dataList` or `itemAlias` — use those on a parent `Container`, `Column`, or `Row` to repeat the component. `visible` and `propertyMap` are fully supported. ```json { "id": "promo-text", "type": "TextComponent", "bindings": { "visible": "user.isPremium === true", "propertyMap": { "config.text": "promo.body", "config.color": "promo.textColor" } }, "config": { "text": "Fallback text
", "fontSize": "16px", "color": "#111827" } } ``` | Property | Description | |---|---| | `visible` | JS expression evaluated against the Data Layer. Hides this component when falsy. | | `propertyMap` | Maps component property paths (prefixed with `config.`) to Data Layer paths. | > Bindable properties include `config.text`, `config.color`, `config.backgroundColor`, `config.fontSize`, `config.fontFamily`, `config.padding`, and any other scalar `TextConfig` field. --- ## Rules & Constraints - **`config.text` must only contain permitted HTML.** Block elements (``, `
`–`
`, `
`, `
`, `
- `, `
`) and `` and `` are the only allowed tags. All semantic inline tags (``, ``, ``, ``, ``, `
`, etc.) are forbidden. - **Use `` for bold, never `` or ``.** Semantic weight tags are not supported. - **Use `` for italic, never `` or ``.** Semantic emphasis tags are not supported. - **Manual inline styles on `` tags are never removed or overridden** — `injectLinkStyles` only adds missing `color` and `text-decoration` properties. - **You can add any supported style property to `` tags** (`color`, `text-decoration`, `font-weight`, `font-style`). - **`` and `` may only carry allowed inline CSS properties.** Layout properties (`margin`, `padding`, `display`, `width`, etc.) are not permitted. - **`wordBreak` defaults to `"break-all"`** unless explicitly set. This is the only property with a non-`undefined` default. Set `"normal"` to opt out. - **`maxWidth` is enforced at the table level, not on the ``.** Do not attempt to replicate this behavior using CSS on inner elements — Outlook Classic will ignore it. - **`padding` is the only spacing mechanism.** There is no `margin` on `Text`. Use `padding` on this component or `gap` on the parent `Column` for spacing between text blocks. - **`backgroundColor` applies to the outer ``, not the ` `.** It covers the full padded area including the padding space. - **`text` takes precedence over `children`** when both are supplied. - **Link styles are only injected when `text` is a string.** React node `children` receive no automatic link styling — style links manually in that case. - **`listStyle` is informational.** It is stored in config and consumed by the builder UI, but is not applied as CSS by this component's renderer. - **`opacity` applies to the inner ``**, not the ``. This means `backgroundColor` (on the ` `) is not affected by `opacity` — the background remains fully opaque even when `opacity < 1`. - **`textAlign` is applied both as CSS and as the HTML `align` attribute** on the ` ` for Outlook Classic compatibility. No special handling needed — set `textAlign` normally. --- ## Quick Reference Card | Do ✅ | Don't ❌ | |------|---------| | `text` | `text` | | `text` | `text` | | `text` | `text` | | `text` | `text` | | `link` (manual color) | Worrying that manual styles will be stripped | | `link` (relies on injection) | Adding unsupported CSS properties | | Use separate ` ` tags for line breaks | `
` tags | | Combine spans: `` | Nest spans unnecessarily | | Let `injectLinkStyles` fill missing `color` and `text-decoration` | Assuming injection overrides your manual styles | --- ## Technical Reference: `injectLinkStyles` Behavior For implementers and advanced users, here is the exact behavior of `injectLinkStyles`: ```typescript // Properties that injectLinkStyles manages const BROWSER_LINK_DEFAULTS = ["color", "text-decoration"]; // Resolution logic for each anchor: // 1. If property exists in anchor's inline style → preserve it (skip injection) // 2. Else if property can be inherited from parent elements → use inherited value // 3. Else if fallback exists from text config → use fallback // 4. Else if property === "text-decoration" → default to "none" // 5. Else → leave undefined (property not added) ``` **This means:** - Your manual `color` and `text-decoration` are **sacred** — never touched - Only links missing these properties receive automatic values - Inheritance from parent `` elements is respected - The text config's `color` and `textDecoration` act as fallbacks, not overrides --- SECTION: TABLE --- # Table Pattern — LLM Reference This document teaches how to build table-like layouts using `Row` and `Column` components. Since no dedicated `Table` component exists, you compose tables by nesting `Row` (horizontal arrangement) inside `Column` (vertical arrangement), all within a `Container`. --- ## Core Concept — Correct Nesting ``` ContainerComponent ← email width container (fixed width only) └── ColumnComponent ← vertical stacking container (required) ├── RowComponent ← table row (horizontal) │ ├── ColumnComponent ← table cell │ └── ColumnComponent ← table cell ├── RowComponent ← table row (horizontal) │ ├── ColumnComponent ← table cell │ └── ColumnComponent ← table cell └── RowComponent ← table row (horizontal) └── ... ``` **Critical:** - `Row` must always be a child of `Column`, never directly inside `Container` - `Container` uses `widthType: "fixed"` only (no `"full"` value) — tables require a fixed pixel width | Component | Role | |---|---| | `ContainerComponent` | Fixed-width email container (e.g., `600px`) | | `ColumnComponent` (parent) | Vertical stacking container — holds all table rows | | `RowComponent` | Represents a **table row** — arranges cells horizontally | | `ColumnComponent` (child of Row) | Represents a **table cell** — contains content | --- ## Row Height Consistency To ensure all rows in a table have the same height, apply the `height` property on `Row` components whenever necessary. This prevents uneven row heights caused by varying content lengths across cells. ```json { "id": "consistent-height-row", "type": "RowComponent", "config": { "layoutColumns": ["33%", "34%", "33%"], "gap": "0px", "fillWidth": true, "height": "60px" }, "children": [...] } ``` **When to use `height`:** - Cells contain content of different lengths (e.g., short text vs. multi-line text) - Images or media of varying sizes appear in the same row - Ensuring visual alignment across complex table layouts **Best practice:** Set the same `height` value on all `Row` components within a table to guarantee uniform row dimensions. --- ## Border Application on Entire Row When there is a high possibility that row heights are not even, borders must be applied on the **entire row** (using `Row` configuration) rather than on individual cells. This prevents broken or misaligned border lines that occur when cells have different heights. ### Incorrect — Border on Cells (Height Mismatch Risk) ```json { "id": "bad-row", "type": "RowComponent", "config": { "layoutColumns": ["50%", "50%"], "gap": "0px", "fillWidth": true }, "children": [ { "id": "cell-1", "type": "ColumnComponent", "config": { "padding": "12px", "border": { "bottom": { "width": "1px", "style": "solid", "color": "#cbd5e1" } } }, "children": [...] }, { "id": "cell-2", "type": "ColumnComponent", "config": { "padding": "12px", "border": { "bottom": { "width": "1px", "style": "solid", "color": "#cbd5e1" } } }, "children": [...] } ] } ``` ### Correct — Border on Entire Row ```json { "id": "good-row", "type": "RowComponent", "config": { "layoutColumns": ["50%", "50%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#cbd5e1" } } }, "children": [ { "id": "cell-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "id": "cell-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] } ] } ``` **Why row-level borders are safer:** - The border spans the full width of the row regardless of individual cell heights - No gaps or breaks appear when cell content heights differ - Maintains visual integrity across all email clients **When to use row borders vs. cell borders:** | Scenario | Recommendation | |---|---| | Uneven row heights possible | Apply border on `Row` component | | All cells have identical content height | Cell borders acceptable | | Table has complex responsive behavior | Row borders preferred | | Borders required on all four sides of table | Use row borders for horizontal lines, container border for outer frame | --- ## Basic Table Structure ```json { "id": "table-container", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "600px", "childrenConstraints": { "widthDistributionType": "equals" } }, "children": [ { "id": "table-column-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "header-row", "type": "RowComponent", "config": { "layoutColumns": "equal", "gap": "0px", "fillWidth": true, "backgroundColor": "#1e293b", "height": "50px" }, "children": [ { "id": "header-cell-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [ { "id": "header-text-1", "type": "TextComponent", "config": { "text": "Column A", "color": "#ffffff", "fontWeight": "bold" } } ] }, { "id": "header-cell-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [ { "id": "header-text-2", "type": "TextComponent", "config": { "text": "Column B", "color": "#ffffff", "fontWeight": "bold" } } ] } ] }, { "id": "data-row-1", "type": "RowComponent", "config": { "layoutColumns": "equal", "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e2e8f0" } } }, "children": [ { "id": "cell-1-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [ { "id": "text-1-1", "type": "TextComponent", "config": { "text": "Row 1, Cell A" } } ] }, { "id": "cell-1-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [ { "id": "text-1-2", "type": "TextComponent", "config": { "text": "Row 1, Cell B — additional content that might wrap to multiple lines" } } ] } ] }, { "id": "data-row-2", "type": "RowComponent", "config": { "layoutColumns": "equal", "gap": "0px", "fillWidth": true }, "children": [ { "id": "cell-2-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [ { "id": "text-2-1", "type": "TextComponent", "config": { "text": "Row 2, Cell A" } } ] }, { "id": "cell-2-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [ { "id": "text-2-2", "type": "TextComponent", "config": { "text": "Row 2, Cell B" } } ] } ] } ] } ] } ``` --- ## Controlling Column Widths with `layoutColumns` `layoutColumns` on `Row` determines how child `Column` components share space. **Use the same `layoutColumns` value for every row** to maintain column alignment. ### Equal Columns (2, 3, or more) ```json { "id": "row-3-columns", "type": "RowComponent", "config": { "layoutColumns": "equal", "gap": "0px", "fillWidth": true, "height": "60px" }, "children": [ { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] } ] } ``` Each cell gets `33.33%` width. ### Proportional Columns (Manual Percentages) ```json { "id": "row-asymmetric", "type": "RowComponent", "config": { "layoutColumns": ["25%", "50%", "25%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e2e8f0" } } }, "children": [ { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] } ] } ``` ### Fixed Width Columns ```json { "id": "row-fixed", "type": "RowComponent", "config": { "layoutColumns": ["100px", "auto", "150px"], "gap": "0px", "fillWidth": true, "height": "50px" }, "children": [ { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] } ] } ``` - First cell: fixed `100px` - Second cell: fills remaining space (`auto`) - Third cell: fixed `150px` --- ## Complete Table Example — 3×3 with Row Borders ```json { "id": "complete-table-container", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "600px", "childrenConstraints": { "widthDistributionType": "equals" } }, "children": [ { "id": "table-vertical-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "header-row", "type": "RowComponent", "config": { "layoutColumns": ["30%", "40%", "30%"], "gap": "0px", "fillWidth": true, "backgroundColor": "#1e293b", "height": "55px" }, "children": [ { "id": "header-cell-1", "type": "ColumnComponent", "config": { "padding": "16px" }, "children": [{ "id": "header-text-1", "type": "TextComponent", "config": { "text": "Product", "color": "#ffffff", "fontWeight": "bold" } }] }, { "id": "header-cell-2", "type": "ColumnComponent", "config": { "padding": "16px" }, "children": [{ "id": "header-text-2", "type": "TextComponent", "config": { "text": "Description", "color": "#ffffff", "fontWeight": "bold" } }] }, { "id": "header-cell-3", "type": "ColumnComponent", "config": { "padding": "16px" }, "children": [{ "id": "header-text-3", "type": "TextComponent", "config": { "text": "Price", "color": "#ffffff", "fontWeight": "bold", "textAlign": "right" } }] } ] }, { "id": "row-1", "type": "RowComponent", "config": { "layoutColumns": ["30%", "40%", "30%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e2e8f0" } } }, "children": [ { "id": "cell-1-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-1-1", "type": "TextComponent", "config": { "text": "Basic Tee" } }] }, { "id": "cell-1-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-1-2", "type": "TextComponent", "config": { "text": "Cotton t-shirt, regular fit" } }] }, { "id": "cell-1-3", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-1-3", "type": "TextComponent", "config": { "text": "$29.00", "textAlign": "right" } }] } ] }, { "id": "row-2", "type": "RowComponent", "config": { "layoutColumns": ["30%", "40%", "30%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e2e8f0" } } }, "children": [ { "id": "cell-2-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-2-1", "type": "TextComponent", "config": { "text": "Premium Hoodie" } }] }, { "id": "cell-2-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-2-2", "type": "TextComponent", "config": { "text": "Fleece-lined, zip-up hoodie with extra warm lining for cold weather" } }] }, { "id": "cell-2-3", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-2-3", "type": "TextComponent", "config": { "text": "$89.00", "textAlign": "right" } }] } ] }, { "id": "footer-row", "type": "RowComponent", "config": { "layoutColumns": ["30%", "40%", "30%"], "gap": "0px", "fillWidth": true, "backgroundColor": "#f8fafc", "height": "50px" }, "children": [ { "id": "footer-cell-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "footer-text-1", "type": "TextComponent", "config": { "text": "Total" } }] }, { "id": "footer-cell-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "footer-text-2", "type": "TextComponent", "config": { "text": "2 items" } }] }, { "id": "footer-cell-3", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "footer-text-3", "type": "TextComponent", "config": { "text": "$118.00", "textAlign": "right", "fontWeight": "bold" } }] } ] } ] } ] } ``` --- ## Full Grid Borders (All Sides) — Row-Level Approach When borders are needed on all sides of a table with potential height mismatches, apply horizontal borders on `Row` components and use a container border for the outer frame. ```json { "id": "bordered-table-container", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "600px", "childrenConstraints": { "widthDistributionType": "equals" }, "border": { "all": { "width": "1px", "style": "solid", "color": "#cbd5e1" } } }, "children": [ { "id": "bordered-table-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "row-1", "type": "RowComponent", "config": { "layoutColumns": ["33%", "34%", "33%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#cbd5e1" } } }, "children": [ { "id": "cell-1", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-1", "type": "TextComponent", "config": { "text": "Cell 1" } }] }, { "id": "cell-2", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-2", "type": "TextComponent", "config": { "text": "Cell 2 with longer content that might wrap" } }] }, { "id": "cell-3", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-3", "type": "TextComponent", "config": { "text": "Cell 3" } }] } ] }, { "id": "row-2", "type": "RowComponent", "config": { "layoutColumns": ["33%", "34%", "33%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#cbd5e1" } } }, "children": [ { "id": "cell-4", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-4", "type": "TextComponent", "config": { "text": "Cell 4" } }] }, { "id": "cell-5", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-5", "type": "TextComponent", "config": { "text": "Cell 5" } }] }, { "id": "cell-6", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-6", "type": "TextComponent", "config": { "text": "Cell 6 — even more text that causes this cell to be taller than others in the same row" } }] } ] } ] } ] } ``` **Note:** The container border wraps the entire table, while row borders provide horizontal lines that span the full width regardless of individual cell heights. --- ## Simulating Rowspan Since `Row` does not support `rowspan` natively, nest multiple `Row` components inside a `Column` on one side. ```json { "id": "rowspan-container", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "600px", "childrenConstraints": { "widthDistributionType": "equals" } }, "children": [ { "id": "rowspan-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "rowspan-row", "type": "RowComponent", "config": { "layoutColumns": ["30%", "70%"], "gap": "0px", "fillWidth": true }, "children": [ { "id": "left-span-cell", "type": "ColumnComponent", "config": { "padding": "16px", "backgroundColor": "#fef3c7" }, "children": [ { "id": "span-content", "type": "TextComponent", "config": { "text": "This spans both rows vertically" } } ] }, { "id": "right-group-cell", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "sub-container", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "100%", "childrenConstraints": { "widthDistributionType": "equals" } }, "children": [ { "id": "sub-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "sub-row-1", "type": "RowComponent", "config": { "layoutColumns": ["100%"], "gap": "0px", "fillWidth": true }, "children": [ { "id": "top-right-cell", "type": "ColumnComponent", "config": { "padding": "12px", "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e5e7eb" } } }, "children": [{ "id": "text-right-1", "type": "TextComponent", "config": { "text": "Top right row" } }] } ] }, { "id": "sub-row-2", "type": "RowComponent", "config": { "layoutColumns": ["100%"], "gap": "0px", "fillWidth": true }, "children": [ { "id": "bottom-right-cell", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-right-2", "type": "TextComponent", "config": { "text": "Bottom right row" } }] } ] } ] } ] } ] } ] } ] } ] } ``` --- ## Shopping Cart Table Example ```json { "id": "cart-container", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "600px", "childrenConstraints": { "widthDistributionType": "equals" } }, "children": [ { "id": "cart-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "cart-header", "type": "RowComponent", "config": { "layoutColumns": ["40%", "15%", "20%", "25%"], "gap": "0px", "fillWidth": true, "backgroundColor": "#111827", "height": "50px" }, "children": [ { "id": "header-product", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-product", "type": "TextComponent", "config": { "text": "Product", "color": "#ffffff", "fontWeight": "bold" } }] }, { "id": "header-qty", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-qty", "type": "TextComponent", "config": { "text": "Qty", "color": "#ffffff", "fontWeight": "bold", "textAlign": "center" } }] }, { "id": "header-price", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-price", "type": "TextComponent", "config": { "text": "Price", "color": "#ffffff", "fontWeight": "bold", "textAlign": "right" } }] }, { "id": "header-total", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "text-total", "type": "TextComponent", "config": { "text": "Total", "color": "#ffffff", "fontWeight": "bold", "textAlign": "right" } }] } ] }, { "id": "cart-item-1", "type": "RowComponent", "config": { "layoutColumns": ["40%", "15%", "20%", "25%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e5e7eb" } } }, "children": [ { "id": "item1-product", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item1-name", "type": "TextComponent", "config": { "text": "Wireless Mouse" } }] }, { "id": "item1-qty", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item1-qty-num", "type": "TextComponent", "config": { "text": "2", "textAlign": "center" } }] }, { "id": "item1-price", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item1-price-val", "type": "TextComponent", "config": { "text": "$25.00", "textAlign": "right" } }] }, { "id": "item1-total", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item1-total-val", "type": "TextComponent", "config": { "text": "$50.00", "textAlign": "right", "fontWeight": "bold" } }] } ] }, { "id": "cart-item-2", "type": "RowComponent", "config": { "layoutColumns": ["40%", "15%", "20%", "25%"], "gap": "0px", "fillWidth": true, "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e5e7eb" } } }, "children": [ { "id": "item2-product", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item2-name", "type": "TextComponent", "config": { "text": "Mechanical Keyboard with RGB lighting and extra keycaps" } }] }, { "id": "item2-qty", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item2-qty-num", "type": "TextComponent", "config": { "text": "1", "textAlign": "center" } }] }, { "id": "item2-price", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item2-price-val", "type": "TextComponent", "config": { "text": "$129.00", "textAlign": "right" } }] }, { "id": "item2-total", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "item2-total-val", "type": "TextComponent", "config": { "text": "$129.00", "textAlign": "right", "fontWeight": "bold" } }] } ] }, { "id": "cart-footer", "type": "RowComponent", "config": { "layoutColumns": ["40%", "15%", "20%", "25%"], "gap": "0px", "fillWidth": true, "backgroundColor": "#f8fafc", "height": "50px" }, "children": [ { "id": "footer-empty", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [] }, { "id": "footer-label", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "footer-label-text", "type": "TextComponent", "config": { "text": "Subtotal:", "fontWeight": "bold", "textAlign": "right" } }] }, { "id": "footer-price", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [] }, { "id": "footer-total", "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [{ "id": "footer-total-text", "type": "TextComponent", "config": { "text": "$179.00", "textAlign": "right", "fontWeight": "bold" } }] } ] } ] } ] } ``` --- ## Quick Reference: Table Template ```json { "id": "table-template", "type": "ContainerComponent", "config": { "widthType": "fixed", "width": "600px", "childrenConstraints": { "widthDistributionType": "equals" } }, "children": [ { "id": "table-rows-wrapper", "type": "ColumnComponent", "config": { "padding": "0px" }, "children": [ { "id": "row-1", "type": "RowComponent", "config": { "layoutColumns": ["25%", "50%", "25%"], "gap": "0px", "fillWidth": true, "height": "60px", "border": { "bottom": { "width": "1px", "style": "solid", "color": "#e2e8f0" } } }, "children": [ { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] } ] }, { "id": "row-2", "type": "RowComponent", "config": { "layoutColumns": ["25%", "50%", "25%"], "gap": "0px", "fillWidth": true, "height": "60px" }, "children": [ { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] }, { "type": "ColumnComponent", "config": { "padding": "12px" }, "children": [...] } ] } ] } ] } ``` --- ## Key Rules for Table Construction | Rule | Explanation | |---|---| | **Container → Column → Row** | Never put `Row` directly inside `Container`. Always wrap with a `Column`. | | **Container uses `widthType: "fixed"` only** | Tables require a fixed pixel width (e.g., `600px`). No `"full"` value. | | **Every table row is a `Row`** | `Row` provides horizontal arrangement of cells. | | **Every cell is a `Column`** | `Column` stacks content vertically within a cell. | | **Use `layoutColumns` on every row** | Ensures column widths stay consistent across all rows. | | **Same `layoutColumns` array for all rows** | Copy the exact same `layoutColumns` value across every row in the table. | | **Set `gap: "0px"` for tables** | Table cells should have no gap; use `Column.padding` for internal spacing. | | **Set `fillWidth: true` on table rows** | Ensures the row fills the container width for proper column distribution. | | **`Container.width` sets email width** | Use `width` on `Container` (with `widthType: "fixed"`) to control the overall table width. | | **Use `height` on rows for consistency** | Apply `height` property on `Row` components when uneven row heights are possible. | | **Apply borders on rows when heights may vary** | When there is a high possibility of uneven row heights, use `Row` borders instead of cell borders. | --- SECTION: SPACER --- # Spacer Component — LLM Reference `Spacer` is a **vertical whitespace primitive**. It renders a fixed-height transparent block that creates consistent vertical spacing between components in an email. It has no visual output beyond the space it occupies — no background, no border, no text. It is the correct tool for adding vertical rhythm between layout elements. --- ## Position in the Document Tree ``` DocumentTree └── Section └── Container └── Column └── Heading (Text) └── Spacer ← vertical gap between elements └── Text └── Spacer ← vertical gap before button └── Button ``` `Spacer` is a leaf node. It can be placed anywhere a child component is accepted: inside `Column`, `Row`, directly in a `Container` cell, or between any sibling components. --- ## ComponentNode JSON Structure ```json { "id": "unique-string-id", "type": "SpacerComponent", "config": { "height": "24px", "hideOnMobile": false } } ``` --- ## `SpacerConfig` — Full Property Reference | Property | Type | Required | Default | Description | |---|---|---|---|---| | `height` | `string` | ✓ Yes | — | Height of the vertical space. Must be a px value (e.g. `"16px"`, `"48px"`). | | `hideOnMobile` | `boolean` | No | `false` | When `true`, adds the `hide-on-mobile` CSS class to the outer table. The builder's global stylesheet collapses the spacer on mobile viewports. | --- ## How It Works `Spacer` creates vertical space using three reinforcing techniques for cross-client reliability: 1. **CSS `height`** on the `` — respected by modern email clients 2. **HTML `height` attribute** on both the `