@seliseblocks/mailcraft 0.2.16 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/DOCS.md +70 -67
- package/README.md +8 -8
- package/README.md.txt +8 -8
- package/dist/mailcraft-editor.bundle.js +89 -76
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/examples/templates/activate-your-account.html +1 -1
- package/examples/templates/back-in-stock.html +4 -4
- package/examples/templates/cart-left-behind.html +3 -3
- package/examples/templates/community-giveaway.html +3 -3
- package/examples/templates/frontend-futures-invite.html +3 -3
- package/examples/templates/give-25-get-25.html +2 -2
- package/examples/templates/invoice-paid.html +3 -3
- package/examples/templates/meet-nova-launch.html +7 -7
- package/examples/templates/mega-weekend-sale.html +1 -1
- package/examples/templates/order-confirmed.html +6 -6
- package/examples/templates/rate-your-headphones.html +3 -3
- package/examples/templates/reset-your-password.html +1 -1
- package/examples/templates/thankyou-promo-code.html +3 -3
- package/examples/templates/the-sunday-brief.html +4 -4
- package/examples/templates/welcome-to-your-workspace.html +4 -4
- package/examples/templates/your-order-shipped.html +2 -2
- package/examples/templates/your-password-was-changed.html +2 -2
- package/examples/templates/your-signin-code.html +1 -1
- package/examples/vanilla.html +66 -66
- package/package.json +1 -1
- package/src/core/binder.js +10 -0
- package/src/core/blocks.js +3 -3
- package/src/core/editor-core.js +195 -6
- package/src/core/export.js +311 -22
- package/src/core/i18n/ar.js +2 -0
- package/src/core/i18n/bg.js +2 -0
- package/src/core/i18n/bn.js +2 -0
- package/src/core/i18n/ca.js +2 -0
- package/src/core/i18n/cs.js +2 -0
- package/src/core/i18n/da.js +2 -0
- package/src/core/i18n/de-CH.js +2 -0
- package/src/core/i18n/de.js +2 -0
- package/src/core/i18n/dz.js +2 -0
- package/src/core/i18n/el.js +2 -0
- package/src/core/i18n/en.js +2 -0
- package/src/core/i18n/es.js +2 -0
- package/src/core/i18n/et.js +2 -0
- package/src/core/i18n/fi.js +2 -0
- package/src/core/i18n/fr.js +2 -0
- package/src/core/i18n/hr.js +2 -0
- package/src/core/i18n/hu.js +2 -0
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +2 -0
- package/src/core/i18n/lt.js +2 -0
- package/src/core/i18n/lv.js +2 -0
- package/src/core/i18n/nb.js +2 -0
- package/src/core/i18n/nl.js +2 -0
- package/src/core/i18n/pl.js +2 -0
- package/src/core/i18n/pt.js +2 -0
- package/src/core/i18n/ro.js +2 -0
- package/src/core/i18n/ru.js +2 -0
- package/src/core/i18n/sk.js +2 -0
- package/src/core/i18n/sl.js +2 -0
- package/src/core/i18n/sv.js +2 -0
- package/src/core/i18n/tr.js +2 -0
- package/src/core/i18n/uk.js +2 -0
- package/src/core/icons.js +17 -43
- package/src/core/ids.js +1 -1
- package/src/core/import-html.js +390 -20
- package/src/core/layout-style.js +25 -0
- package/src/core/parse.js +10 -10
- package/src/core/placeholder.js +15 -15
- package/src/core/sanitize.js +4 -1
- package/src/core/theme.js +22 -1
- package/src/core/variables.js +11 -11
- package/src/mailcraft-editor.js +11 -1
- package/src/render/block-body.js +129 -17
- package/src/render/canvas.js +35 -6
- package/src/render/fields.js +19 -0
- package/src/render/focus-preserve.js +158 -158
- package/src/render/screenshot.js +26 -3
- package/src/render/style.js +6 -1
package/src/core/blocks.js
CHANGED
|
@@ -22,7 +22,7 @@ BLOCKS.push(
|
|
|
22
22
|
{ type: 'codeblock', code: 'PRE', label: 'Code', hint: 'Preformatted code sample', make: () => ({ code: 'curl -X POST https://api.example.com/v1/sends \\\n -H "Authorization: Bearer $KEY" \\\n -d campaign=mc-4471', bg: '#172033', color: '#e8e9ea', size: 12.5, pad: 14 }) },
|
|
23
23
|
);
|
|
24
24
|
BLOCKS.push(
|
|
25
|
-
{ type: 'box', code: 'BOX', label: 'Section box', hint: 'Styled container with free content', make: () => ({ html: '<strong style="font-size:19px;display:block;margin-bottom:6px">Section title</strong>Drop copy here, or paste markup. The box takes background, padding, border and radius.', bg: '#f8fafc', bgImage: '', border: 1, borderStyle: 'solid', lineColor: '#e2e8f0', topBorder: true, rightBorder: true, bottomBorder: true, leftBorder: true, radius: 12, pad: 22, align: 'left', minH: 0, maxW: 100, shadow: false }) },
|
|
25
|
+
{ type: 'box', code: 'BOX', label: 'Section box', hint: 'Styled container with free content', make: () => ({ html: '<strong style="font-size:19px;display:block;margin-bottom:6px">Section title</strong>Drop copy here, or paste markup. The box takes background, padding, border and radius.', bg: '#f8fafc', bgImage: '', bgSize: 'cover', bgPos: 'center', bgRepeat: 'no-repeat', border: 1, borderStyle: 'solid', lineColor: '#e2e8f0', topBorder: true, rightBorder: true, bottomBorder: true, leftBorder: true, radius: 12, pad: 22, align: 'left', minH: 0, maxW: 100, shadow: false }) },
|
|
26
26
|
{ type: 'svg', code: 'SVG', label: 'Inline SVG', hint: 'Paste SVG markup', make: () => ({ code: '<svg viewBox="0 0 120 40" width="120" height="40" fill="none" stroke="#0065b3" stroke-width="1.5"><rect x="0.75" y="0.75" width="118.5" height="38.5"/><path d="M12 28l14-16 12 10 10-8 18 14"/></svg>', align: 'left', width: 100, py: 10 }) },
|
|
27
27
|
);
|
|
28
28
|
// Dynamic-content markers. Same contract as merge tags (variables.js): the
|
|
@@ -167,7 +167,7 @@ export const GROUPS = {
|
|
|
167
167
|
row.cols[0].blocks = [
|
|
168
168
|
blk('divider', { py: 0 }),
|
|
169
169
|
blk('social', { align: 'center', size: 18, shape: 'bare' }),
|
|
170
|
-
blk('text', { html: '<strong>
|
|
170
|
+
blk('text', { html: '<strong>Selise Group</strong><br />You are receiving this because you signed up at selisegroup.com.', size: 11.5, align: 'center', py: 6 }),
|
|
171
171
|
blk('text', { html: '<a href="{{ unsubscribe_url }}">Unsubscribe</a> · <a href="#">Update preferences</a>', size: 11.5, align: 'center', py: 0 }),
|
|
172
172
|
];
|
|
173
173
|
return [row];
|
|
@@ -289,7 +289,7 @@ export function normalizeDoc(input) {
|
|
|
289
289
|
// rebuilding the column without them silently stripped a card
|
|
290
290
|
// column's paint on every loadDoc/loadTemplate, before export ever
|
|
291
291
|
// saw it.
|
|
292
|
-
['bg', 'radius', 'padY', 'padX', 'border', 'borderStyle', 'lineColor'].forEach((k) => {
|
|
292
|
+
['bg', 'bgImage', 'bgSize', 'bgPos', 'bgRepeat', 'overlay', 'radius', 'padY', 'padX', 'border', 'borderStyle', 'lineColor'].forEach((k) => {
|
|
293
293
|
if (c[k] !== undefined && c[k] !== '' && c[k] !== 0) col[k] = c[k];
|
|
294
294
|
});
|
|
295
295
|
return col;
|
package/src/core/editor-core.js
CHANGED
|
@@ -81,6 +81,24 @@ function syncRichContent(block, key, val) {
|
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/*
|
|
85
|
+
* The background-image vocabulary, shared by a row's panel and the content
|
|
86
|
+
* area's. Duplicated inline in both, they drifted the moment either grew an
|
|
87
|
+
* option -- and two panels offering different words for the same CSS is the
|
|
88
|
+
* kind of difference a user reads as a bug.
|
|
89
|
+
*/
|
|
90
|
+
const BG_FIT = [{ value: 'cover', label: 'Fill the section' }, { value: 'contain', label: 'Fit inside' }, { value: 'auto', label: 'Actual size' }];
|
|
91
|
+
// All nine CSS keyword positions. The importer keeps `right bottom` and
|
|
92
|
+
// friends faithfully; with only five single keywords on offer, a select showed
|
|
93
|
+
// nothing for such a value and the first touch of the control destroyed it.
|
|
94
|
+
const BG_POS = [
|
|
95
|
+
{ value: 'center', label: 'Center' }, { value: 'top', label: 'Top' }, { value: 'bottom', label: 'Bottom' },
|
|
96
|
+
{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' },
|
|
97
|
+
{ value: 'left top', label: 'Top left' }, { value: 'right top', label: 'Top right' },
|
|
98
|
+
{ value: 'left bottom', label: 'Bottom left' }, { value: 'right bottom', label: 'Bottom right' },
|
|
99
|
+
];
|
|
100
|
+
const BG_REPEAT = [{ value: 'no-repeat', label: 'No repeat' }, { value: 'repeat', label: 'Tile' }, { value: 'repeat-x', label: 'Tile across' }];
|
|
101
|
+
|
|
84
102
|
const BORDER_STYLES = [
|
|
85
103
|
{ value: 'solid', label: 'Solid' },
|
|
86
104
|
{ value: 'dashed', label: 'Dashed' },
|
|
@@ -200,6 +218,47 @@ export const AI_TONES = [
|
|
|
200
218
|
/** Every goal string, flattened -- the order the `<select>` presents them in. */
|
|
201
219
|
export const AI_GOAL_VALUES = AI_GOALS.flatMap((g) => g.items);
|
|
202
220
|
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* What the editor knows about an image source that no control can express.
|
|
224
|
+
*
|
|
225
|
+
* All three of these render fine on the canvas and then arrive broken -- or
|
|
226
|
+
* as an empty box -- in a real inbox, which is exactly the failure nobody
|
|
227
|
+
* catches before sending: the built-in placeholder is an SVG data URI, and
|
|
228
|
+
* Gmail and Outlook block data URIs outright; a `cid:` source only resolves
|
|
229
|
+
* inside a sent message that actually carries the attachment.
|
|
230
|
+
*/
|
|
231
|
+
export function imageSourceNotes(src) {
|
|
232
|
+
const s = String(src || '');
|
|
233
|
+
if (/^cid:/i.test(s)) return ['Attached image (cid:). It cannot resolve in the editor or a preview -- only in a sent message that carries the attachment.'];
|
|
234
|
+
if (!/^data:/i.test(s)) return [];
|
|
235
|
+
/*
|
|
236
|
+
* The built-in placeholder, told apart from any other SVG data URI by the
|
|
237
|
+
* hatched pattern `core/placeholder.js` draws and nothing else does -- the
|
|
238
|
+
* demo templates ship real illustrations as data URIs too, and calling
|
|
239
|
+
* those "a placeholder" would be simply untrue. Both still warn: the
|
|
240
|
+
* reason differs, the outcome in Gmail and Outlook does not.
|
|
241
|
+
*/
|
|
242
|
+
if (/pattern%20id%3D%22s%22/.test(s)) return ['Placeholder, not artwork. Gmail and Outlook block data: images, so this ships as an empty box -- replace it from the library before sending.'];
|
|
243
|
+
return ['A data: image is blocked by Gmail and Outlook and inflates the message. Upload the file and use its URL instead.'];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Every image source in the document that `imageSourceNotes` objects to -- block artwork and the three background-image slots alike, since a placeholder behind a row ships just as silently as one in an image block. */
|
|
247
|
+
export function unsendableSources(doc) {
|
|
248
|
+
const out = [];
|
|
249
|
+
const check = (v) => { if (imageSourceNotes(v).length) out.push(String(v)); };
|
|
250
|
+
const t = (doc && doc.theme) || {};
|
|
251
|
+
check(t.bgImage); check(t.contentBgImage);
|
|
252
|
+
((doc && doc.rows) || []).forEach((r) => {
|
|
253
|
+
check(r.props && r.props.bgImage);
|
|
254
|
+
(r.cols || []).forEach((c) => {
|
|
255
|
+
check(c.bgImage);
|
|
256
|
+
(c.blocks || []).forEach((b) => check(b.props && b.props.src));
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
203
262
|
export class EditorCore {
|
|
204
263
|
constructor({ variables, aiProvider, iconProvider, messages, storageProvider, storageLimits } = {}) {
|
|
205
264
|
this.variablesRaw = variables ?? null;
|
|
@@ -881,6 +940,24 @@ export class EditorCore {
|
|
|
881
940
|
if (rewrote) this.editStale = id;
|
|
882
941
|
}
|
|
883
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Several props of one block in ONE undo step.
|
|
945
|
+
*
|
|
946
|
+
* `setProp` twice is two entries in the history, so a control that has to
|
|
947
|
+
* move a pair together -- switching an image's width to pixels seeds the
|
|
948
|
+
* pixel value at the same moment -- would otherwise need two Ctrl-Z to put
|
|
949
|
+
* back. No rich-content sync here: this is for plain value props, not the
|
|
950
|
+
* `html`/`size` pair `setProp` guards.
|
|
951
|
+
*/
|
|
952
|
+
setProps(id, patch) {
|
|
953
|
+
this.commit((doc) => {
|
|
954
|
+
const f = this.find(doc, id);
|
|
955
|
+
const target = f.block ? f.block.props : (f.row ? f.row.props : null);
|
|
956
|
+
if (!target) return;
|
|
957
|
+
Object.assign(target, patch);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
884
961
|
setTheme(key, val) { this.commit((doc) => { doc.theme[key] = val; }); }
|
|
885
962
|
|
|
886
963
|
/** Column-level styling (`bg`, `radius`, `padY`, `padX` on a col object -- all optional, absent means unstyled). What lets one section hold differently-colored card columns, which row-level props can't express. */
|
|
@@ -1417,7 +1494,24 @@ export class EditorCore {
|
|
|
1417
1494
|
useAsset(a) {
|
|
1418
1495
|
const t = this.state.assetTarget;
|
|
1419
1496
|
if (t) {
|
|
1420
|
-
|
|
1497
|
+
// `{ theme: true, key }` targets the document's own paint (the content
|
|
1498
|
+
// area's background image); `{ id, ci, key }` one column of a row;
|
|
1499
|
+
// everything else is a block or row id.
|
|
1500
|
+
if (t.theme) this.setTheme(t.key, a.url);
|
|
1501
|
+
else if (t.ci !== undefined) this.setColProp(t.id, t.ci, t.key || 'bgImage', a.url);
|
|
1502
|
+
// Artwork picked from the library knows its own dimensions, so the
|
|
1503
|
+
// aspect ratio comes with it -- that is what lets the export write a
|
|
1504
|
+
// `height` beside the `width` and reserve the image's box while the
|
|
1505
|
+
// recipient still has images blocked (render/block-body.js). Both
|
|
1506
|
+
// props in one undo step, and only for an image block's own source.
|
|
1507
|
+
// Guarded on the block actually BEING an image: a video block's
|
|
1508
|
+
// thumbnail picker uses the same `src` key, and a ratio there is a
|
|
1509
|
+
// prop nothing reads -- which the video's `data-mcp` marker would then
|
|
1510
|
+
// carry into every export.
|
|
1511
|
+
else if ((t.key || 'src') === 'src' && a.w > 0 && a.ht > 0 && (this.find(this.state.doc, t.id || t).block || {}).type === 'image') {
|
|
1512
|
+
this.setProps(t.id || t, { src: a.url, ratio: Math.round((a.ht / a.w) * 10000) / 10000 });
|
|
1513
|
+
}
|
|
1514
|
+
else this.setProp(t.id || t, t.key || 'src', a.url);
|
|
1421
1515
|
this.setState({ libraryOpen: false, assetTarget: null });
|
|
1422
1516
|
this.flash(this.t('toast.imageReplaced'));
|
|
1423
1517
|
} else {
|
|
@@ -1439,6 +1533,16 @@ export class EditorCore {
|
|
|
1439
1533
|
|
|
1440
1534
|
openExport = () => {
|
|
1441
1535
|
this.setState({ exportOpen: true, exportCode: this.buildHtml(), copied: false, libraryOpen: false, aiOpen: false, codeOpen: false });
|
|
1536
|
+
/*
|
|
1537
|
+
* The preflight, at the one moment it can still change the outcome: the
|
|
1538
|
+
* author is about to take the HTML out of the editor. A placeholder or a
|
|
1539
|
+
* data:/cid: source renders perfectly on the canvas and arrives as an
|
|
1540
|
+
* empty box in the inbox, which is precisely why it ships unnoticed --
|
|
1541
|
+
* nothing in the editor ever said so. Named here, not blocked: a `cid:`
|
|
1542
|
+
* source is legitimate for a host that attaches the file at send time.
|
|
1543
|
+
*/
|
|
1544
|
+
const bad = unsendableSources(this.state.doc);
|
|
1545
|
+
if (bad.length) this.flash(bad.length === 1 ? this.t('toast.exportImageOne') : this.t('toast.exportImageMany', { count: bad.length }));
|
|
1442
1546
|
};
|
|
1443
1547
|
|
|
1444
1548
|
copyExport = () => {
|
|
@@ -1585,7 +1689,55 @@ export class EditorCore {
|
|
|
1585
1689
|
? [{ kind: 'richLinks', label: 'Links', html: b.props.html || '', onChange: (v) => this.setProp(b.id, 'html', v) }]
|
|
1586
1690
|
: [],
|
|
1587
1691
|
[B.area('Text', 'html'), B.sel('Font', 'fontFamily', this.fontOptions(true, b.props.fontFamily)), B.range('Text size', 'size', ...SIZE_SPAN.text, 1, 'px'), B.range('Line spacing', 'lh', 0.8, 3, 0.05, ''), B.seg('Align', 'align', ALIGN), B.sel('Text weight', 'weight', [{ value: '400', label: 'Regular' }, { value: '500', label: 'Medium' }, { value: '700', label: 'Bold' }]), B.color('Text color', 'color')], padF));
|
|
1588
|
-
case 'image':
|
|
1692
|
+
case 'image': {
|
|
1693
|
+
/*
|
|
1694
|
+
* Width in PERCENT or in PIXELS.
|
|
1695
|
+
*
|
|
1696
|
+
* Percent is the default and the right answer for a hero -- it
|
|
1697
|
+
* follows the column on every screen. It cannot say "88px",
|
|
1698
|
+
* though, and forcing a logo through it both rounded the size
|
|
1699
|
+
* (88 of 620 is 14.2%) and let it drift: the next save re-derived
|
|
1700
|
+
* the percentage from the rounded pixels. Pixels pin the number
|
|
1701
|
+
* exactly, and `max-width:100%` in the sent mail keeps a pinned
|
|
1702
|
+
* image from holding a phone column open.
|
|
1703
|
+
*
|
|
1704
|
+
* Switching to pixels seeds the current on-canvas size, so the
|
|
1705
|
+
* image does not jump at the moment of the switch -- and both
|
|
1706
|
+
* props move in ONE undo step (`setProps`).
|
|
1707
|
+
*/
|
|
1708
|
+
const colSpan = Number((this.find(this.state.doc, b.id).col || {}).span) || 100;
|
|
1709
|
+
const colPx = Math.max(20, Math.round((Number(this.state.doc.theme.width) || 620) * (colSpan / 100)));
|
|
1710
|
+
const pinned = b.props.wUnit === 'px';
|
|
1711
|
+
const unitSeg = {
|
|
1712
|
+
kind: 'seg',
|
|
1713
|
+
label: 'Width unit',
|
|
1714
|
+
options: [{ value: 'pct', label: 'Percent' }, { value: 'px', label: 'Pixels' }].map((o) => {
|
|
1715
|
+
const on = (o.value === 'px') === pinned;
|
|
1716
|
+
return {
|
|
1717
|
+
label: o.label,
|
|
1718
|
+
bg: on ? 'var(--ed-accent)' : 'transparent',
|
|
1719
|
+
fg: on ? 'var(--ed-accent-ink)' : 'var(--ed-muted)',
|
|
1720
|
+
onClick: () => {
|
|
1721
|
+
if (o.value === 'px') {
|
|
1722
|
+
this.setProps(b.id, { wUnit: 'px', wpx: Number(b.props.wpx) > 0 ? Number(b.props.wpx) : Math.max(1, Math.round(colPx * ((Number(b.props.width) || 100) / 100))) });
|
|
1723
|
+
} else {
|
|
1724
|
+
// The percentage the pinned size currently amounts to,
|
|
1725
|
+
// so the image does not jump back either.
|
|
1726
|
+
this.setProps(b.id, { wUnit: 'pct', width: Math.max(5, Math.min(100, Math.round(((Number(b.props.wpx) || colPx) / colPx) * 100))) });
|
|
1727
|
+
}
|
|
1728
|
+
},
|
|
1729
|
+
};
|
|
1730
|
+
}),
|
|
1731
|
+
};
|
|
1732
|
+
return decorate(base.concat(
|
|
1733
|
+
[B.btn('Choose from library', () => this.openLibrary({ id: b.id, key: 'src' }))],
|
|
1734
|
+
imageSourceNotes(b.props.src).map((n) => B.note(n)),
|
|
1735
|
+
[B.text('Alt text', 'alt', 'Describe the image'), B.text('Link URL', 'href', 'https://'), unitSeg],
|
|
1736
|
+
[pinned ? B.range('Width', 'wpx', 8, Math.max(200, colPx), 1, 'px') : B.range('Width', 'width', 5, 100, 1, '%')],
|
|
1737
|
+
[B.seg('Align', 'align', ALIGN), B.range('Rounded corners', 'radius', 0, 200, 1, 'px')],
|
|
1738
|
+
padF,
|
|
1739
|
+
));
|
|
1740
|
+
}
|
|
1589
1741
|
case 'button': return decorate(base.concat([B.text('Label', 'label'), B.text('Link URL', 'href', 'https://'), B.color('Button color', 'bg'), B.color('Text color', 'color'), B.sel('Font', 'fontFamily', this.fontOptions(true, b.props.fontFamily)), B.range('Text size', 'size', 8, 48, 1, 'px'), B.range('Rounded corners', 'radius', 0, 60, 1, 'px'), B.range('Outline thickness', 'borderW', 0, 6, 1, 'px')].concat(b.props.borderW ? [B.sel('Outline style', 'borderStyle', BORDER_STYLES), B.color('Outline color', 'borderColor')] : []).concat([B.range('Button height', 'py', 0, 60, 1, 'px'), B.range('Button width', 'px', 0, 120, 2, 'px'), B.seg('Align', 'align', ALIGN), B.tog('Full width', 'full')])));
|
|
1590
1742
|
case 'divider': return decorate(base.concat([B.range('Thickness', 'thickness', 1, 20, 1, 'px'), B.sel('Line style', 'lineStyle', BORDER_STYLES), B.range('Width', 'width', 5, 100, 5, '%'), B.color('Color', 'color'), B.range('Space above & below', 'py', 0, 160, 2, 'px')]));
|
|
1591
1743
|
case 'spacer': return decorate(base.concat([B.range('Height', 'height', 0, 400, 2, 'px')]));
|
|
@@ -1632,7 +1784,7 @@ export class EditorCore {
|
|
|
1632
1784
|
B.color('Header color', 'headBg'), B.color('Line color', 'lineColor'), B.seg('Align', 'align', ALIGN),
|
|
1633
1785
|
]));
|
|
1634
1786
|
case 'css': return decorate(base.concat([B.area('CSS', 'code'), B.text('Note to yourself', 'note')]));
|
|
1635
|
-
case 'box': return decorate(base.concat([B.area('Content (HTML allowed)', 'html'), B.color('Background color', 'bg'), B.btn('Choose background image', () => this.openLibrary({ id: b.id, key: 'bgImage' })), B.text('Background image URL', 'bgImage', 'https://'), B.range('Space inside', 'pad', 0, 160, 2, 'px'), B.range('Border thickness', 'border', 0, 20, 1, 'px')].concat(b.props.border ? [
|
|
1787
|
+
case 'box': return decorate(base.concat([B.area('Content (HTML allowed)', 'html'), B.color('Background color', 'bg'), B.btn('Choose background image', () => this.openLibrary({ id: b.id, key: 'bgImage' })), B.text('Background image URL', 'bgImage', 'https://'), ...(b.props.bgImage ? [B.sel('Image fit', 'bgSize', BG_FIT), B.sel('Image position', 'bgPos', BG_POS), B.sel('Image repeat', 'bgRepeat', BG_REPEAT)] : []), B.range('Space inside', 'pad', 0, 160, 2, 'px'), B.range('Border thickness', 'border', 0, 20, 1, 'px')].concat(b.props.border ? [
|
|
1636
1788
|
B.sel('Border style', 'borderStyle', BORDER_STYLES), B.color('Border color', 'lineColor'),
|
|
1637
1789
|
B.tog('Top border', 'topBorder', true), B.tog('Right border', 'rightBorder', true),
|
|
1638
1790
|
B.tog('Bottom border', 'bottomBorder', true), B.tog('Left border', 'leftBorder', true),
|
|
@@ -1706,6 +1858,19 @@ export class EditorCore {
|
|
|
1706
1858
|
return acc.concat([
|
|
1707
1859
|
B.head('Column ' + (ci + 1)),
|
|
1708
1860
|
CB.color('Background color', 'bg'),
|
|
1861
|
+
// A photo behind ONE column of a row -- the two-up "image beside
|
|
1862
|
+
// copy" band that until now could only be built by putting the
|
|
1863
|
+
// image in the column as a block, where it could not sit behind
|
|
1864
|
+
// the text. Same three controls the section background offers,
|
|
1865
|
+
// in the same order, so the two read as one feature at two
|
|
1866
|
+
// scales.
|
|
1867
|
+
CB.btn(c.bgImage ? 'Change background image' : 'Add background image', () => this.openLibrary({ id: r.id, ci, key: 'bgImage' })),
|
|
1868
|
+
...(c.bgImage ? [
|
|
1869
|
+
CB.btn('Remove background image', () => this.setColProp(r.id, ci, 'bgImage', '')),
|
|
1870
|
+
CB.sel('Image fit', 'bgSize', BG_FIT),
|
|
1871
|
+
CB.sel('Image position', 'bgPos', BG_POS),
|
|
1872
|
+
CB.range('Darken image', 'overlay', 0, 100, 1, '%'),
|
|
1873
|
+
] : []),
|
|
1709
1874
|
CB.range('Border thickness', 'border', 0, 20, 1, 'px'),
|
|
1710
1875
|
...(c.border ? [CB.sel('Border style', 'borderStyle', BORDER_STYLES), CB.color('Border color', 'lineColor')] : []),
|
|
1711
1876
|
CB.range('Rounded corners', 'radius', 0, 100, 1, 'px'),
|
|
@@ -1718,8 +1883,8 @@ export class EditorCore {
|
|
|
1718
1883
|
B.btn(p.bgImage ? 'Change background image' : 'Add background image', () => this.openLibrary({ id: r.id, key: 'bgImage' })),
|
|
1719
1884
|
...(p.bgImage ? [
|
|
1720
1885
|
B.btn('Remove background image', () => this.setProp(r.id, 'bgImage', '')),
|
|
1721
|
-
B.sel('Image fit', 'bgSize',
|
|
1722
|
-
B.sel('Image position', 'bgPos',
|
|
1886
|
+
B.sel('Image fit', 'bgSize', BG_FIT),
|
|
1887
|
+
B.sel('Image position', 'bgPos', BG_POS),
|
|
1723
1888
|
B.range('Darken image', 'overlay', 0, 100, 1, '%'),
|
|
1724
1889
|
] : []),
|
|
1725
1890
|
|
|
@@ -1771,7 +1936,7 @@ export class EditorCore {
|
|
|
1771
1936
|
] : []),
|
|
1772
1937
|
...(p.layout === 'grid' ? [B.range('Grid columns', 'gridCols', 1, 12, 1, '')] : []),
|
|
1773
1938
|
B.text('Background image URL', 'bgImage', 'https://'),
|
|
1774
|
-
B.sel('Image repeat', 'bgRepeat',
|
|
1939
|
+
B.sel('Image repeat', 'bgRepeat', BG_REPEAT),
|
|
1775
1940
|
B.range('Max width', 'maxW', 10, 100, 5, '%'),
|
|
1776
1941
|
B.seg('Vertical align', 'valign', [{ value: 'top', label: 'Top' }, { value: 'middle', label: 'Middle' }, { value: 'bottom', label: 'Bottom' }]),
|
|
1777
1942
|
] : []),
|
|
@@ -1817,7 +1982,11 @@ export class EditorCore {
|
|
|
1817
1982
|
|
|
1818
1983
|
themeFields() {
|
|
1819
1984
|
const B = binder(() => this.state.doc.theme, (k, v) => this.setTheme(k, v), this);
|
|
1985
|
+
const th = this.state.doc.theme;
|
|
1820
1986
|
return decorate([
|
|
1987
|
+
B.head('Message'),
|
|
1988
|
+
B.text('Preview text', 'preheader', 'Shown next to the subject line in the inbox'),
|
|
1989
|
+
B.sel('Reading direction', 'dir', [{ value: '', label: 'Left to right' }, { value: 'rtl', label: 'Right to left' }]),
|
|
1821
1990
|
B.head('Canvas'),
|
|
1822
1991
|
B.slider('Content area width', 'width', 280, 900, 5, 'px'),
|
|
1823
1992
|
// The page section: everything outside the content column. Its colour
|
|
@@ -1825,12 +1994,32 @@ export class EditorCore {
|
|
|
1825
1994
|
// way to size it, the band around a template could only be seen (and
|
|
1826
1995
|
// never adjusted) in a sent message.
|
|
1827
1996
|
B.color('Page background color', 'bg', { transparent: true, solid: '#eef2f7' }),
|
|
1997
|
+
B.btn(th.bgImage ? 'Change background image' : 'Add background image', () => this.openLibrary({ theme: true, key: 'bgImage' })),
|
|
1998
|
+
...(th.bgImage ? [
|
|
1999
|
+
B.btn('Remove background image', () => this.setTheme('bgImage', '')),
|
|
2000
|
+
B.sel('Image fit', 'bgSize', BG_FIT),
|
|
2001
|
+
B.sel('Image position', 'bgPos', BG_POS),
|
|
2002
|
+
B.sel('Image repeat', 'bgRepeat', BG_REPEAT),
|
|
2003
|
+
] : []),
|
|
2004
|
+
B.text('Background image URL', 'bgImage', 'https://'),
|
|
1828
2005
|
group('Space around content', [
|
|
1829
2006
|
B.range('Top & bottom', 'padY', 0, 120, 2, 'px'),
|
|
1830
2007
|
B.range('Sides', 'padX', 0, 120, 2, 'px'),
|
|
1831
2008
|
]),
|
|
1832
2009
|
B.head('Content area'),
|
|
1833
2010
|
B.color('Content area background color', 'contentBg', { transparent: true, solid: '#ffffff' }),
|
|
2011
|
+
// ONE place to add a background image: the full-width page, above. A
|
|
2012
|
+
// second "add" here read as a choice nobody could make sense of. The
|
|
2013
|
+
// content column can still CARRY an image -- an imported template that
|
|
2014
|
+
// painted its content table keeps it, faithfully -- so when one is
|
|
2015
|
+
// present it is shown, adjustable and removable rather than becoming
|
|
2016
|
+
// state the panel hides. Nothing offers to add one from here.
|
|
2017
|
+
...(th.contentBgImage ? [
|
|
2018
|
+
B.btn('Remove content background image', () => this.setTheme('contentBgImage', '')),
|
|
2019
|
+
B.sel('Image fit', 'contentBgSize', BG_FIT),
|
|
2020
|
+
B.sel('Image position', 'contentBgPos', BG_POS),
|
|
2021
|
+
B.sel('Image repeat', 'contentBgRepeat', BG_REPEAT),
|
|
2022
|
+
] : []),
|
|
1834
2023
|
B.range('Corner radius', 'radius', 0, 48, 1, 'px'),
|
|
1835
2024
|
B.range('Border thickness', 'borderW', 0, 12, 1, 'px'),
|
|
1836
2025
|
B.sel('Drop shadow', 'shadow', [
|