Introduce mail branding settings

This commit is contained in:
Samuel Georges 2017-07-22 17:20:48 +10:00
parent 181be5979a
commit 31ffadbc4c
17 changed files with 1042 additions and 78 deletions

View File

@ -435,6 +435,15 @@ class ServiceProvider extends ModuleServiceProvider
'permissions' => ['system.manage_mail_settings'],
'order' => 620
],
'mail_brand_settings' => [
'label' => 'system::lang.mail_brand.menu_label',
'description' => 'system::lang.mail_brand.menu_description',
'category' => SettingsManager::CATEGORY_MAIL,
'icon' => 'icon-paint-brush',
'url' => Backend::url('system/mailbrandsettings'),
'permissions' => ['system.manage_mail_settings'],
'order' => 630
],
'event_logs' => [
'label' => 'system::lang.event_log.menu_label',
'description' => 'system::lang.event_log.menu_description',

View File

@ -0,0 +1,4 @@
.field-colorpicker {
float: right;
margin-top: -10px;
}

View File

@ -0,0 +1,40 @@
$(document).on('change', '.field-colorpicker', function() {
$('#brandSettingsForm').request('onRefresh', {
data: { 'fields': ['_mail_preview'] }
})
})
function createPreviewContainer(el, content) {
var newiframe
// Shadow DOM ignores media queries
// if (document.body.attachShadow) {
if (false) {
var shadow = el.attachShadow({ mode: 'open' })
shadow.innerHTML = content
}
else {
newiframe = document.createElement('iframe')
'srcdoc' in newiframe
? newiframe.srcdoc = content
: newiframe.src = 'data:text/html;charset=UTF-8,' + content
var parent = el.parentNode
parent.replaceChild(newiframe, el)
newiframe.style.width = '100%'
newiframe.setAttribute('frameborder', 0)
newiframe.onload = adjustIframeHeight
}
function adjustIframeHeight() {
newiframe.style.height = '500px'
newiframe.style.height = 1 + (newiframe.contentWindow.document.getElementsByTagName('body')[0].scrollHeight) +'px'
}
$(document).render(adjustIframeHeight)
$(window).resize(adjustIframeHeight)
}

View File

@ -4,6 +4,7 @@ use Twig;
use Markdown;
use System\Models\MailPartial;
use System\Models\MailTemplate;
use System\Models\MailBrandSetting;
use System\Helpers\View as ViewHelper;
use System\Classes\PluginManager;
use System\Classes\MarkupManager;
@ -48,7 +49,12 @@ class MailManager
/**
* @var bool Internal marker for rendering mode
*/
protected $isHtmlRenderMode;
protected $isHtmlRenderMode = false;
/**
* @var bool Internal marker for booting custom twig extensions.
*/
protected $isTwigStarted = false;
/**
* This function hijacks the `addContent` method of the `October\Rain\Mail\Mailer`
@ -66,12 +72,7 @@ class MailManager
/*
* Start twig transaction
*/
$markupManager = MarkupManager::instance();
$markupManager->beginTransaction();
$markupManager->registerTokenParsers([
new MailPartialTokenParser,
new MailComponentTokenParser
]);
$this->startTwig();
/*
* Inject global view variables
@ -84,55 +85,98 @@ class MailManager
/*
* Subject
*/
$customSubject = $message->getSwiftMessage()->getSubject();
if (empty($customSubject)) {
$swiftMessage = $message->getSwiftMessage();
if (empty($swiftMessage->getSubject())) {
$message->subject(Twig::parse($template->subject, $data));
}
if (!isset($data['subject'])) {
$data['subject'] = $swiftMessage->getSubject();
}
/*
* HTML contents
*/
$html = $this->renderHtmlContents($template, $data);
$html = $this->renderTemplate($template, $data);
$message->setBody($html, 'text/html');
/*
* Text contents
*/
$text = $this->renderTextContents($template, $data);
$text = $this->renderTextTemplate($template, $data);
$message->addPart($text, 'text/plain');
/*
* End twig transaction
*/
$markupManager->endTransaction();
$this->stopTwig();
}
//
// Rendering
//
public function renderHtmlContents($template, $data)
/**
* Render the Markdown template into HTML.
*
* @param string $content
* @param array $data
* @return string
*/
public function render($content, $data = [])
{
if (!$content) {
return '';
}
$html = $this->renderTwig($content, $data);
$html = Markdown::parse($html);
return $html;
}
public function renderTemplate($template, $data = [])
{
$this->isHtmlRenderMode = true;
$templateHtml = $template->content_html;
$html = Twig::parse($templateHtml, $data);
$html = Markdown::parse($html);
$html = $this->render($template->content_html, $data);
if ($template->layout) {
$html = Twig::parse($template->layout->content_html, [
$html = $this->renderTwig($template->layout->content_html, [
'content' => $html,
'css' => $template->layout->content_css
'css' => $template->layout->content_css,
'brandCss' => MailBrandSetting::compileCss()
] + (array) $data);
}
return $html;
}
public function renderTextContents($template, $data)
/**
* Render the Markdown template into text.
*
* @param string $view
* @param array $data
* @return string
*/
public function renderText($content, $data = [])
{
if (!$content) {
return '';
}
$text = $this->renderTwig($content, $data);
$text = html_entity_decode(preg_replace("/[\r\n]{2,}/", "\n\n", $text), ENT_QUOTES, 'UTF-8');
return $text;
}
public function renderTextTemplate($template, $data = [])
{
$this->isHtmlRenderMode = false;
@ -142,9 +186,10 @@ class MailManager
$templateText = $template->content_html;
}
$text = Twig::parse($templateText, $data);
$text = $this->renderText($templateText, $data);
if ($template->layout) {
$text = Twig::parse($template->layout->content_text, [
$text = $this->renderTwig($template->layout->content_text, [
'content' => $text
] + (array) $data);
}
@ -166,7 +211,7 @@ class MailManager
}
}
public function renderHtmlPartial($partial, $params)
protected function renderHtmlPartial($partial, $params)
{
$content = $partial->content_html;
@ -174,12 +219,12 @@ class MailManager
return '';
}
$params['body'] = Markdown::parse(array_get($params, 'body'));
$params['body'] = array_get($params, 'body');
return Twig::parse($content, $params);
return $this->renderTwig($content, $params);
}
public function renderTextPartial($partial, $params)
protected function renderTextPartial($partial, $params)
{
$content = $partial->content_text ?: $partial->content_html;
@ -187,7 +232,53 @@ class MailManager
return '';
}
return Twig::parse($content, $params);
return $this->renderTwig($content, $params);
}
/**
* Internal helper for rendering Twig
*/
protected function renderTwig($content, $data = [])
{
if ($this->isTwigStarted) {
return Twig::parse($content, $data);
}
$this->startTwig();
$result = Twig::parse($content, $data);
$this->stopTwig();
return $result;
}
protected function startTwig()
{
if ($this->isTwigStarted) {
return;
}
$this->isTwigStarted = true;
$markupManager = MarkupManager::instance();
$markupManager->beginTransaction();
$markupManager->registerTokenParsers([
new MailPartialTokenParser,
new MailComponentTokenParser
]);
}
protected function stopTwig()
{
if (!$this->isTwigStarted) {
return;
}
$markupManager = MarkupManager::instance();
$markupManager->endTransaction();
$this->isTwigStarted = false;
}
//

View File

@ -39,7 +39,7 @@ class MarkupManager
/**
* @var array Transaction based extension items
*/
protected $tranItems;
protected $transactionItems;
/**
* @var bool Manager is in transaction mode
@ -114,7 +114,7 @@ class MarkupManager
*/
public function registerExtensions($type, array $definitions)
{
$items = $this->transactionMode ? 'tranItems' : 'items';
$items = $this->transactionMode ? 'transactionItems' : 'items';
if (is_null($this->$items)) {
$this->$items = [];
@ -182,8 +182,8 @@ class MarkupManager
$results = $this->items[$type];
}
if ($this->tranItems !== null && isset($this->tranItems[$type])) {
$results = array_merge($results, $this->tranItems[$type]);
if ($this->transactionItems !== null && isset($this->transactionItems[$type])) {
$results = array_merge($results, $this->transactionItems[$type]);
}
return $results;
@ -381,6 +381,6 @@ class MarkupManager
{
$this->transactionMode = false;
$this->tranItems = null;
$this->transactionItems = null;
}
}

View File

@ -0,0 +1,79 @@
<?php namespace System\Controllers;
use File;
use Flash;
use Backend;
use BackendMenu;
use ApplicationException;
use System\Models\MailBrandSetting;
use System\Classes\SettingsManager;
use System\Classes\MailManager;
use Backend\Classes\Controller;
use System\Models\MailLayout;
use System\Models\MailTemplate;
use Exception;
/**
* Mail brand customization controller
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*
*/
class MailBrandSettings extends Controller
{
public $implement = [
\Backend\Behaviors\FormController::class
];
public $formConfig = 'config_form.yaml';
public $requiredPermissions = ['system.manage_mail_templates'];
public $bodyClass = 'compact-container';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->pageTitle = 'Customize mail appearance';
BackendMenu::setContext('October.System', 'system', 'settings');
SettingsManager::setContext('October.System', 'mail_brand_settings');
}
public function index()
{
$this->addJs('/modules/system/assets/js/mailbrandsettings/mailbrandsettings.js', 'core');
$this->addCss('/modules/system/assets/css/mailbrandsettings/mailbrandsettings.css', 'core');
$setting = MailBrandSetting::instance();
if ($setting->exists) {
return $this->update($setting->id);
}
else {
return $this->create();
}
}
public function renderSampleMessage()
{
$layout = new MailLayout;
$layout->fillFromCode('default');
$template = new MailTemplate;
$template->layout = $layout;
$template->content_html = File::get(base_path('modules/system/models/mailbrandsetting/sample_template.htm'));
return MailManager::instance()->renderTemplate($template);
}
public function formCreateModelObject()
{
return MailBrandSetting::instance();
}
}

View File

@ -0,0 +1,11 @@
<div id="<?= $this->getId('mailPreviewContainer') ?>"></div>
<script type="text/template" id="<?= $this->getId('mailPreviewTemplate') ?>"><?= $this->renderSampleMessage() ?></script>
<script>
(function($){
var previewContents = $('#<?= $this->getId('mailPreviewTemplate') ?>').html()
previewFrame = $('#<?= $this->getId('mailPreviewContainer') ?>').get(0)
createPreviewContainer(previewFrame, previewContents)
})(window.jQuery)
</script>

View File

@ -0,0 +1,15 @@
# ===================================
# Form Behavior Config
# ===================================
# Record name
name: system::lang.mail_brand.menu_label
# Fields are defined by extension
form: ~/modules/system/models/mailbrandsetting/fields.yaml
# Model Class name
modelClass: System\Models\MailBrandSetting
# Default redirect location
defaultRedirect: system/themes

View File

@ -0,0 +1,68 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?php Block::put('form-contents') ?>
<div class="layout">
<div class="layout-row">
<?= $this->formRenderOutsideFields() ?>
<?= $this->formRenderPrimaryTabs() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('backend/users') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
<button
type="button"
class="oc-icon-trash-o btn-icon danger pull-right"
data-request="onDelete"
data-load-indicator="<?= e(trans('backend::lang.form.deleting')) ?>"
data-request-confirm="<?= e(trans('backend::lang.user.delete_confirm')) ?>">
</button>
</div>
</div>
</div>
<?php Block::endPut() ?>
<?php Block::put('form-sidebar') ?>
<div class="hide-tabs"><?= $this->formRenderSecondaryTabs() ?></div>
<?php Block::endPut() ?>
<?php Block::put('body') ?>
<?= Form::open(['id' => 'brandSettingsForm', 'class'=>'layout stretch']) ?>
<?= $this->makeLayout('form-with-sidebar') ?>
<?= Form::close() ?>
<?php Block::endPut() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('system/mailtemplates') ?>" class="btn btn-default"><?= e(trans('system::lang.mail_templates.return')) ?></a></p>
<?php endif ?>

View File

@ -224,6 +224,10 @@ return [
'sending' => 'Sending test message...',
'return' => 'Return to template list'
],
'mail_brand' => [
'menu_label' => 'Customize mail templates',
'menu_description' => 'Modify the colors and appearance of mail templates.',
],
'install' => [
'project_label' => 'Attach to Project',
'plugin_label' => 'Install Plugin',

View File

@ -0,0 +1,156 @@
<?php namespace System\Models;
use App;
use Url;
use Str;
use File;
use Lang;
use Model;
use Cache;
use Config;
use Less_Parser;
use Exception;
/**
* Mail brand settings
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class MailBrandSetting extends Model
{
use \System\Traits\ViewMaker;
use \October\Rain\Database\Traits\Validation;
/**
* @var array Behaviors implemented by this model.
*/
public $implement = [
\System\Behaviors\SettingsModel::class
];
/**
* @var string Unique code
*/
public $settingsCode = 'system_mail_brand_settings';
/**
* @var mixed Settings form field defitions
*/
public $settingsFields = 'fields.yaml';
const CACHE_KEY = 'system::mailbrand.custom_css';
const WHITE_COLOR = '#fff';
const BODY_BG = '#f5f8fa';
const PRIMARY_BG = '#3097d1';
const POSITIVE_BG = '#2ab27b';
const NEGATIVE_BG = '#bf5329';
const HEADER_COLOR = '#bbbfc3';
const HEADING_COLOR = '#2f3133';
const TEXT_COLOR = '#74787e';
const LINK_COLOR = '#3869d4';
const FOOTER_COLOR = '#aeaeae';
const BORDER_COLOR = '#edeff2';
const PROMOTION_BORDER_COLOR = '#9ba2ab';
/**
* Validation rules
*/
public $rules = [
];
/**
* Initialize the seed data for this model. This only executes when the
* model is first created or reset to default.
* @return void
*/
public function initSettingsData()
{
$config = App::make('config');
$vars = static::getCssVars();
foreach ($vars as $var => $default) {
$this->{$var} = $config->get('brand.mail.'.Str::studly($var), $default);
}
}
public function afterSave()
{
Cache::forget(self::CACHE_KEY);
}
public static function renderCss()
{
if (Cache::has(self::CACHE_KEY)) {
return Cache::get(self::CACHE_KEY);
}
try {
$customCss = self::compileCss();
Cache::forever(self::CACHE_KEY, $customCss);
}
catch (Exception $ex) {
$customCss = '/* ' . $ex->getMessage() . ' */';
}
return $customCss;
}
protected static function getCssVars()
{
$vars = [
'body_bg' => self::BODY_BG,
'content_bg' => self::WHITE_COLOR,
'content_inner_bg' => self::WHITE_COLOR,
'button_text_color' => self::WHITE_COLOR,
'button_primary_bg' => self::PRIMARY_BG,
'button_positive_bg' => self::POSITIVE_BG,
'button_negative_bg' => self::NEGATIVE_BG,
'header_color' => self::HEADER_COLOR,
'heading_color' => self::HEADING_COLOR,
'text_color' => self::TEXT_COLOR,
'link_color' => self::LINK_COLOR,
'footer_color' => self::FOOTER_COLOR,
'body_border_color' => self::BORDER_COLOR,
'subcopy_border_color' => self::BORDER_COLOR,
'table_border_color' => self::BORDER_COLOR,
'panel_bg' => self::BORDER_COLOR,
'promotion_bg' => self::WHITE_COLOR,
'promotion_border_color' => self::PROMOTION_BORDER_COLOR,
];
return $vars;
}
protected static function makeCssVars()
{
$vars = static::getCssVars();
$result = [];
foreach ($vars as $var => $default) {
// panel_bg -> panel-bg
$cssVar = str_replace('_', '-', $var);
$result[$cssVar] = self::get($var, $default);
}
return $result;
}
public static function compileCss()
{
$parser = new Less_Parser(['compress' => true]);
$basePath = base_path('modules/system/models/mailbrandsetting');
$parser->ModifyVars(static::makeCssVars());
$parser->parse(File::get($basePath . '/custom.less'));
$css = $parser->getCss();
return $css;
}
}

View File

@ -79,45 +79,55 @@ class MailLayout extends Model
continue;
}
self::createLayoutFromFile($code, $path);
$layout = new static;
$layout->code = $code;
$layout->is_locked = true;
$layout->fillFromView($path);
$layout->save();
}
}
/**
* Creates a layout using the contents of a specified file.
* @param string $code New Layout code
* @param string $viewPath View path
* @return void
*/
public static function createLayoutFromFile($code, $viewPath)
public function fillFromCode($code = null)
{
$sections = self::getTemplateSections($viewPath);
$definitions = MailManager::instance()->listRegisteredLayouts();
$name = array_get($sections, 'settings.name', '???');
$css = 'a, a:hover {
text-decoration: none;
color: #0862A2;
font-weight: bold;
if ($code === null) {
$code = $this->code;
}
td, tr, th, table {
padding: 0px;
margin: 0px;
if (!$definition = array_get($definitions, $code)) {
throw new ApplicationException('Unable to find a registered layout with code: '.$code);
}
p {
margin: 10px 0;
}';
$this->fillFromView($definition);
}
self::create([
'is_locked' => true,
'name' => $name,
'code' => $code,
'content_css' => $css,
'content_html' => array_get($sections, 'html'),
'content_text' => array_get($sections, 'text')
]);
public function fillFromView($path)
{
$sections = self::getTemplateSections($path);
$css = '
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
';
$this->name = array_get($sections, 'settings.name', '???');
$this->content_css = $css;
$this->content_html = array_get($sections, 'html');
$this->content_text = array_get($sections, 'text');
}
protected static function getTemplateSections($code)

View File

@ -43,6 +43,13 @@ class MailPartial extends Model
'content_html' => 'required',
];
public function afterFetch()
{
if (!$this->is_custom) {
$this->fillFromCode();
}
}
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
@ -58,29 +65,36 @@ class MailPartial extends Model
continue;
}
self::createPartialFromFile($code, $path);
$partial = new static;
$partial->code = $code;
$partial->is_custom = 0;
$partial->fillFromView($path);
$partial->save();
}
}
/**
* Creates a layout using the contents of a specified file.
* @param string $code New Partial code
* @param string $viewPath View path
* @return void
*/
public static function createPartialFromFile($code, $viewPath)
public function fillFromCode($code = null)
{
$sections = self::getTemplateSections($viewPath);
$definitions = MailManager::instance()->listRegisteredPartials();
$name = array_get($sections, 'settings.name', '???');
if ($code === null) {
$code = $this->code;
}
self::create([
'name' => $name,
'code' => $code,
'is_custom' => 0,
'content_html' => array_get($sections, 'html'),
'content_text' => array_get($sections, 'text')
]);
if (!$definition = array_get($definitions, $code)) {
throw new ApplicationException('Unable to find a registered partial with code: '.$code);
}
$this->fillFromView($definition);
}
public function fillFromView($path)
{
$sections = self::getTemplateSections($path);
$this->name = array_get($sections, 'settings.name', '???');
$this->content_html = array_get($sections, 'html');
$this->content_text = array_get($sections, 'text');
}
protected static function getTemplateSections($code)

