@seliseblocks/mailcraft 0.2.11 → 0.2.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/mailcraft",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Framework-agnostic drag-and-drop email template editor, packaged as a zero-dependency Web Component.",
5
5
  "license": "MIT",
6
6
  "author": "SELISE Digital Platforms",
@@ -423,11 +423,48 @@ export class EditorCore {
423
423
  if (this.onFormatChange) this.onFormatChange();
424
424
  };
425
425
  document.addEventListener('selectionchange', this.onSelect);
426
+ /**
427
+ * Click-outside fallback for closing the RTE toolbar. The blur path
428
+ * (`blockCtx.onBlur`, canvas.js) only runs if the edited block still holds
429
+ * focus at the moment of the outside press -- but several toolbar controls
430
+ * legitimately move focus to themselves (the Text style / Merge Tags
431
+ * selects, the color inputs, the link popover's href field). Dismiss one
432
+ * of those without committing and no block blur can ever fire again, so
433
+ * `state.editing` -- and the toolbar -- stayed open no matter where the
434
+ * user clicked. A completed click whose composed path contains neither the
435
+ * edited block nor the toolbar closes the edit explicitly.
436
+ *
437
+ * `click`, deliberately not `pointerdown`: by click time the press's
438
+ * native blur/focus transition has fully settled, so this never rebuilds
439
+ * the canvas mid-gesture (the dropped-click problem documented in
440
+ * `blockCtx.onFocus`, canvas.js) and never races focus-preserve into
441
+ * refocusing -- and thereby reopening -- the block it just closed. It also
442
+ * ignores scrollbar drags, which emit no click.
443
+ */
444
+ this.onOutsideClick = (e) => {
445
+ if (!this.state.editing || this.rendering) return;
446
+ // A drag-selection that starts inside the block but ends outside it
447
+ // fires its click on a common ancestor -- but the block keeps focus
448
+ // through such a drag, while a genuine outside press blurs it first
449
+ // (and the blur pipeline has then already handled the close).
450
+ const active = this.exportRoot && this.exportRoot.activeElement;
451
+ if (active && active === this.editEl) return;
452
+ const path = e.composedPath ? e.composedPath() : [];
453
+ for (const n of path) {
454
+ if (!n || n.nodeType !== 1) continue;
455
+ if (n.getAttribute && n.getAttribute('data-mc-content') === this.state.editing) return;
456
+ if (n.hasAttribute && n.hasAttribute('data-rte-root')) return;
457
+ }
458
+ this.closeEditing();
459
+ };
460
+ if (this.exportRoot) this.exportRoot.addEventListener('click', this.onOutsideClick);
426
461
  }
427
462
 
428
463
  unmountKeyboard() {
429
464
  window.removeEventListener('keydown', this.onKey);
430
465
  document.removeEventListener('selectionchange', this.onSelect);
466
+ if (this.exportRoot && this.onOutsideClick) this.exportRoot.removeEventListener('click', this.onOutsideClick);
467
+ this.onOutsideClick = null;
431
468
  }
432
469
 
