---
name: dragble-config
description: Configure the Dragble editor appearance, features, merge tags, special links, fonts, localization, display conditions, collaboration, branding colors, and editor behavior.
license: MIT
metadata:
  author: dragble
  version: "1.0"
---

# Dragble Editor Configuration

## Overview

All editor configuration is passed through the `options` object in `dragble.init()`. The top-level `DragbleConfig` controls boot-time settings (container, auth, mode), while `options` holds all runtime-configurable settings (appearance, tools, features, localization, merge tags, etc.).

```js
dragble.init({
  containerId: 'editor-container',
  editorKey: 'ek_xxx',
  editorMode: 'email',
  options: {
    appearance: { theme: 'light', accentColor: 'indigo' },
    features: { preview: true, undoRedo: true },
    tools: { video: { enabled: false } },
    locale: 'en-US',
    // ... all other config
  },
});
```

---

## DragbleConfig (Top Level)

| Property | Type | Required | Description |
|---|---|---|---|
| `containerId` | `string` | **Yes** | DOM element ID where the editor iframe is mounted |
| `editorKey` | `string` | **Yes** | Project editor key for authentication (`ek_...`) |
| `editorMode` | `'email' \| 'web' \| 'popup'` | No | Builder type. Default: `'email'` |
| `designMode` | `'edit' \| 'live'` | No | `'edit'` = admin mode (shows Row Actions), `'live'` = end-user mode (enforces row permissions). Default: `'live'` |
| `design` | `DesignJson \| null` | No | Initial design JSON. `null` = blank design, `undefined` = no design loaded |
| `popup` | `PopupConfig` | No | Popup builder config (only relevant when `editorMode` is `'popup'`) |
| `callbacks` | `DragbleCallbacks` | No | Event callbacks (`onReady`, `onChange`, `onLoad`, `onError`, `onModuleSave`, `onPreview`, `onContentDialog`, etc.) |
| `options` | `EditorOptions` | No | All editor configuration (appearance, tools, features, locale, merge tags, etc.) |
| `editorUrl` | `string` | No | Custom editor source URL for enterprise self-hosted editors |
| `editorVersion` | `'stable' \| 'latest' \| string` | No | Editor version to load. Default: `'latest'`. Ignored when `editorUrl` is set |
| `environment` | `'production' \| 'development' \| 'staging'` | No | Environment mode |

```js
dragble.init({
  containerId: 'editor',
  editorKey: 'ek_live_abc123',
  editorMode: 'email',
  designMode: 'live',
  design: savedDesignJson,
  callbacks: {
    onReady: () => console.log('Editor ready'),
    onChange: (data) => console.log('Design changed', data),
  },
  options: { /* ... */ },
});
```

---

## Appearance

### Theme

The editor supports 4 themes. **Branded themes ignore `accentColor`.**

| Theme | `accentColor` | Description |
|---|---|---|
| `'light'` | Customizable | Generic light theme, accent tints the UI |
| `'dark'` | Customizable | Generic dark theme, accent tints the UI |
| `'dragble-light'` | **IGNORED** (fixed indigo) | Dragble branded light theme |
| `'dragble-dark'` | **IGNORED** (fixed white-on-black) | Dragble branded dark theme |

**CORRECT:**

```js
options: {
  appearance: {
    theme: 'light',
    accentColor: 'teal', // Works — generic theme respects accentColor
  }
}
```

**WRONG:**

```js
options: {
  appearance: {
    theme: 'dragble-light',
    accentColor: 'teal', // IGNORED — branded themes use fixed colors
  }
}
```

### Accent Colors

24 Radix UI color scale names:

`gray`, `gold`, `bronze`, `brown`, `yellow`, `amber`, `orange`, `tomato`, `red`, `ruby`, `crimson`, `pink`, `plum`, `purple`, `violet`, `iris`, `indigo`, `blue`, `cyan`, `teal`, `jade`, `green`, `grass`, `mint`, `sky`

