@seliseblocks/mailcraft 0.2.16 → 0.2.18
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 +33 -0
- package/DOCS.md +70 -67
- package/README.md +8 -8
- package/README.md.txt +8 -8
- package/dist/mailcraft-editor.bundle.js +84 -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-password-was-changed.html +2 -2
- package/examples/templates/your-signin-code.html +1 -1
- package/examples/vanilla.html +63 -63
- package/package.json +1 -1
- package/src/core/binder.js +10 -0
- package/src/core/blocks.js +2 -2
- 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/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/import-html.js +390 -20
- package/src/core/layout-style.js +25 -0
- package/src/core/sanitize.js +4 -1
- package/src/core/theme.js +22 -1
- package/src/mailcraft-editor.js +8 -0
- 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/screenshot.js +26 -3
package/src/render/block-body.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { icon, brandIcon, socialKey, SOCIAL_BRAND, contrastInk } from '../core/icons.js';
|
|
2
2
|
import { pad } from '../core/layout-style.js';
|
|
3
3
|
import { parseItems, cellsOf } from '../core/parse.js';
|
|
4
|
-
import { linkHref } from '../core/sanitize.js';
|
|
4
|
+
import { linkHref, cssUrl } from '../core/sanitize.js';
|
|
5
5
|
|
|
6
6
|
function el(tag, style, attrs) {
|
|
7
7
|
const node = document.createElement(tag);
|
|
@@ -93,7 +93,7 @@ function overrideLinkColor(root, link) {
|
|
|
93
93
|
* toolbar node or null), `ctx.onTableCellBlur(block, ri, ci, value)`, `ctx.now`
|
|
94
94
|
* (for the countdown), `ctx.vars` and `ctx.onInsertVariable` for the code-view escape hatch.
|
|
95
95
|
*/
|
|
96
|
-
export function blockBody(b, theme, live, ctx) {
|
|
96
|
+
export function blockBody(b, theme, live, ctx, colPx) {
|
|
97
97
|
const p = b.props; const t = theme;
|
|
98
98
|
const attr = live ? { 'data-mc-content': b.id } : {};
|
|
99
99
|
|
|
@@ -119,20 +119,87 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
119
119
|
case 'image': {
|
|
120
120
|
const wrap = el('div', { padding: pad(p), textAlign: p.align, fontSize: '0' }, attr);
|
|
121
121
|
const imgHref = linkHref(p.href);
|
|
122
|
+
/*
|
|
123
|
+
* Two things every exported image needs and had neither of.
|
|
124
|
+
*
|
|
125
|
+
* `width` as an ATTRIBUTE, in px: the Word engine behind Classic
|
|
126
|
+
* Outlook does not resolve a percentage width on an image and falls
|
|
127
|
+
* back to the file's intrinsic size, so a 1200px hero authored at 60%
|
|
128
|
+
* shipped 1200px wide and tore the layout open. The CSS percentage
|
|
129
|
+
* stays for every client that can do better -- the attribute is only
|
|
130
|
+
* the floor Word reads -- and `height:auto` keeps the aspect ratio once
|
|
131
|
+
* a width is pinned.
|
|
132
|
+
*
|
|
133
|
+
* A real `font-size` on the IMG: the wrapper sets `font-size:0` to kill
|
|
134
|
+
* the inline-block whitespace gap, and alt text inherits it, so with
|
|
135
|
+
* images blocked (Outlook's default) the alt text rendered at 0px and
|
|
136
|
+
* the recipient saw nothing at all. The wrapper keeps its 0; the image
|
|
137
|
+
* carries type of its own for the blocked state.
|
|
138
|
+
*/
|
|
139
|
+
/*
|
|
140
|
+
* Two ways to size an image, and the second one is why a logo used to
|
|
141
|
+
* drift.
|
|
142
|
+
*
|
|
143
|
+
* The default is a PERCENTAGE of the column -- responsive, and the
|
|
144
|
+
* right answer for a hero. But a percentage cannot express "88px", and
|
|
145
|
+
* an import that met one had to round it into the nearest whole
|
|
146
|
+
* percent (88 of 600 -> 15% -> 93px). Every save re-derived the
|
|
147
|
+
* percent from the new pixel width, so the logo crept a little wider
|
|
148
|
+
* each time and never landed back on 88.
|
|
149
|
+
*
|
|
150
|
+
* `wUnit: 'px'` with `wpx` pins the real number instead: the CSS width,
|
|
151
|
+
* the Word `width` attribute and the read-back all carry the same
|
|
152
|
+
* integer, so the value is a fixed point. `max-width:100%` (plus the
|
|
153
|
+
* stylesheet's own `img` rule in the sent mail) keeps a pinned image
|
|
154
|
+
* from holding a narrow phone column open.
|
|
155
|
+
*/
|
|
156
|
+
const fixed = p.wUnit === 'px' && Number(p.wpx) > 0;
|
|
157
|
+
const pxW = fixed
|
|
158
|
+
? Math.max(1, Math.round(Number(p.wpx)))
|
|
159
|
+
: Math.max(1, Math.round(((Number(colPx) || Number(t.width) || 620)) * ((Number(p.width) || 100) / 100)));
|
|
160
|
+
const cssW = fixed ? pxW + 'px' : (Number(p.width) || 100) + '%';
|
|
161
|
+
/*
|
|
162
|
+
* `height` as an attribute, derived from the source's own aspect ratio
|
|
163
|
+
* rather than invented: with images blocked (Outlook's default, and
|
|
164
|
+
* Gmail's for an unknown sender) a width-only image collapses to a
|
|
165
|
+
* one-line strip and the layout below it jumps when the image loads.
|
|
166
|
+
* A width AND a height reserve the real box. The CSS `height:auto`
|
|
167
|
+
* still wins in every client that reads CSS, so a client that scales
|
|
168
|
+
* the image down keeps its proportions -- the attribute is only the
|
|
169
|
+
* floor Word and a blocked-image placeholder read.
|
|
170
|
+
*
|
|
171
|
+
* Absent unless the ratio is known (an import that carried both
|
|
172
|
+
* dimensions, or a library asset), so no image gains a guessed height.
|
|
173
|
+
*/
|
|
174
|
+
const ratio = Number(p.ratio) > 0 ? Number(p.ratio) : 0;
|
|
175
|
+
const hAttr = ratio ? String(Math.max(1, Math.round(pxW * ratio))) : undefined;
|
|
176
|
+
// Retina and responsive sources ride through untouched. Apple Mail and
|
|
177
|
+
// iOS pick from `srcset`; every other client ignores both attributes
|
|
178
|
+
// and uses `src`, which is why `src` stays the 1x fallback.
|
|
179
|
+
const srcAttrs = {
|
|
180
|
+
srcset: p.srcset || undefined,
|
|
181
|
+
sizes: p.srcset && p.sizes ? p.sizes : undefined,
|
|
182
|
+
title: p.title || undefined,
|
|
183
|
+
};
|
|
184
|
+
const altType = { fontSize: '13px', lineHeight: '1.4', fontFamily: t.font, color: t.text };
|
|
122
185
|
if (imgHref) {
|
|
123
186
|
// The % width must live on the anchor, not the img: a percentage on a
|
|
124
187
|
// child of a shrink-to-fit inline-block resolves against the image's
|
|
125
188
|
// own intrinsic size (i.e. not at all), which rendered every linked
|
|
126
189
|
// logo/icon at full intrinsic width no matter what `width` said.
|
|
127
|
-
|
|
190
|
+
// `max-width` only in pixel mode: a percentage anchor is already
|
|
191
|
+
// capped by its container, and adding the declaration unconditionally
|
|
192
|
+
// would rewrite the markup of every linked image that never asked for
|
|
193
|
+
// this feature.
|
|
194
|
+
const a = el('a', { display: 'inline-block', width: cssW, ...(fixed ? { maxWidth: '100%' } : {}) }, { href: imgHref });
|
|
128
195
|
// Same guard as every other anchor the canvas draws: without it a
|
|
129
196
|
// click on a linked logo navigates the host application away from the
|
|
130
197
|
// editor, taking the uncommitted document with it.
|
|
131
198
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
132
|
-
a.appendChild(el('img', { width: '100%', borderRadius: p.radius + 'px', display: 'block', border: '0' }, { src: p.src, alt: p.alt }));
|
|
199
|
+
a.appendChild(el('img', { width: '100%', height: 'auto', borderRadius: p.radius + 'px', display: 'block', border: '0', ...altType }, { src: p.src, alt: p.alt, width: String(pxW), height: hAttr, border: '0', ...srcAttrs }));
|
|
133
200
|
wrap.appendChild(a);
|
|
134
201
|
} else {
|
|
135
|
-
wrap.appendChild(el('img', { width:
|
|
202
|
+
wrap.appendChild(el('img', { width: cssW, height: 'auto', maxWidth: '100%', borderRadius: p.radius + 'px', display: 'inline-block', border: '0', ...altType }, { src: p.src, alt: p.alt, width: String(pxW), height: hAttr, border: '0', ...srcAttrs }));
|
|
136
203
|
}
|
|
137
204
|
return wrap;
|
|
138
205
|
}
|
|
@@ -197,11 +264,29 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
197
264
|
}
|
|
198
265
|
case 'divider': {
|
|
199
266
|
const wrap = el('div', { padding: p.py + 'px 0' }, attr);
|
|
200
|
-
|
|
267
|
+
const rule = p.thickness + 'px ' + (p.lineStyle || 'solid') + ' ' + p.color;
|
|
268
|
+
// Word gives a zero-height div nothing to draw the border on, so the
|
|
269
|
+
// same rule ships twice: once on this div, which every other client
|
|
270
|
+
// renders and the importer reads (classifyDivider: a DIV bar with a
|
|
271
|
+
// border-top), and once on a <td> inside an MSO-only table, the shape
|
|
272
|
+
// Word does paint. `<!--[if !mso]><!-->` keeps the div out of Word so
|
|
273
|
+
// it never draws both. Comments are inert to the importer.
|
|
274
|
+
const doc = wrap.ownerDocument;
|
|
275
|
+
wrap.appendChild(doc.createComment('[if !mso]><!'));
|
|
276
|
+
wrap.appendChild(el('div', { height: '0', width: p.width + '%', margin: '0 auto', borderTop: rule, fontSize: '1px', lineHeight: '1px' }, { html: ' ' }));
|
|
277
|
+
wrap.appendChild(doc.createComment('<![endif]'));
|
|
278
|
+
wrap.appendChild(doc.createComment('[if mso]><table role="presentation" width="' + p.width + '%" align="center" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-top:' + rule + ';font-size:1px;line-height:1px;"> </td></tr></table><![endif]'));
|
|
201
279
|
return wrap;
|
|
202
280
|
}
|
|
203
|
-
case 'spacer':
|
|
204
|
-
|
|
281
|
+
case 'spacer': {
|
|
282
|
+
// An empty div has no height in Word: it needs a character to give a
|
|
283
|
+
// line to, and an exact line-height to size that line (msoHarden adds
|
|
284
|
+
// mso-line-height-rule:exactly wherever a px line-height stands). This
|
|
285
|
+
// is mj-spacer's shape. The NBSP trims to nothing for the importer, so
|
|
286
|
+
// classifySpacer still sees an empty div with a height.
|
|
287
|
+
const h = Math.max(0, Number(p.height) || 0);
|
|
288
|
+
return el('div', { height: h + 'px', lineHeight: h + 'px', fontSize: '1px' }, { ...attr, html: ' ' });
|
|
289
|
+
}
|
|
205
290
|
case 'social': {
|
|
206
291
|
// Two independent axes cover every style the block offers without a
|
|
207
292
|
// second hand-drawn icon set per platform: `palette` picks the source
|
|
@@ -241,14 +326,32 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
241
326
|
}
|
|
242
327
|
case 'video': {
|
|
243
328
|
const wrap = el('div', { padding: '4px 0', textAlign: 'center' }, attr);
|
|
244
|
-
|
|
329
|
+
/*
|
|
330
|
+
* The play badge used to float over the thumbnail with
|
|
331
|
+
* position:absolute. Gmail strips `position` outright and Word ignores
|
|
332
|
+
* it, so in both the badge fell to a line below the picture. Now the
|
|
333
|
+
* thumbnail is the cell's background (the one overlay mechanism email
|
|
334
|
+
* has) and the badge is plain centred content inside it. The cell needs
|
|
335
|
+
* a height, since a background gives none: 16:9 of the column width,
|
|
336
|
+
* which is what a video thumbnail is. The whole cell is the link.
|
|
337
|
+
*/
|
|
338
|
+
const vw = Math.max(120, Number(colPx) || Number(t.width) || 620);
|
|
339
|
+
const vh = Math.round(vw * 9 / 16);
|
|
340
|
+
const vurl = cssUrl(p.src);
|
|
341
|
+
const table = el('table', { width: '100%', borderCollapse: 'collapse' }, { role: 'presentation', cellpadding: '0', cellspacing: '0', border: '0', width: '100%' });
|
|
342
|
+
const tr = el('tr'); table.appendChild(tr);
|
|
343
|
+
const cell = el('td', {
|
|
344
|
+
height: vh + 'px', textAlign: 'center', verticalAlign: 'middle',
|
|
345
|
+
backgroundColor: '#111111', backgroundImage: vurl ? 'url("' + vurl + '")' : 'none',
|
|
346
|
+
backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat',
|
|
347
|
+
}, { height: String(vh), align: 'center', valign: 'middle', background: p.src || undefined, bgcolor: '#111111' });
|
|
348
|
+
const a = el('a', { display: 'inline-block', textDecoration: 'none' }, { href: linkHref(p.href), title: p.caption || undefined });
|
|
245
349
|
a.addEventListener('click', (e) => e.preventDefault());
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
wrap.appendChild(a);
|
|
350
|
+
const circle = el('span', { display: 'inline-block', width: '54px', height: '54px', lineHeight: '54px', borderRadius: '50%', background: p.badge, color: '#fff', textAlign: 'center', fontSize: '18px', fontFamily: 'Arial, sans-serif' }, { text: '▶' });
|
|
351
|
+
a.appendChild(circle);
|
|
352
|
+
cell.appendChild(a);
|
|
353
|
+
tr.appendChild(cell);
|
|
354
|
+
wrap.appendChild(table);
|
|
252
355
|
wrap.appendChild(el('div', { fontFamily: t.font, fontSize: '12.5px', color: p.badge, opacity: '0.7', marginTop: '8px' }, { text: p.caption }));
|
|
253
356
|
return wrap;
|
|
254
357
|
}
|
|
@@ -385,8 +488,17 @@ export function blockBody(b, theme, live, ctx) {
|
|
|
385
488
|
const edit = live;
|
|
386
489
|
const borderSide = (on) => (p.border && on !== false ? p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + p.lineColor : '0');
|
|
387
490
|
const box = el('div', {
|
|
388
|
-
|
|
389
|
-
|
|
491
|
+
// Colour and image as separate longhands, never one shorthand. The
|
|
492
|
+
// shorthand made `p.bg` unreachable the moment an image was set --
|
|
493
|
+
// there was no colour left underneath when a client refused the image
|
|
494
|
+
// -- and it opened with a no-op `linear-gradient()` that outlook.com's
|
|
495
|
+
// sanitiser drops the whole declaration over, taking the photo with
|
|
496
|
+
// it. Same rule as a row's background (core/export.js).
|
|
497
|
+
backgroundColor: p.bg || 'transparent',
|
|
498
|
+
backgroundImage: p.bgImage ? 'url("' + cssUrl(p.bgImage) + '")' : 'none',
|
|
499
|
+
// The same three controls a row has; hard-coded cover/center meant a
|
|
500
|
+
// box background could never tile or pin to an edge.
|
|
501
|
+
backgroundSize: p.bgSize || 'cover', backgroundPosition: p.bgPos || 'center', backgroundRepeat: p.bgRepeat || 'no-repeat',
|
|
390
502
|
borderTop: borderSide(p.topBorder), borderRight: borderSide(p.rightBorder),
|
|
391
503
|
borderBottom: borderSide(p.bottomBorder), borderLeft: borderSide(p.leftBorder),
|
|
392
504
|
borderRadius: p.radius + 'px', padding: p.pad + 'px', textAlign: p.align,
|
package/src/render/canvas.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEF } from '../core/blocks.js';
|
|
2
|
-
import { boxStyle, rowBg, rowPad, colsWrap, colStyle } from '../core/layout-style.js';
|
|
3
|
-
import { scopeCss } from '../core/sanitize.js';
|
|
2
|
+
import { boxStyle, rowBg, rowPad, colsWrap, colStyle, colBg } from '../core/layout-style.js';
|
|
3
|
+
import { scopeCss, cssUrl } from '../core/sanitize.js';
|
|
4
4
|
import { cellsOf } from '../core/parse.js';
|
|
5
5
|
import { icon } from '../core/icons.js';
|
|
6
6
|
import { blockBody } from './block-body.js';
|
|
@@ -189,7 +189,14 @@ export function renderDoc(core, live) {
|
|
|
189
189
|
const radius = Number(theme.radius) || 0;
|
|
190
190
|
const borderW = Number(theme.borderW) || 0;
|
|
191
191
|
const root = el('div', {
|
|
192
|
-
width: width + 'px', maxWidth: '100%',
|
|
192
|
+
width: width + 'px', maxWidth: '100%', color: theme.text, fontFamily: theme.font,
|
|
193
|
+
// Colour and image as separate longhands, mirroring what the exporter
|
|
194
|
+
// writes -- the canvas has to show the same paint the recipient gets.
|
|
195
|
+
backgroundColor: theme.contentBg || 'transparent',
|
|
196
|
+
backgroundImage: theme.contentBgImage ? 'url("' + cssUrl(theme.contentBgImage) + '")' : 'none',
|
|
197
|
+
backgroundSize: theme.contentBgSize || 'cover',
|
|
198
|
+
backgroundPosition: theme.contentBgPos || 'center',
|
|
199
|
+
backgroundRepeat: theme.contentBgRepeat || 'no-repeat',
|
|
193
200
|
// Only when the document actually asks for a shape: an unconditional
|
|
194
201
|
// `0px` would override the editor chrome's own soft corner on the sheet
|
|
195
202
|
// (style.js) and square off every template that never touched the field.
|
|
@@ -224,6 +231,15 @@ export function renderDoc(core, live) {
|
|
|
224
231
|
const padX = Number(theme.padX) || 0;
|
|
225
232
|
const page = el('div', {
|
|
226
233
|
background: theme.bg || 'transparent',
|
|
234
|
+
// The page image, only when there is one: the longhands are added AFTER
|
|
235
|
+
// the shorthand so they override it, and are absent otherwise so a
|
|
236
|
+
// document with no image paints exactly as it always did.
|
|
237
|
+
...(theme.bgImage ? {
|
|
238
|
+
backgroundImage: 'url("' + cssUrl(theme.bgImage) + '")',
|
|
239
|
+
backgroundSize: theme.bgSize || 'cover',
|
|
240
|
+
backgroundPosition: theme.bgPos || 'center',
|
|
241
|
+
backgroundRepeat: theme.bgRepeat || 'no-repeat',
|
|
242
|
+
} : {}),
|
|
227
243
|
padding: padY + 'px ' + padX + 'px',
|
|
228
244
|
boxSizing: 'border-box', maxWidth: '100%', display: 'flex', justifyContent: 'center',
|
|
229
245
|
transition: 'background 0.2s, padding 0.22s cubic-bezier(0.22,0.61,0.36,1)',
|
|
@@ -310,7 +326,20 @@ export function renderDoc(core, live) {
|
|
|
310
326
|
}
|
|
311
327
|
: { display: 'block' })
|
|
312
328
|
: colsWrap(r.props));
|
|
329
|
+
/*
|
|
330
|
+
* What this column measures in a sent email, in px. Word cannot resolve a
|
|
331
|
+
* percentage width on an image (it falls back to the file's intrinsic
|
|
332
|
+
* size, which is how a 1200px hero tore the layout open in Outlook), so
|
|
333
|
+
* the image renderer needs a real number to put in a `width` attribute.
|
|
334
|
+
* Approximate by construction -- cell gutters and column padding are not
|
|
335
|
+
* modelled here -- but an approximate cap is the whole difference between
|
|
336
|
+
* a scaled image and a 1200px one.
|
|
337
|
+
*/
|
|
338
|
+
const rowPadX = (r.props.pl ?? r.props.px ?? 0) + (r.props.pr ?? r.props.px ?? 0);
|
|
339
|
+
const gapPx = (r.props.gap || 0) * Math.max(0, r.cols.length - 1);
|
|
340
|
+
const innerPx = Math.max(40, (Number(theme.width) || 620) - rowPadX - gapPx);
|
|
313
341
|
r.cols.forEach((c, ci) => {
|
|
342
|
+
const colPx = Math.max(20, Math.round(innerPx * ((c.span || 100) / 100)));
|
|
314
343
|
const colLines = [];
|
|
315
344
|
const items = [];
|
|
316
345
|
c.blocks.forEach((b, bi) => {
|
|
@@ -356,7 +385,7 @@ export function renderDoc(core, live) {
|
|
|
356
385
|
// few pixels from the RTE's own controls. The RTE stands in for it for
|
|
357
386
|
// the duration of the edit; it comes back on blur (still selected).
|
|
358
387
|
if (bSel && ctx.editingId !== b.id) bWrap.appendChild(toolbar(core, b.id, 'block', DEF(b.type).code));
|
|
359
|
-
bWrap.appendChild(blockBody(b, theme, live, ctx));
|
|
388
|
+
bWrap.appendChild(blockBody(b, theme, live, ctx, colPx));
|
|
360
389
|
items.push(bWrap);
|
|
361
390
|
});
|
|
362
391
|
let colTracker;
|
|
@@ -379,9 +408,9 @@ export function renderDoc(core, live) {
|
|
|
379
408
|
// background must stop at the column's visual edge, not bleed across
|
|
380
409
|
// the gutter. `host` is also what the drag listeners and
|
|
381
410
|
// `indexFromPoint` must use -- the block slots are its children.
|
|
382
|
-
const styled = c.bg || c.border || c.radius || c.padY || c.padX;
|
|
411
|
+
const styled = c.bg || c.bgImage || c.border || c.radius || c.padY || c.padX;
|
|
383
412
|
const host = styled
|
|
384
|
-
? el('div', {
|
|
413
|
+
? el('div', { ...colBg(c), border: c.border ? c.border + 'px ' + (c.borderStyle || 'solid') + ' ' + (c.lineColor || '#e2e2e5') : '0', borderRadius: (c.radius || 0) + 'px', padding: (c.padY || 0) + 'px ' + (c.padX || 0) + 'px', height: '100%', boxSizing: 'border-box' })
|
|
385
414
|
: colEl;
|
|
386
415
|
if (live) {
|
|
387
416
|
host.addEventListener('dragover', (e) => {
|
package/src/render/fields.js
CHANGED
|
@@ -447,6 +447,25 @@ export function renderField(f) {
|
|
|
447
447
|
return wrap;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
/*
|
|
451
|
+
* A note is prose, not a control: no label row, no focus key, nothing to
|
|
452
|
+
* commit. It sits in the flow where the panel put it, tinted by tone --
|
|
453
|
+
* amber for a warning the author should act on before sending, muted for
|
|
454
|
+
* an aside. The left rule rather than a filled panel keeps it from reading
|
|
455
|
+
* as a field the user failed to fill in.
|
|
456
|
+
*/
|
|
457
|
+
if (f.isNote) {
|
|
458
|
+
const warn = f.tone !== 'info';
|
|
459
|
+
wrap.appendChild(el('div', {
|
|
460
|
+
fontFamily: 'var(--ed-font)', fontSize: '11px', lineHeight: '1.5',
|
|
461
|
+
color: warn ? 'var(--ed-text)' : 'var(--ed-muted)',
|
|
462
|
+
background: warn ? 'rgba(214,141,32,0.10)' : 'var(--ed-panel-2)',
|
|
463
|
+
borderLeft: '2px solid ' + (warn ? 'rgba(214,141,32,0.85)' : 'var(--ed-line)'),
|
|
464
|
+
padding: '7px 9px', boxSizing: 'border-box',
|
|
465
|
+
}, { text: f.label, class: 'mc-field-note', role: warn ? 'status' : undefined }));
|
|
466
|
+
return wrap;
|
|
467
|
+
}
|
|
468
|
+
|
|
450
469
|
// No isHead branch: heads never reach renderField -- renderFieldCards
|
|
451
470
|
// consumes them as card kickers. A field list rendered without the card
|
|
452
471
|
// grouper would drop its headings, which is the loud failure we want.
|
package/src/render/screenshot.js
CHANGED
|
@@ -97,11 +97,34 @@ async function inlineExternalImages(root) {
|
|
|
97
97
|
));
|
|
98
98
|
});
|
|
99
99
|
root.querySelectorAll('[style]').forEach((el) => {
|
|
100
|
-
|
|
100
|
+
/*
|
|
101
|
+
* Only the `url(...)` layer is rewritten, never the whole declaration.
|
|
102
|
+
* A tinted section paints `linear-gradient(rgba(20,22,24,α),…),url(…)`
|
|
103
|
+
* (core/layout-style.js), and replacing the property wholesale dropped
|
|
104
|
+
* the gradient -- so every darkened hero came out of the screenshot
|
|
105
|
+
* lighter than the canvas it was captured from. A fetch that fails
|
|
106
|
+
* leaves a transparent pixel in the layer's place rather than `none`,
|
|
107
|
+
* which would be invalid inside a layer list and would take the tint
|
|
108
|
+
* down with it a second way.
|
|
109
|
+
*/
|
|
110
|
+
const decl = el.style.backgroundImage || '';
|
|
111
|
+
const m = decl.match(/url\(["']?(?!data:)([^"')]+)["']?\)/);
|
|
101
112
|
if (!m) return;
|
|
102
113
|
jobs.push(toDataUri(m[1]).then(
|
|
103
|
-
(uri) => { el.style.backgroundImage = 'url("' + uri + '")'; },
|
|
104
|
-
() => { el.style.backgroundImage = '
|
|
114
|
+
(uri) => { el.style.backgroundImage = decl.replace(m[0], 'url("' + uri + '")'); },
|
|
115
|
+
() => { el.style.backgroundImage = decl.replace(m[0], 'url("' + BLANK_PIXEL + '")'); },
|
|
116
|
+
));
|
|
117
|
+
});
|
|
118
|
+
// The legacy `background` attribute -- what an imported Outlook-first
|
|
119
|
+
// template, and the exporter's own rows, carry alongside the CSS. Read only
|
|
120
|
+
// where no CSS image was inlined above, so the two never fetch twice.
|
|
121
|
+
root.querySelectorAll('[background]').forEach((el) => {
|
|
122
|
+
const src = el.getAttribute('background') || '';
|
|
123
|
+
if (!src || src.startsWith('data:')) return;
|
|
124
|
+
if (/url\(/.test(el.style.backgroundImage || '')) return;
|
|
125
|
+
jobs.push(toDataUri(src).then(
|
|
126
|
+
(uri) => { el.setAttribute('background', uri); el.style.backgroundImage = 'url("' + uri + '")'; },
|
|
127
|
+
() => { el.removeAttribute('background'); },
|
|
105
128
|
));
|
|
106
129
|
});
|
|
107
130
|
await Promise.all(jobs);
|