433
470
  unmount() {
@@ -450,6 +487,30 @@ export class EditorCore {
450
487
 
451
488
  // ---- rich text editing ----------------------------------------------
452
489
 
490
+ /**
491
+ * Explicitly ends the active rich-text edit: commits the live content the
492
+ * way `blockCtx.onBlur` (canvas.js) would, then clears `editing`/`linkDraft`.
493
+ * Used by the click-outside fallback (`mountKeyboard`), which fires exactly
494
+ * when the block no longer holds focus, so no blur will ever arrive to do
495
+ * this. Any blur this close itself provokes is deliberately swallowed via
496
+ * `rteActive`: the commit below is the single commit path -- letting onBlur
497
+ * also run would compare against the same `editOriginal` and push a second
498
+ * undo entry for the same change.
499
+ */
500
+ closeEditing() {
501
+ const id = this.state.editing;
502
+ if (!id) return;
503
+ const elNode = this.editEl;
504
+ const val = elNode && elNode.isConnected && this.editKey
505
+ ? (this.editPlain ? elNode.textContent : elNode.innerHTML)
506
+ : null;
507
+ this.rteActive = true;
508
+ if (elNode && this.exportRoot && this.exportRoot.activeElement === elNode) elNode.blur();
509
+ this.rteActive = false;
510
+ if (val !== null && val !== this.editOriginal) this.setProp(id, this.editKey, val);
511
+ if (this.state.editing === id) this.setState({ editing: null, linkDraft: null });
512
+ }
513
+
453
514
  exec(cmd, arg) {
454
515
  this.rteActive = true;
455
516
  try {
@@ -1,83 +1,83 @@
1
- import { EN as ENBase } from './en.js';
2
-
3
- /**
4
- * Builds a translator. `overrides` is whatever a host passes as `.messages`
5
- * on the element -- a host's own table, an imported locale, or both merged
6
- * via `defineMessages` below.
7
- *
8
- * Three deliberate properties:
9
- * 1. English always resolves. A locale is an overlay, never a replacement,
10
- * so a partial or missing translation shows English rather than a gap.
11
- * 2. Params interpolate `{name}`.
12
- * 3. A truly missing key (not in `overrides`, not in `EN`) renders as the
13
- * key itself, not an empty string -- a visible `toast.deleted` in the UI
14
- * is obviously wrong and names the exact key to add, where blank text
15
- * just looks like a broken build.
16
- */
17
- export function createTranslator(overrides) {
18
- const table = overrides || {};
19
- return function t(key, params) {
20
- const template = table[key] ?? ENBase[key] ?? key;
21
- if (!params) return template;
22
- return template.replace(/\{(\w+)\}/g, (whole, name) => (name in params ? String(params[name]) : whole));
23
- };
24
- }
25
-
26
- /** Merges a locale over a base, for a host assembling its own table -- the documented way to combine a shipped locale with a few product-specific overrides. */
27
- export function defineMessages(base, overrides) {
28
- return Object.assign({}, base, overrides);
29
- }
30
-
31
- /** Keys in `base` that `locale` does not translate. What a translator has left to do. */
32
- export function missingKeys(locale, base) {
33
- const source = base || ENBase;
34
- return Object.keys(source).filter((key) => locale[key] === undefined).sort();
35
- }
36
-
37
- /**
38
- * Every locale that ships, for a host building a language switcher.
39
- * Metadata only -- no message tables -- so listing the locales never pulls
40
- * every translation file into a consumer's bundle; a host deep-imports the
41
- * one it wants, e.g. `mailcraft-editor/src/core/i18n/bn.js`.
42
- */
43
- export const LOCALES = [
44
- { tag: 'en', name: 'English' },
45
- { tag: 'ar', name: 'Arabic', rtl: true },
46
- { tag: 'bn', name: 'Bangla' },
47
- { tag: 'dz', name: 'Dzongkha' },
48
- { tag: 'bg', name: 'Bulgarian' },
49
- { tag: 'ca', name: 'Catalan' },
50
- { tag: 'cs', name: 'Czech' },
51
- { tag: 'da', name: 'Danish' },
52
- { tag: 'de', name: 'German' },
53
- { tag: 'de-CH', name: 'Swiss German' },
54
- { tag: 'el', name: 'Greek' },
55
- { tag: 'es', name: 'Spanish' },
56
- { tag: 'et', name: 'Estonian' },
57
- { tag: 'fi', name: 'Finnish' },
58
- { tag: 'fr', name: 'French' },
59
- { tag: 'hr', name: 'Croatian' },
60
- { tag: 'hu', name: 'Hungarian' },
61
- { tag: 'it', name: 'Italian' },
62
- { tag: 'lt', name: 'Lithuanian' },
63
- { tag: 'lv', name: 'Latvian' },
64
- { tag: 'nb', name: 'Norwegian Bokmål' },
65
- { tag: 'nl', name: 'Dutch' },
66
- { tag: 'pl', name: 'Polish' },
67
- { tag: 'pt', name: 'Portuguese' },
68
- { tag: 'ro', name: 'Romanian' },
69
- { tag: 'ru', name: 'Russian' },
70
- { tag: 'sk', name: 'Slovak' },
71
- { tag: 'sl', name: 'Slovenian' },
72
- { tag: 'sv', name: 'Swedish' },
73
- { tag: 'tr', name: 'Turkish' },
74
- { tag: 'uk', name: 'Ukrainian' },
75
- ];
76
-
77
- /** `true` when the tag is written right to left. Metadata only -- `dir` is still what actually flips the layout. */
78
- export function isRtl(tag) {
79
- const entry = LOCALES.find((l) => l.tag === tag);
80
- return entry ? entry.rtl === true : false;
81
- }
82
-
83
- export { EN, MESSAGE_KEYS } from './en.js';
1
+ import { EN as ENBase } from './en.js';
2
+
3
+ /**
4
+ * Builds a translator. `overrides` is whatever a host passes as `.messages`
5
+ * on the element -- a host's own table, an imported locale, or both merged
6
+ * via `defineMessages` below.
7
+ *
8
+ * Three deliberate properties:
9
+ * 1. English always resolves. A locale is an overlay, never a replacement,
10
+ * so a partial or missing translation shows English rather than a gap.
11
+ * 2. Params interpolate `{name}`.
12
+ * 3. A truly missing key (not in `overrides`, not in `EN`) renders as the
13
+ * key itself, not an empty string -- a visible `toast.deleted` in the UI
14
+ * is obviously wrong and names the exact key to add, where blank text
15
+ * just looks like a broken build.
16
+ */
17
+ export function createTranslator(overrides) {
18
+ const table = overrides || {};
19
+ return function t(key, params) {
20
+ const template = table[key] ?? ENBase[key] ?? key;
21
+ if (!params) return template;
22
+ return template.replace(/\{(\w+)\}/g, (whole, name) => (name in params ? String(params[name]) : whole));
23
+ };
24
+ }
25
+
26
+ /** Merges a locale over a base, for a host assembling its own table -- the documented way to combine a shipped locale with a few product-specific overrides. */
27
+ export function defineMessages(base, overrides) {
28
+ return Object.assign({}, base, overrides);
29
+ }
30
+
31
+ /** Keys in `base` that `locale` does not translate. What a translator has left to do. */
32
+ export function missingKeys(locale, base) {
33
+ const source = base || ENBase;
34
+ return Object.keys(source).filter((key) => locale[key] === undefined).sort();
35
+ }
36
+
37
+ /**
38
+ * Every locale that ships, for a host building a language switcher.
39
+ * Metadata only -- no message tables -- so listing the locales never pulls
40
+ * every translation file into a consumer's bundle; a host deep-imports the
41
+ * one it wants, e.g. `mailcraft-editor/src/core/i18n/bn.js`.
42
+ */
43
+ export const LOCALES = [
44
+ { tag: 'en', name: 'English' },
45
+ { tag: 'ar', name: 'Arabic', rtl: true },
46
+ { tag: 'bn', name: 'Bangla' },
47
+ { tag: 'dz', name: 'Dzongkha' },
48
+ { tag: 'bg', name: 'Bulgarian' },
49
+ { tag: 'ca', name: 'Catalan' },
50
+ { tag: 'cs', name: 'Czech' },
51
+ { tag: 'da', name: 'Danish' },
52
+ { tag: 'de', name: 'German' },
53
+ { tag: 'de-CH', name: 'Swiss German' },
54
+ { tag: 'el', name: 'Greek' },
55
+ { tag: 'es', name: 'Spanish' },
56
+ { tag: 'et', name: 'Estonian' },
57
+ { tag: 'fi', name: 'Finnish' },
58
+ { tag: 'fr', name: 'French' },
59
+ { tag: 'hr', name: 'Croatian' },
60
+ { tag: 'hu', name: 'Hungarian' },
61
+ { tag: 'it', name: 'Italian' },
62
+ { tag: 'lt', name: 'Lithuanian' },
63
+ { tag: 'lv', name: 'Latvian' },
64
+ { tag: 'nb', name: 'Norwegian Bokmål' },
65
+ { tag: 'nl', name: 'Dutch' },
66
+ { tag: 'pl', name: 'Polish' },
67
+ { tag: 'pt', name: 'Portuguese' },
68
+ { tag: 'ro', name: 'Romanian' },
69
+ { tag: 'ru', name: 'Russian' },
70
+ { tag: 'sk', name: 'Slovak' },
71
+ { tag: 'sl', name: 'Slovenian' },
72
+ { tag: 'sv', name: 'Swedish' },
73
+ { tag: 'tr', name: 'Turkish' },
74
+ { tag: 'uk', name: 'Ukrainian' },
75
+ ];
76
+
77
+ /** `true` when the tag is written right to left. Metadata only -- `dir` is still what actually flips the layout. */
78
+ export function isRtl(tag) {
79
+ const entry = LOCALES.find((l) => l.tag === tag);
80
+ return entry ? entry.rtl === true : false;
81
+ }
82
+
83
+ export { EN, MESSAGE_KEYS } from './en.js';
package/src/core/ids.js CHANGED
@@ -1 +1 @@
1
- export const uid = () => Math.random().toString(36).slice(2, 9);
1
+ export const uid = () => Math.random().toString(36).slice(2, 9);
@@ -1,46 +1,46 @@
1
- /**
2
- * Pure style-computation functions shared by the live renderer and the export
3
- * builder, ported verbatim. Objects use camelCase keys so they can be applied
4
- * directly via `Object.assign(el.style, obj)`.
5
- */
6
-
7
- import { cssUrl } from './sanitize.js';
8
- export function pad(p) {
9
- return (p.py || 0) + 'px ' + (p.px || 0) + 'px';
10
- }
11
-
12
- export function boxStyle(p) {
13
- const on = (key) => p[key] !== false;
14
- const side = (key) => (p.bBorder && on(key) ? p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5') : '0');
15
- return {
16
- background: p.bBg || 'transparent',
17
- borderTop: side('bTop'), borderRight: side('bRight'), borderBottom: side('bBottom'), borderLeft: side('bLeft'),
18
- borderRadius: (p.bRadius || 0) + 'px',
19
- padding: (p.bPad || 0) + 'px',
20
- };
21
- }
22
-
23
- export function boxCss(p) {
24
- const bits = [];
25
- if (p.bBg) bits.push('background:' + p.bBg);
26
- if (p.bBorder) {
27
- const value = p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5');
28
- const sides = { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
29
- if (sides.top && sides.right && sides.bottom && sides.left) bits.push('border:' + value);
30
- else Object.keys(sides).filter((key) => sides[key]).forEach((key) => bits.push('border-' + key + ':' + value));
31
- }
32
- if (p.bRadius) bits.push('border-radius:' + p.bRadius + 'px');
33
- if (p.bPad) bits.push('padding:' + p.bPad + 'px');
34
- return bits.length ? bits.join(';') + ';' : 'margin:0';
35
- }
36
-
37
- /** A row's effective padding as a four-value CSS shorthand. `pt/pb/pl/pr` are optional per-side overrides (set by the inspector's "Per-side padding" split, or by the importer for asymmetric source padding); wherever a side is absent it follows the linked `py`/`px` pair, so documents that never split keep behaving exactly as before. */
1
+ /**
2
+ * Pure style-computation functions shared by the live renderer and the export
3
+ * builder, ported verbatim. Objects use camelCase keys so they can be applied
4
+ * directly via `Object.assign(el.style, obj)`.
5
+ */
6
+
7
+ import { cssUrl } from './sanitize.js';
8
+ export function pad(p) {
9
+ return (p.py || 0) + 'px ' + (p.px || 0) + 'px';
10
+ }
11
+
12
+ export function boxStyle(p) {
13
+ const on = (key) => p[key] !== false;
14
+ const side = (key) => (p.bBorder && on(key) ? p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5') : '0');
15
+ return {
16
+ background: p.bBg || 'transparent',
17
+ borderTop: side('bTop'), borderRight: side('bRight'), borderBottom: side('bBottom'), borderLeft: side('bLeft'),
18
+ borderRadius: (p.bRadius || 0) + 'px',
19
+ padding: (p.bPad || 0) + 'px',
20
+ };
21
+ }
22
+
23
+ export function boxCss(p) {
24
+ const bits = [];
25
+ if (p.bBg) bits.push('background:' + p.bBg);
26
+ if (p.bBorder) {
27
+ const value = p.bBorder + 'px ' + (p.bStyle || 'solid') + ' ' + (p.bLine || '#e2e2e5');
28
+ const sides = { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
29
+ if (sides.top && sides.right && sides.bottom && sides.left) bits.push('border:' + value);
30
+ else Object.keys(sides).filter((key) => sides[key]).forEach((key) => bits.push('border-' + key + ':' + value));
31
+ }
32
+ if (p.bRadius) bits.push('border-radius:' + p.bRadius + 'px');
33
+ if (p.bPad) bits.push('padding:' + p.bPad + 'px');
34
+ return bits.length ? bits.join(';') + ';' : 'margin:0';
35
+ }
36
+
37
+ /** A row's effective padding as a four-value CSS shorthand. `pt/pb/pl/pr` are optional per-side overrides (set by the inspector's "Per-side padding" split, or by the importer for asymmetric source padding); wherever a side is absent it follows the linked `py`/`px` pair, so documents that never split keep behaving exactly as before. */
38
38
  export function rowPad(p) {
39
- const t = p.pt ?? p.py ?? 0;
40
- const b = p.pb ?? p.py ?? 0;
41
- const l = p.pl ?? p.px ?? 0;
42
- const r = p.pr ?? p.px ?? 0;
43
- return t + 'px ' + r + 'px ' + b + 'px ' + l + 'px';
39
+ const t = p.pt ?? p.py ?? 0;
40
+ const b = p.pb ?? p.py ?? 0;
41
+ const l = p.pl ?? p.px ?? 0;
42
+ const r = p.pr ?? p.px ?? 0;
43
+ return t + 'px ' + r + 'px ' + b + 'px ' + l + 'px';
44
44
  }
45
45
 
46
46
  /** A row's outside spacing in CSS clockwise order, with the old vertical `my` value as a saved-document fallback. Empty horizontal margins can remain `auto` in the live canvas so the advanced max-width control stays centered. */
@@ -52,64 +52,64 @@ export function rowMargin(p, centerEmpty) {
52
52
  const emptyHorizontal = centerEmpty && !r && !l;
53
53
  return t + 'px ' + (emptyHorizontal ? 'auto' : r + 'px') + ' ' + b + 'px ' + (emptyHorizontal ? 'auto' : l + 'px');
54
54
  }
55
-
56
- /** Which sides a row's border draws on. Sides default ON (`!== false`) so documents saved before per-side toggles existed keep their full border. */
57
- export function rowBorderSides(p) {
58
- return { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
59
- }
60
-
61
- /** The row border as an inline-CSS string (export path). Empty when the width is 0 or every side is toggled off. */
62
- export function rowBorderCss(p) {
63
- if (!p.border) return '';
64
- const s = rowBorderSides(p);
65
- const value = p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5');
66
- if (s.top && s.right && s.bottom && s.left) return 'border:' + value + ';';
67
- return ['top', 'right', 'bottom', 'left'].filter((k) => s[k]).map((k) => 'border-' + k + ':' + value + ';').join('');
68
- }
69
-
70
- export function rowBg(p) {
71
- const ov = (p.overlay || 0) / 100;
72
- const layers = [];
73
- if (p.bgImage && ov) layers.push('linear-gradient(rgba(20,22,24,' + ov + '),rgba(20,22,24,' + ov + '))');
74
- if (p.bgImage) layers.push('url("' + cssUrl(p.bgImage) + '")');
75
- const s = rowBorderSides(p);
76
- const side = (on) => (p.border && on ? p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5') : '0');
77
- return {
78
- backgroundColor: p.bg || 'transparent',
79
- backgroundImage: layers.length ? layers.join(',') : 'none',
80
- backgroundSize: p.bgSize || 'cover',
81
- backgroundPosition: p.bgPos || 'center',
82
- backgroundRepeat: p.bgRepeat || 'no-repeat',
83
- borderTop: side(s.top),
84
- borderRight: side(s.right),
85
- borderBottom: side(s.bottom),
86
- borderLeft: side(s.left),
87
- borderRadius: (p.radius || 0) + 'px',
88
- // A raw CSS string, not a boolean: imports keep the source's exact
89
- // shadow; the inspector toggle writes/clears a standard one.
90
- boxShadow: p.shadow || 'none',
91
- maxWidth: (p.maxW || 100) + '%',
55
+
56
+ /** Which sides a row's border draws on. Sides default ON (`!== false`) so documents saved before per-side toggles existed keep their full border. */
57
+ export function rowBorderSides(p) {
58
+ return { top: p.bTop !== false, right: p.bRight !== false, bottom: p.bBottom !== false, left: p.bLeft !== false };
59
+ }
60
+
61
+ /** The row border as an inline-CSS string (export path). Empty when the width is 0 or every side is toggled off. */
62
+ export function rowBorderCss(p) {
63
+ if (!p.border) return '';
64
+ const s = rowBorderSides(p);
65
+ const value = p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5');
66
+ if (s.top && s.right && s.bottom && s.left) return 'border:' + value + ';';
67
+ return ['top', 'right', 'bottom', 'left'].filter((k) => s[k]).map((k) => 'border-' + k + ':' + value + ';').join('');
68
+ }
69
+
70
+ export function rowBg(p) {
71
+ const ov = (p.overlay || 0) / 100;
72
+ const layers = [];
73
+ if (p.bgImage && ov) layers.push('linear-gradient(rgba(20,22,24,' + ov + '),rgba(20,22,24,' + ov + '))');
74
+ if (p.bgImage) layers.push('url("' + cssUrl(p.bgImage) + '")');
75
+ const s = rowBorderSides(p);
76
+ const side = (on) => (p.border && on ? p.border + 'px ' + (p.borderStyle || 'solid') + ' ' + (p.lineColor || '#e2e2e5') : '0');
77
+ return {
78
+ backgroundColor: p.bg || 'transparent',
79
+ backgroundImage: layers.length ? layers.join(',') : 'none',
80
+ backgroundSize: p.bgSize || 'cover',
81
+ backgroundPosition: p.bgPos || 'center',
82
+ backgroundRepeat: p.bgRepeat || 'no-repeat',
83
+ borderTop: side(s.top),
84
+ borderRight: side(s.right),
85
+ borderBottom: side(s.bottom),
86
+ borderLeft: side(s.left),
87
+ borderRadius: (p.radius || 0) + 'px',
88
+ // A raw CSS string, not a boolean: imports keep the source's exact
89
+ // shadow; the inspector toggle writes/clears a standard one.
90
+ boxShadow: p.shadow || 'none',
91
+ maxWidth: (p.maxW || 100) + '%',
92
92
  margin: rowMargin(p, true),
93
93
  };
94
94
  }
95
-
96
- export function colsWrap(p) {
97
- const gap = p.gap || 0;
98
- if (p.layout === 'grid') return { display: 'grid', gridTemplateColumns: 'repeat(' + (p.gridCols || 2) + ', minmax(0, 1fr))', gap: gap + 'px' };
99
- if (p.layout === 'flex') {
100
- return {
101
- display: 'flex', flexDirection: p.flexDir || 'row', justifyContent: p.justify || 'flex-start',
102
- alignItems: p.alignItems || 'stretch', flexWrap: p.wrap ? 'wrap' : 'nowrap', gap: gap + 'px',
103
- };
104
- }
105
- return { display: 'flex', alignItems: 'stretch', margin: '0 ' + (-gap / 2) + 'px' };
106
- }
107
-
108
- export function colStyle(p, c) {
109
- if (p.layout === 'grid') return { minWidth: 0 };
110
- if (p.layout === 'flex') return { flex: (p.flexDir || 'row').indexOf('column') === 0 ? '0 0 auto' : c.span + ' 1 auto', minWidth: 0 };
111
- return {
112
- flex: c.span + ' 1 0%', minWidth: 0, padding: '0 ' + (p.gap || 0) / 2 + 'px',
113
- alignSelf: p.valign === 'middle' ? 'center' : (p.valign === 'bottom' ? 'flex-end' : 'flex-start'),
114
- };
115
- }
95
+
96
+ export function colsWrap(p) {
97
+ const gap = p.gap || 0;
98
+ if (p.layout === 'grid') return { display: 'grid', gridTemplateColumns: 'repeat(' + (p.gridCols || 2) + ', minmax(0, 1fr))', gap: gap + 'px' };
99
+ if (p.layout === 'flex') {
100
+ return {
101
+ display: 'flex', flexDirection: p.flexDir || 'row', justifyContent: p.justify || 'flex-start',
102
+ alignItems: p.alignItems || 'stretch', flexWrap: p.wrap ? 'wrap' : 'nowrap', gap: gap + 'px',
103
+ };
104
+ }
105
+ return { display: 'flex', alignItems: 'stretch', margin: '0 ' + (-gap / 2) + 'px' };
106
+ }
107
+
108
+ export function colStyle(p, c) {
109
+ if (p.layout === 'grid') return { minWidth: 0 };
110
+ if (p.layout === 'flex') return { flex: (p.flexDir || 'row').indexOf('column') === 0 ? '0 0 auto' : c.span + ' 1 auto', minWidth: 0 };
111
+ return {
112
+ flex: c.span + ' 1 0%', minWidth: 0, padding: '0 ' + (p.gap || 0) / 2 + 'px',
113
+ alignSelf: p.valign === 'middle' ? 'center' : (p.valign === 'bottom' ? 'flex-end' : 'flex-start'),
114
+ };
115
+ }
package/src/core/parse.js CHANGED
@@ -1,10 +1,10 @@
1
- export function parseItems(s) {
2
- return String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).map((l) => {
3
- const i = l.indexOf('|');
4
- return i < 0 ? { label: l, href: '#' } : { label: l.slice(0, i).trim(), href: l.slice(i + 1).trim() };
5
- });
6
- }
7
-
8
- export function cellsOf(p) {
9
- return String(p.data || '').split('\n').filter((l) => l.trim()).map((l) => l.split('|').map((c) => c.trim()));
10
- }
1
+ export function parseItems(s) {
2
+ return String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).map((l) => {
3
+ const i = l.indexOf('|');
4
+ return i < 0 ? { label: l, href: '#' } : { label: l.slice(0, i).trim(), href: l.slice(i + 1).trim() };
5
+ });
6
+ }
7
+
8
+ export function cellsOf(p) {
9
+ return String(p.data || '').split('\n').filter((l) => l.trim()).map((l) => l.split('|').map((c) => c.trim()));
10
+ }
@@ -1,15 +1,15 @@
1
- /** Data-URI placeholder image generator, ported verbatim from the original. */
2
- function enc(s) {
3
- return encodeURIComponent(s).replace(/\(/g, '%28').replace(/\)/g, '%29');
4
- }
5
-
6
- export function PH(label, w, ht) {
7
- return 'data:image/svg+xml;utf8,' + enc(
8
- '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">' +
9
- '<defs><pattern id="s" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">' +
10
- '<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>' +
11
- '<rect width="100%" height="100%" fill="url(#s)"/>' +
12
- '<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>' +
13
- '<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="' + Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
14
- );
15
- }
1
+ /** Data-URI placeholder image generator, ported verbatim from the original. */
2
+ function enc(s) {
3
+ return encodeURIComponent(s).replace(/\(/g, '%28').replace(/\)/g, '%29');
4
+ }
5
+
6
+ export function PH(label, w, ht) {
7
+ return 'data:image/svg+xml;utf8,' + enc(
8
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' + w + '" height="' + ht + '">' +
9
+ '<defs><pattern id="s" width="9" height="9" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">' +
10
+ '<rect width="9" height="9" fill="#ececed"/><line x1="0" y1="0" x2="0" y2="9" stroke="#cfd3d8" stroke-width="3"/></pattern></defs>' +
11
+ '<rect width="100%" height="100%" fill="url(#s)"/>' +
12
+ '<rect x="0.5" y="0.5" width="' + (w - 1) + '" height="' + (ht - 1) + '" fill="none" stroke="#9aa2ab"/>' +
13
+ '<text x="50%" y="50%" dy="4" text-anchor="middle" font-family="ui-monospace,monospace" font-size="' + Math.max(11, Math.round(w / 40)) + '" fill="#5b6672">' + label + '</text></svg>',
14
+ );
15
+ }
@@ -1,11 +1,11 @@
1
- export const DEFAULT_VARS = 'first_name\nlast_name\nemail\ncompany\ncity\norder_id\nplan\ndiscount\nunsubscribe_url';
2
- export const TOKEN = (t) => '{' + '{ ' + t + ' }' + '}';
3
-
4
- /** Variables are supplied by the host application -- the editor only ever shows the tokens, never a substituted value. */
5
- export function vars(raw) {
6
- const list = Array.isArray(raw) ? raw : String(raw == null ? DEFAULT_VARS : raw).split(/[\n,]/);
7
- return list.map((v) => String(v).trim().replace(/^\{\{\s*|\s*\}\}$/g, '')).filter(Boolean);
8
- }
9
-
10
- /** Which prop field a merge tag lands in when inserted from the Data tab, keyed by the selected block's type. */
11
- export const INSERT_KEYS = { text: 'html', heading: 'text', button: 'label', html: 'code', codeblock: 'code', quote: 'text', list: 'items', table: 'data' };
1
+ export const DEFAULT_VARS = 'first_name\nlast_name\nemail\ncompany\ncity\norder_id\nplan\ndiscount\nunsubscribe_url';
2
+ export const TOKEN = (t) => '{' + '{ ' + t + ' }' + '}';
3
+
4
+ /** Variables are supplied by the host application -- the editor only ever shows the tokens, never a substituted value. */
5
+ export function vars(raw) {
6
+ const list = Array.isArray(raw) ? raw : String(raw == null ? DEFAULT_VARS : raw).split(/[\n,]/);
7
+ return list.map((v) => String(v).trim().replace(/^\{\{\s*|\s*\}\}$/g, '')).filter(Boolean);
8
+ }
9
+
10
+ /** Which prop field a merge tag lands in when inserted from the Data tab, keyed by the selected block's type. */
11
+ export const INSERT_KEYS = { text: 'html', heading: 'text', button: 'label', html: 'code', codeblock: 'code', quote: 'text', list: 'items', table: 'data' };