### Side Panel

```js
options: {
  appearance: {
    sidePanel: {
      tabs: {
        content: { visible: true },   // CONTENT tab (tools + saved modules)
        modules: { visible: true },    // MODULES tab (rows + custom tabs)
        styles: { visible: true },     // STYLES tab (body settings)
      },
      modulesTab: {
        rows: { visible: true, defaultExpanded: true },
        savedModules: { visible: true, defaultExpanded: false, title: 'My Modules' },
        customTabs: [
          { id: 'snippets', label: 'Snippets', content: '...', icon: 'star', order: 1, visible: true }
        ],
      },
      stylesTab: {
        general: { visible: true, defaultExpanded: true },
        fonts: { visible: true, defaultExpanded: false },
        contentAlignment: { visible: true },
        backgroundImage: { visible: true },
        linkStyles: { visible: true },
        preheader: { visible: true },
      },
      dock: 'right',           // 'left' | 'right' (default: 'right')
      width: 380,              // Panel width in pixels (default: 380)
      collapsible: false,      // Whether panel can be collapsed (default: false)
      accordionsCollapsed: false, // Start all accordions collapsed (default: false)
    },
  }
}
```

### Action Bar

```js
options: {
  appearance: {
    actionBar: {
      placement: 'top_left',  // 'top_left' | 'top_right' | 'bottom_left' | 'bottom_right'
      compact: false,          // Smaller buttons when true
    },
  }
}
```

### Shortcut Bar

```js
options: {
  appearance: {
    shortcutBar: {
      placement: 'top_left',  // 'top_left' | 'top_right' | 'bottom_left' | 'bottom_right'
    },
  }
}
```

### Custom Loader

Priority: `svg` > `url` > `html` + `css` > default spinner.

```js
options: {
  appearance: {
    loader: {
      svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="25" r="20" fill="none" stroke="#6366f1" stroke-width="4"><animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite"/></circle></svg>',
      // OR
      url: 'https://example.com/loader.gif',
      // OR
      html: '<div class="my-loader">Loading...</div>',
      css: '.my-loader { color: #6366f1; font-size: 18px; }',
    },
  }
}
```

### Branding Colors

Set brand colors for all color pickers in the editor at runtime:

```js
// Simple flat array
dragble.setBrandingColors({
  colors: ['#FF0000', '#00FF00', '#0000FF', '#FFAA00'],
  defaultColors: true, // show default palette below brand colors (default: true)
});

// Named palettes
dragble.setBrandingColors({
  colors: [
    { id: 'primary', label: 'Primary', colors: ['#6366f1', '#4f46e5', '#4338ca'], default: true },
    { id: 'secondary', label: 'Secondary', colors: ['#f59e0b', '#d97706', '#b45309'] },
  ],
  defaultColors: false, // hide default palette
});
```

### Runtime

```js
dragble.setAppearance({ theme: 'dark', accentColor: 'violet' });
```

---

## Feature Flags

Feature flags live in `options.features`. All are optional.