View File

@ -112,6 +112,7 @@ class MailTemplate extends Model
public function fillFromView()
{
$sections = self::getTemplateSections($this->code);
$this->content_html = $sections['html'];
$this->content_text = $sections['text'];
$this->subject = array_get($sections, 'settings.subject', 'No subject');

View File

@ -0,0 +1,285 @@
/* Base */
body, body *:not(html):not(style):not(br):not(tr):not(code) {
font-family: Avenir, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
background-color: @body-bg;
color: #74787E;
height: 100%;
hyphens: auto;
line-height: 1.4;
margin: 0;
-moz-hyphens: auto;
-ms-word-break: break-all;
width: 100% !important;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
word-break: break-all;
word-break: break-word;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: @link-color;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #2F3133;
font-size: 19px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
color: #2F3133;
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
color: #2F3133;
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
color: #74787E;
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
background-color: @body-bg;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.content {
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #bbbfc3;
font-size: 19px;
font-weight: bold;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body */
.body {
background-color: @content-bg;
border-bottom: 1px solid @body-border-color;
border-top: 1px solid @body-border-color;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.inner-body {
background-color: @content-inner-bg;
margin: 0 auto;
padding: 0;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid @subcopy-border-color;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 12px;
}
/* Footer */
.footer {
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
.footer p {
color: #AEAEAE;
font-size: 12px;
text-align: center;
}
/* Tables */
.table table {
margin: 30px auto;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.table th {
border-bottom: 1px solid #EDEFF2;
padding-bottom: 8px;
}
.table td {
color: #74787E;
font-size: 15px;
line-height: 18px;
padding: 10px 0;
}
.content-cell {
padding: 35px;
}
/* Buttons */
.action {
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.button {
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
color: @button-text-color;
display: inline-block;
text-decoration: none;
-webkit-text-size-adjust: none;
}
.button-primary {
background-color: @button-primary-bg;
border-top: 10px solid @button-primary-bg;
border-right: 18px solid @button-primary-bg;
border-bottom: 10px solid @button-primary-bg;
border-left: 18px solid @button-primary-bg;
}
.button-positive {
background-color: @button-positive-bg;
border-top: 10px solid @button-positive-bg;
border-right: 18px solid @button-positive-bg;
border-bottom: 10px solid @button-positive-bg;
border-left: 18px solid @button-positive-bg;
}
.button-negative {
background-color: @button-negative-bg;
border-top: 10px solid @button-negative-bg;
border-right: 18px solid @button-negative-bg;
border-bottom: 10px solid @button-negative-bg;
border-left: 18px solid @button-negative-bg;
}
/* Panels */
.panel {
margin: 0 0 21px;
}
.panel-content {
background-color: #EDEFF2;
padding: 16px;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Promotions */
.promotion {
background-color: #FFFFFF;
border: 2px dashed #9BA2AB;
margin: 0;
margin-bottom: 25px;
margin-top: 25px;
padding: 24px;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.promotion h1 {
text-align: center;
}
.promotion p {
font-size: 15px;
text-align: center;
}

View File

@ -0,0 +1,122 @@
# ===================================
# Field Definitions
# ===================================
fields:
_mail_preview:
type: partial
path: field_mail_preview
secondaryTabs:
fields:
_section_background:
label: Background
type: section
body_bg:
label: Body background
type: colorpicker
availableColors: []
content_bg:
label: Content background
type: colorpicker
availableColors: []
content_inner_bg:
label: Inner content background
type: colorpicker
availableColors: []
_section_buttons:
label: Buttons
type: section
button_text_color:
label: Button text color
type: colorpicker
availableColors: []
button_primary_bg:
label: Primary button background
type: colorpicker
availableColors: []
button_positive_bg:
label: Positive button background
type: colorpicker
availableColors: []
button_negative_bg:
label: Negative button background
type: colorpicker
availableColors: []
_section_type:
label: Typography
type: section
header_color:
label: Site name color
type: colorpicker
availableColors: []
heading_color:
label: Heading color
type: colorpicker
availableColors: []
text_color:
label: Text color
type: colorpicker
availableColors: []
link_color:
label: Link color
type: colorpicker
availableColors: []
footer_color:
label: Footer color
type: colorpicker
availableColors: []
_section_borders:
label: Borders
type: section
body_border_color:
label: Body border color
type: colorpicker
availableColors: []
subcopy_border_color:
label: Subcopy border color
type: colorpicker
availableColors: []
table_border_color:
label: Table border color
type: colorpicker
availableColors: []
_section_components:
label: Components
type: section
panel_bg:
label: Panel background
type: colorpicker
availableColors: []
promotion_bg:
label: Promotion background
type: colorpicker
availableColors: []
promotion_border_color:
label: Promotion border color
type: colorpicker
availableColors: []

View File

@ -0,0 +1,55 @@
{% component 'header' %}
October CMS
{% endcomponent %}
# Heading 1
This is a paragraph filled with Lorem Ipsum and a link.
Cumque dicta <a>doloremque eaque</a>, enim error laboriosam pariatur possimus tenetur veritatis voluptas.
## Heading 2
{% component 'table' %}
| Item | Description | Price |
| ------------- |:-------------:| --------:|
| Item 1 | Centered | $10 |
| Item 2 | Right-Aligned | $20 |
{% endcomponent %}
### Heading 3
This is a paragraph filled with Lorem Ipsum and a link.
Cumque dicta <a>doloremque eaque</a>, enim error laboriosam pariatur possimus tenetur veritatis voluptas.
{% component 'button' url='javascript:;' %}
Primary button
{% endcomponent %}
{% component 'button' type='positive' url='javascript:;' %}
Positive button
{% endcomponent %}
{% component 'button' type='negative' url='javascript:;' %}
Negative button
{% endcomponent %}
{% component 'panel' %}
How awesome is this panel?
{% endcomponent %}
Some more text
{% component 'promotion' %}
Coupon code: OCTOBER
{% endcomponent %}
Thanks,
October CMS
{% component 'subcopy' %}
This is the subcopy of the email
{% endcomponent %}
{% component 'footer' %}
&copy; 2017 October CMS. All rights reserved.
{% endcomponent %}