| Feature | Type | Default | Description |
|---|---|---|---|
| `preview` | `boolean` | `true` | Show preview button |
| `undoRedo` | `boolean` | `true` | Show undo/redo buttons |
| `responsiveDesign` | `boolean` | `true` | Show responsive design view toggles (desktop/mobile) |
| `imageEditor` | `boolean` | `undefined` | Enable inline image editor. `false` = force disable even if plan allows |
| `audit` | `boolean` | `false` | Enable template audit trail |
| `stockImages` | `boolean \| StockImagesConfig` | `true` | Stock images in file manager. `false` = hide even if plan allows |
| `userUploads` | `boolean` | `true` | Enable user image uploads |
| `fileManager` | `boolean` | `true` | Show File Manager modal. `false` hides "Choose Image" button but keeps direct upload |
| `smartMergeTags` | `boolean` | `false` | Enable smart merge tag suggestions |
| `specialLinks` | `boolean` | `true` | Show special links in text toolbar |
| `languageSelector` | `boolean` | `true` | Show language dropdown in side panel. `false` = language controlled via SDK only |
| `preheaderText` | `boolean` | `true` | Show preheader text input in Styles panel (email mode only) |
| `amp` | `boolean` | `false` | Enable AMP for Email (email mode only). Unlocks interactive tools |
| `modules` | `boolean \| ModuleFeaturesConfig` | `false` | Enable modules (save row as module, synced modules). Not available in `contentType: 'module'` |
| `header` | `boolean` | `false` | Enable locked header row from SDK. Requires plan permission |
| `footer` | `boolean` | `false` | Enable locked footer row from SDK. Requires plan permission |
| `dynamicImage` | `boolean` | `undefined` | Enable dynamic image merge tag on image tool (Business+ plans) |
| `collaboration` | `boolean \| CollaborationFeaturesConfig` | `false` | Team collaboration (commenting, reviewer role). See [Collaboration](#collaboration) |
| `export` | `ExportFeaturesConfig` | all `true` | Granular export toggles: `{ html, json, image, pdf, zip }` |

```js
options: {
  features: {
    preview: true,
    undoRedo: true,
    responsiveDesign: true,
    imageEditor: true,
    audit: true,
    stockImages: { enabled: true, safeSearch: true, defaultSearchTerm: 'business' },
    userUploads: true,
    fileManager: true,
    smartMergeTags: false,
    specialLinks: true,
    languageSelector: true,
    preheaderText: true,
    amp: false,
    modules: { saveRowAsModule: true },
    header: false,
    footer: false,
    dynamicImage: true,
    collaboration: true,
    export: { html: true, json: true, image: true, pdf: true, zip: true },
  }
}
```

---

## Merge Tags

Merge tags are dynamic content placeholders (e.g., `{{first_name}}`) that users can insert into text.

### MergeTagsConfig

```ts
interface MergeTagsConfig {
  excludeDefaults?: boolean;     // Hide built-in tags, show only custom (default: false)
  sort?: boolean;                // Sort alphabetically
  customMergeTags?: (MergeTag | MergeTagGroup)[];
}

interface MergeTag {
  label: string;                 // Display label in dropdown
  value: string;                 // Value inserted (e.g., '{{first_name}}')
  category?: string;             // Optional grouping category
  sample?: string;               // Preview sample value
}

interface MergeTagGroup {
  name: string;                  // Group name
  mergeTags: (MergeTag | MergeTagGroup)[];  // Nested tags
}
```

### Flat Example

```js
options: {
  mergeTags: {
    customMergeTags: [
      { label: 'First Name', value: '{{first_name}}', sample: 'Jane' },
      { label: 'Last Name', value: '{{last_name}}', sample: 'Doe' },
      { label: 'Email', value: '{{email}}', sample: 'jane@example.com' },
    ],
  }
}
```

### Grouped Example

```js
options: {
  mergeTags: {
    excludeDefaults: true,
    customMergeTags: [
      {
        name: 'Contact Info',
        mergeTags: [
          { label: 'First Name', value: '{{contact.first_name}}', sample: 'Jane' },
          { label: 'Last Name', value: '{{contact.last_name}}', sample: 'Doe' },
        ],
      },
      {
        name: 'Company',
        mergeTags: [
          { label: 'Company Name', value: '{{company.name}}', sample: 'Acme Inc' },
          { label: 'Website', value: '{{company.website}}', sample: 'https://acme.com' },
        ],
      },
    ],
  }
}
```

### Runtime

```js
dragble.setMergeTags({
  customMergeTags: [
    { label: 'Coupon Code', value: '{{coupon_code}}', sample: 'SAVE20' },
  ],
});

const tags = await dragble.getMergeTags();
```

---

## Special Links

Special links add custom link categories to the link picker in the text editor toolbar.

### SpecialLinksConfig

```ts
interface SpecialLinksConfig {
  excludeDefaults?: boolean;
  customSpecialLinks?: (SpecialLink | SpecialLinkGroup)[];
}

interface SpecialLink {
  name: string;                  // Display name
  href: string;                  // Link href value
  target?: '_blank' | '_self' | '_parent' | '_top';
}

interface SpecialLinkGroup {
  name: string;                  // Group name
  specialLinks: SpecialLink[];
}
```

### Flat Example

```js
options: {
  specialLinks: {
    customSpecialLinks: [
      { name: 'Unsubscribe', href: '{{unsubscribe_url}}', target: '_blank' },
      { name: 'View in Browser', href: '{{webview_url}}', target: '_blank' },
      { name: 'Manage Preferences', href: '{{preferences_url}}', target: '_blank' },
    ],
  }
}
```

### Grouped Example

```js
options: {
  specialLinks: {
    excludeDefaults: true,
    customSpecialLinks: [
      {
        name: 'Email Actions',
        specialLinks: [
          { name: 'Unsubscribe', href: '{{unsubscribe_url}}' },
          { name: 'View in Browser', href: '{{webview_url}}' },
        ],
      },
      {
        name: 'Social',
        specialLinks: [
          { name: 'Share on Twitter', href: '{{share_twitter_url}}' },
          { name: 'Share on Facebook', href: '{{share_facebook_url}}' },
        ],
      },
    ],
  }
}
```

### Runtime

```js
dragble.setSpecialLinks({
  customSpecialLinks: [
    { name: 'Survey', href: 'https://example.com/survey' },
  ],
});

const links = await dragble.getSpecialLinks();
```

---

## Fonts

### FontsConfig

```ts
interface FontsConfig {
  excludeDefaults?: boolean;   // Hide built-in fonts, show only custom (default: false)
  customFonts?: FontDefinition[];
}

interface FontDefinition {
  label: string;               // Display label in dropdown
  value: string;               // CSS font-family value
  url?: string;                // URL to load font (Google Fonts, etc.)
  weights?: number[];          // Available font weights (e.g., [400, 700])
  defaultFont?: boolean;       // Whether this is a default system font
}
```

### Example

```js
options: {
  fonts: {
    excludeDefaults: false,
    customFonts: [
      {
        label: 'Inter',
        value: "'Inter', sans-serif",
        url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
        weights: [400, 500, 600, 700],
      },
      {
        label: 'Playfair Display',
        value: "'Playfair Display', serif",
        url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap',
        weights: [400, 700],
      },
    ],
  }
}
```

### Runtime

```js
dragble.setFonts({
  customFonts: [
    { label: 'Roboto', value: "'Roboto', sans-serif", url: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap' },
  ],
});

const fonts = await dragble.getFonts();
```

---

## Display Conditions

Display conditions enable conditional rendering of rows based on user data or context. Conditions wrap row HTML with before/after syntax (e.g., Liquid, Jinja, Handlebars) that the sending platform evaluates at render time.

### DisplayConditionsConfig

```ts
interface DisplayConditionsConfig {
  enabled?: boolean;                          // Enable feature (default: false)
  conditions?: DisplayConditionDefinition[];  // Pre-defined conditions for picker
  permissions?: DisplayConditionsPermissions;
}

interface DisplayConditionDefinition {
  type: string;        // Category (e.g., 'Customer Segment')
  label: string;       // Display label in picker
  description?: string;
  before: string;      // Syntax before the row (e.g., '{% if customer.vip %}')
  after: string;       // Syntax after the row (e.g., '{% endif %}')
}

interface DisplayConditionsPermissions {
  canSelect?: boolean;  // Select from pre-defined conditions (default: true)
  canAdd?: boolean;     // Add custom conditions with own syntax (default: true)
  canEdit?: boolean;    // Edit existing conditions (default: true)
  canRemove?: boolean;  // Remove conditions from rows (default: true)
}
```

### Template Engine Syntax

The `before` and `after` fields wrap the row's HTML at export time:

```html
<!-- before -->
{% if customer.tier == "VIP" %}
  <tr><!-- row HTML --></tr>
{% endif %}
<!-- after -->
```

Any template engine syntax works (Liquid, Jinja2, Handlebars, Mustache, etc.) since the editor passes these strings through verbatim.

### Example

```js
options: {
  displayConditions: {
    enabled: true,
    conditions: [
      {
        type: 'Customer Segment',
        label: 'VIP Members',
        description: 'Only show to VIP tier customers',
        before: '{% if customer.tier == "VIP" %}',
        after: '{% endif %}',
      },
      {
        type: 'Cart Status',
        label: 'Has Items in Cart',
        before: '{% if cart.items_count > 0 %}',
        after: '{% endif %}',
      },
      {
        type: 'Location',
        label: 'US Customers Only',
        before: '{{#if (eq country "US")}}',
        after: '{{/if}}',
      },
    ],
    permissions: {
      canSelect: true,
      canAdd: false,   // Users can only pick from pre-defined, not create custom
      canEdit: false,
      canRemove: true,
    },
  }
}
```

### Runtime

```js
dragble.setDisplayConditions({
  enabled: true,
  conditions: [
    { type: 'Segment', label: 'New Users', before: '{% if user.new %}', after: '{% endif %}' },
  ],
  permissions: { canSelect: true, canAdd: true, canRemove: true },
});

// Disable display conditions
dragble.setDisplayConditions({ enabled: false });
```

---

## Localization

### Supported Locales

The editor ships with full translations for 29 locales (BCP 47 format):

| Code | Language |
|---|---|
| `en-US` | English (US) |
| `en-CA` | English (Canada) |
| `ar-AE` | Arabic (UAE) — RTL |
| `zh-CN` | Chinese (Simplified) |
| `zh-TW` | Chinese (Traditional) |
| `cs-CZ` | Czech |
| `da-DK` | Danish |
| `nl-NL` | Dutch |
| `et-EE` | Estonian |
| `fa-IR` | Farsi / Persian — RTL |
| `fi-FI` | Finnish |
| `fr-FR` | French (France) |
| `fr-CA` | French (Canada) |
| `de-DE` | German |
| `hu-HU` | Hungarian |
| `id-ID` | Indonesian |
| `it-IT` | Italian |
| `ja-JP` | Japanese |
| `ko-KR` | Korean |
| `no-NO` | Norwegian |
| `pl-PL` | Polish |
| `pt-BR` | Portuguese (Brazil) |
| `pt-PT` | Portuguese (Portugal) |
| `ru-RU` | Russian |
| `es-ES` | Spanish |
| `sv-SE` | Swedish |
| `tr-TR` | Turkish |
| `uk-UA` | Ukrainian |
| `vi-VN` | Vietnamese |

Base codes (e.g., `'en'`, `'fr'`, `'de'`) are also accepted and auto-mapped to their default regional variant.

### Configuration

```js
options: {
  locale: 'fr-FR',

  // Override or extend built-in translations
  translations: {
    'fr-FR': {
      'content_tools.paragraph': 'Mon Paragraphe',
      'buttons.save': 'Sauvegarder',
    },
    // Add a completely new language
    'th': {
      'content_tools.paragraph': 'ย่อหน้า',
      'buttons.save': 'บันทึก',
      // Missing keys fall back to English
    },
  },

  textDirection: 'ltr',  // 'ltr' | 'rtl'. Auto-set when language has rtl flag.

  // Template language (distinct from UI locale)
  language: {
    label: 'العربية',
    value: 'ar-SA',
    rtl: true,       // Auto-detects RTL from language code (ar, he, fa, ur) when omitted
    default: false,
  },
}
```

Priority: `translations[locale]` > built-in locale > English US defaults.

### Runtime

```js
// Switch UI locale
dragble.setLocale('fr-FR');

// Switch with custom overrides
dragble.setLocale('fr-CA', { 'buttons.save': 'Sauvegarder' });

// Set template language (auto-switches text direction for RTL)
dragble.setLanguage({ label: 'العربية', value: 'ar-SA', rtl: true });

// Get current template language
const lang = await dragble.getLanguage();

// Manual text direction control
dragble.setTextDirection('rtl');
const dir = await dragble.getTextDirection();
```

---

## Collaboration

Team collaboration enables commenting, reviewer roles, and @mentions on the editor canvas.

### CollaborationFeaturesConfig

```ts
interface CollaborationFeaturesConfig {
  enabled?: boolean;                   // Enable collaboration (default: false)
  role?: 'editor' | 'reviewer';       // 'editor' = full edit + comment, 'reviewer' = read-only + comment
  commenting?: boolean | CollaborationCommentingConfig;
  onComment?: (action: CommentAction) => void;
}

interface CollaborationCommentingConfig {
  enabled?: boolean;                   // Enable commenting (default: true when collaboration enabled)
  mentions?: boolean;                  // Enable @mentions (default: true)
  getMentions?: (search: string) => Promise<MentionUser[]>;
}

interface MentionUser {
  userHandle: string;                  // e.g., '@jane'
  username: string;                    // 'Jane Smith'
  userColor?: string;                  // '#3498DB'
}
```

### CommentAction Types

| Type | Description |
|---|---|
| `NEW_COMMENT` | A new comment or reply was created |
| `COMMENT_EDITED` | An existing comment's content was modified |
| `COMMENT_DELETED` | A comment was removed |
| `COMMENT_THREAD_RESOLVED` | A root comment thread was marked as resolved |
| `COMMENT_THREAD_REOPENED` | A previously resolved thread was reopened |

### Requirements

Collaboration requires `user.id` to be set in options so comments can be attributed:

```js
options: {
  user: { id: 'user-123', name: 'Jane Doe', email: 'jane@example.com', color: '#3b82f6' },
}
```

### Simple Example

```js
options: {
  user: { id: 'user-123', name: 'Jane Doe' },
  features: {
    collaboration: true, // Enable commenting for current user as 'editor' role
  },
}
```

### Reviewer Role

```js
options: {
  user: { id: 'user-456', name: 'Bob Reviewer' },
  features: {
    collaboration: {
      enabled: true,
      role: 'reviewer', // Read-only editing + commenting
      commenting: true,
    },
  },
}
```

### Advanced with @Mentions and Notifications

```js
options: {
  user: { id: 'user-123', name: 'Jane Doe', color: '#E74C3C' },
  features: {
    collaboration: {
      enabled: true,
      role: 'editor',
      commenting: {
        enabled: true,
        mentions: true,
        getMentions: async (search) => {
          const res = await fetch(`/api/team?q=${encodeURIComponent(search)}`);
          return await res.json();
          // Returns: [{ userHandle: '@bob', username: 'Bob Smith', userColor: '#3498DB' }]
        },
      },
      onComment: (action) => {
        console.log(`${action.type}: ${action.comment.content}`);
        if (action.type === 'NEW_COMMENT' && action.mentions.length > 0) {
          notifyMentionedUsers(action.mentions);
        }
        // Sync all comments to backend
        saveCommentsToBackend(action.comments);
      },
    },
  },
}
```

---

## Editor Behavior

### EditorBehaviorConfig

```ts
interface EditorBehaviorConfig {
  minRows?: number;                   // Minimum rows in body (default: 0)
  maxRows?: number | null;            // Maximum rows (default: unlimited)
  contentType?: 'module';             // 'module' = single-row module editor (locked to 1 row)
  autoSelectOnDrop?: boolean;         // Auto-select element when dropped (default: false)
  confirmOnDelete?: boolean;          // Show confirm dialog before deleting (default: false)
  rows?: boolean;                     // Show Rows accordion in Modules tab (default: true)
  title?: string;                     // Editor title (shown in header)
}
```

### Example

```js
options: {
  editor: {
    minRows: 1,
    maxRows: 10,
    autoSelectOnDrop: true,
    confirmOnDelete: true,
    rows: true,
    title: 'Newsletter Editor',
  },
}
```

### Module Editor (Single-Row)

```js
options: {
  editor: {
    contentType: 'module', // Locks editor to 1 row for reusable content editing
  },
}
```

### Runtime

```js
dragble.setEditorConfig({
  maxRows: 5,
  autoSelectOnDrop: true,
  confirmOnDelete: false,
});

const config = await dragble.getEditorConfig();
```

---

## Body/Canvas Values

Control the email/page body defaults (background, content width, fonts, links, preheader).

### setBodyValues / getBodyValues

```ts
interface SetBodyValuesOptions {
  backgroundColor?: string;
  contentWidth?: string;             // e.g., '600px'
  contentAlign?: 'left' | 'center';
  backgroundImage?: Partial<BackgroundImage>;
  fontFamily?: FontFamily;           // { label: string, value: string, url?: string }
  textColor?: string;
  preheaderText?: string;            // Email only
  linkStyle?: Partial<LinkStyle>;
}
```

### Example

```js
// Set at init
options: {
  bodyValues: {
    backgroundColor: '#f5f5f5',
    contentWidth: '600px',
    contentAlign: 'center',
    textColor: '#333333',
    fontFamily: { label: 'Inter', value: "'Inter', sans-serif" },
    preheaderText: 'Check out our latest deals...',
    linkStyle: {
      linkColor: '#6366f1',
      linkHoverColor: '#4f46e5',
      linkUnderline: true,
      linkHoverUnderline: true,
    },
  },
}

// Set at runtime
dragble.setBodyValues({
  backgroundColor: '#ffffff',
  contentWidth: '700px',
});

// Get current values
const bodyValues = await dragble.getBodyValues();
```

---

## Tools Configuration

### Built-in Tools

14 built-in content tools:

| Tool Key | Description |
|---|---|
| `text` | Text / paragraph (alias: `paragraph`) |
| `heading` | Heading |
| `button` | Button / CTA |
| `image` | Image |
| `divider` | Horizontal divider |
| `menu` | Navigation menu |
| `html` | Raw HTML |
| `social` | Social media icons |
| `video` | Video embed |
| `table` | Table |
| `timer` | Countdown timer |
| `form` | Form (AMP only in email mode) |
| `spacer` | Vertical spacer |
| `paragraph` | Alias for `text` |

### ToolConfig

```ts
interface ToolConfig {
  enabled?: boolean;
  position?: number;                     // Sort order in tools panel
  properties?: Record<string, ToolPropertyConfig>;
}

interface ToolPropertyConfig {
  value?: unknown;                       // Default value override
  editable?: boolean;                    // Whether user can change this property
}
```

### Example

```js
options: {
  tools: {
    video: { enabled: false },
    form: { enabled: false },
    heading: {
      position: 1,
      properties: {
        text: { value: 'Default Heading Text' },
        fontSize: { value: '28px' },
        color: { value: '#111827' },
      },
    },
    button: {
      properties: {
        backgroundColor: { value: '#6366f1' },
        borderRadius: { value: '8px', editable: true },
      },
    },
    image: {
      properties: {
        alt: { value: '', editable: true },
      },
    },
  }
}
```

### Runtime

```js
dragble.setToolsConfig({
  video: { enabled: true },
  social: { enabled: false },
});
```

---

## Popup Config

Popup configuration is only relevant when `editorMode` is `'popup'`.

### PopupConfig

```ts
interface PopupConfig {
  defaultWidth?: string;       // Default popup width
  defaultHeight?: string;      // Default popup height
  exportMode?: 'full' | 'partial';  // 'full' = complete HTML document, 'partial' = embeddable fragment
  popupId?: string;            // Custom popup ID for HTML element IDs
}
```

### Example

```js
dragble.init({
  containerId: 'editor',
  editorKey: 'ek_xxx',
  editorMode: 'popup',
  popup: {
    defaultWidth: '500px',
    defaultHeight: 'auto',
    exportMode: 'full',
    popupId: 'my-popup',
  },
});
```

### getPopupValues

Retrieve the current popup settings from the design:

```js
const popupValues = await dragble.getPopupValues();
// Returns: {
//   campaignType: 'lightbox',
//   position: { horizontal: 'center', vertical: 'center' },
//   width: '500px',
//   height: 'auto',
//   overlay: { enabled: true, color: 'rgba(0,0,0,0.5)', closeOnClick: true },
//   closeButton: { enabled: true, position: 'top-right', size: '24px', color: '#fff', ... },
//   animation: 'fade',
//   animationDuration: '300ms',
//   borderRadius: '12px',
//   boxShadow: '0 25px 50px rgba(0,0,0,0.25)',
//   padding: '20px',
//   border: { width: '0', style: 'none', color: '#000' },
//   displayDelay: 0,
//   contentAlign: 'center',
//   contentVerticalAlign: 'middle',
// }
```

Note: `backgroundColor` and `backgroundImage` are NOT part of popup values. They are managed through `bodyValues`, same as email/web mode.

---

## Common Mistakes & Troubleshooting

### 1. Using `accentColor` with branded themes

```js
// WRONG: accentColor is ignored with dragble-light / dragble-dark
appearance: { theme: 'dragble-light', accentColor: 'teal' }

// CORRECT: Use generic themes for custom accent
appearance: { theme: 'light', accentColor: 'teal' }
```

### 2. Missing `user.id` for collaboration

```js
// WRONG: Collaboration won't attribute comments
features: { collaboration: true }

// CORRECT: Always set user.id
options: {
  user: { id: 'user-123', name: 'Jane' },
  features: { collaboration: true },
}
```

### 3. Calling methods before editor is ready

```js
// WRONG: Editor may not be ready yet
dragble.init({ ... });
dragble.setMergeTags({ ... }); // May throw

// CORRECT: Use onReady callback
dragble.init({
  containerId: 'editor',
  editorKey: 'ek_xxx',
  callbacks: {
    onReady: () => {
      dragble.setMergeTags({ customMergeTags: [...] });
    },
  },
});
```

### 4. Confusing `locale` (UI) with `language` (template content)

- `locale` controls the **editor interface** language (button labels, panel text, tooltips)
- `language` controls the **template content** direction and language metadata

```js
options: {
  locale: 'en-US',  // Editor UI in English
  language: { label: 'العربية', value: 'ar-SA', rtl: true }, // Template content in Arabic (RTL)
}
```

### 5. Using `panels` instead of `sidePanel`

`panels` is deprecated. Use `sidePanel` for all panel configuration:

```js
// WRONG (deprecated)
appearance: { panels: { tools: { dock: 'left' } } }

// CORRECT
appearance: { sidePanel: { dock: 'left', width: 380 } }
```

### 6. Expecting `backgroundColor` in popup values

Popup background color and image are managed through `bodyValues`, not `popupValues`:

```js
// Access popup background via design, not popup values
const design = await dragble.getDesign();
const bgColor = design.body.values.backgroundColor;
```

### 7. Feature flags vs plan permissions

Feature flags can only **disable** features, not grant them. If a feature requires a plan upgrade (e.g., image editor, dynamic image), setting `features.imageEditor: true` won't work unless the plan allows it. Setting `features.imageEditor: false` always disables it regardless of plan.
