tess_edit 0.1.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +225 -0
- data/Rakefile +3 -0
- data/app/assets/tailwind/tess_edit/engine.css +10 -0
- data/app/controllers/tess_edit/application_controller.rb +5 -0
- data/app/controllers/tess_edit/compilations_controller.rb +10 -0
- data/app/helpers/tess_edit/application_helper.rb +4 -0
- data/app/javascript/tess_edit/blocks/builtin.js +26 -0
- data/app/javascript/tess_edit/blocks/button.js +32 -0
- data/app/javascript/tess_edit/blocks/column.js +24 -0
- data/app/javascript/tess_edit/blocks/condition.js +15 -0
- data/app/javascript/tess_edit/blocks/divider.js +24 -0
- data/app/javascript/tess_edit/blocks/fields.js +42 -0
- data/app/javascript/tess_edit/blocks/hero.js +60 -0
- data/app/javascript/tess_edit/blocks/image.js +29 -0
- data/app/javascript/tess_edit/blocks/loop.js +23 -0
- data/app/javascript/tess_edit/blocks/navbar.js +49 -0
- data/app/javascript/tess_edit/blocks/raw.js +18 -0
- data/app/javascript/tess_edit/blocks/root.js +83 -0
- data/app/javascript/tess_edit/blocks/section.js +40 -0
- data/app/javascript/tess_edit/blocks/social.js +39 -0
- data/app/javascript/tess_edit/blocks/spacer.js +10 -0
- data/app/javascript/tess_edit/blocks/text.js +39 -0
- data/app/javascript/tess_edit/blocks/wrapper.js +24 -0
- data/app/javascript/tess_edit/controllers/editor_controller.js +758 -0
- data/app/javascript/tess_edit/core/document.js +192 -0
- data/app/javascript/tess_edit/core/history.js +46 -0
- data/app/javascript/tess_edit/core/lock.js +36 -0
- data/app/javascript/tess_edit/core/media.js +54 -0
- data/app/javascript/tess_edit/core/registry.js +94 -0
- data/app/javascript/tess_edit/core/starter.js +89 -0
- data/app/javascript/tess_edit/export/compile.js +28 -0
- data/app/javascript/tess_edit/export/mjml.js +116 -0
- data/app/javascript/tess_edit/export/richtext.js +111 -0
- data/app/javascript/tess_edit/index.js +21 -0
- data/app/javascript/tess_edit/ui/blocks_panel.js +44 -0
- data/app/javascript/tess_edit/ui/canvas.js +437 -0
- data/app/javascript/tess_edit/ui/code_input.js +137 -0
- data/app/javascript/tess_edit/ui/dom.js +35 -0
- data/app/javascript/tess_edit/ui/icons.js +63 -0
- data/app/javascript/tess_edit/ui/inspector.js +377 -0
- data/app/javascript/tess_edit/ui/text_editing.js +124 -0
- data/app/javascript/tess_edit/ui/tree.js +138 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +3 -0
- data/lib/tasks/tess_edit_tasks.rake +4 -0
- data/lib/tess_edit/engine.rb +18 -0
- data/lib/tess_edit/version.rb +3 -0
- data/lib/tess_edit.rb +28 -0
- data/vendor/javascript/tess_edit/tiptap.js +147 -0
- metadata +149 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
type: "raw",
|
|
3
|
+
name: "HTML",
|
|
4
|
+
icon: "code",
|
|
5
|
+
category: "content",
|
|
6
|
+
roles: ["body", "content"],
|
|
7
|
+
defaults: () => ({ data: { html: '<p style="margin:0;padding:10px 25px;">Custom HTML</p>' } }),
|
|
8
|
+
fields: [{ title: "HTML", fields: [{ key: "data.html", label: "HTML", type: "code", hint: "Copied into the email as-is. Liquid works here too." }] }],
|
|
9
|
+
render: {
|
|
10
|
+
mjml(node, { editor, context }) {
|
|
11
|
+
const html = node.data.html || ""
|
|
12
|
+
if (!editor) return `<mj-raw>${html}</mj-raw>`
|
|
13
|
+
// mj-raw output cannot carry a class, so the canvas shows it inside a text block.
|
|
14
|
+
const text = `<mj-text css-class="tess-${node.id}" padding="0px">${html}</mj-text>`
|
|
15
|
+
return context === "column" ? text : `<mj-section padding="0px"><mj-column>${text}</mj-column></mj-section>`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The root of every document: renders the whole <mjml> document, with the
|
|
2
|
+
// email's subject, preview text and theme in <mj-head>.
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
type: "root",
|
|
6
|
+
name: "Root",
|
|
7
|
+
icon: "root",
|
|
8
|
+
roles: [],
|
|
9
|
+
accepts: ["body", "wrapper"],
|
|
10
|
+
defaults: () => ({
|
|
11
|
+
attributes: { width: "600px", "background-color": "#f3f4f6" },
|
|
12
|
+
data: {
|
|
13
|
+
subject: "",
|
|
14
|
+
preheader: "",
|
|
15
|
+
breakpoint: "480px",
|
|
16
|
+
fontFamily: "Helvetica, Arial, sans-serif",
|
|
17
|
+
fontSize: "14px",
|
|
18
|
+
lineHeight: "1.6",
|
|
19
|
+
textColor: "#111827",
|
|
20
|
+
css: ""
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
fields: [
|
|
24
|
+
{
|
|
25
|
+
title: "Email",
|
|
26
|
+
fields: [
|
|
27
|
+
{ key: "data.subject", label: "Subject", type: "text" },
|
|
28
|
+
{ key: "data.preheader", label: "Preview text", type: "text", hint: "Shown after the subject in most inboxes." }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "Layout",
|
|
33
|
+
fields: [
|
|
34
|
+
{ key: "attributes.width", label: "Width", type: "size", placeholder: "600px" },
|
|
35
|
+
{ key: "data.breakpoint", label: "Breakpoint", type: "size", placeholder: "480px", hint: "Below this width columns stack." },
|
|
36
|
+
{ key: "attributes.background-color", label: "Background", type: "color" }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: "Theme",
|
|
41
|
+
fields: [
|
|
42
|
+
{ key: "data.fontFamily", label: "Font family", type: "text" },
|
|
43
|
+
{ key: "data.fontSize", label: "Font size", type: "size" },
|
|
44
|
+
{ key: "data.lineHeight", label: "Line height", type: "text" },
|
|
45
|
+
{ key: "data.textColor", label: "Text color", type: "color" }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "Custom CSS",
|
|
50
|
+
collapsed: true,
|
|
51
|
+
fields: [{ key: "data.css", label: "CSS", type: "code", language: "css", hint: "Added to the email's <style> block." }]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
render: {
|
|
55
|
+
mjml(node, { attrs, cssClass, children, indent, escapeText, escapeAttribute }) {
|
|
56
|
+
const d = node.data || {}
|
|
57
|
+
const head = []
|
|
58
|
+
if (d.subject) head.push(`<mj-title>${escapeText(d.subject)}</mj-title>`)
|
|
59
|
+
if (d.preheader) head.push(`<mj-preview>${escapeText(d.preheader)}</mj-preview>`)
|
|
60
|
+
if (d.breakpoint) head.push(`<mj-breakpoint width="${escapeAttribute(d.breakpoint)}" />`)
|
|
61
|
+
head.push(
|
|
62
|
+
[
|
|
63
|
+
"<mj-attributes>",
|
|
64
|
+
` <mj-all${attrs({ "font-family": d.fontFamily })} />`,
|
|
65
|
+
` <mj-text${attrs({ "font-size": d.fontSize, "line-height": d.lineHeight, color: d.textColor })} />`,
|
|
66
|
+
"</mj-attributes>"
|
|
67
|
+
].join("\n")
|
|
68
|
+
)
|
|
69
|
+
if (d.css) head.push(`<mj-style>\n${d.css}\n</mj-style>`)
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
"<mjml>",
|
|
73
|
+
" <mj-head>",
|
|
74
|
+
indent(head.join("\n"), 2),
|
|
75
|
+
" </mj-head>",
|
|
76
|
+
` <mj-body${attrs(node.attributes, cssClass(node))}>`,
|
|
77
|
+
indent(children(node, "body").join("\n"), 2),
|
|
78
|
+
" </mj-body>",
|
|
79
|
+
"</mjml>"
|
|
80
|
+
].join("\n")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ALIGN, background, border, padding } from "tess_edit/blocks/fields"
|
|
2
|
+
import { imageField } from "tess_edit/core/media"
|
|
3
|
+
|
|
4
|
+
const section = (createNode, widths) =>
|
|
5
|
+
createNode("section", { children: widths.map((width) => createNode("column", { attributes: width ? { width } : {} })) })
|
|
6
|
+
|
|
7
|
+
const layouts = [
|
|
8
|
+
["section-1", "1 column", "cols-1", [null]],
|
|
9
|
+
["section-2", "2 columns", "cols-2", ["50%", "50%"]],
|
|
10
|
+
["section-3", "3 columns", "cols-3", ["33.33%", "33.33%", "33.33%"]],
|
|
11
|
+
["section-4", "4 columns", "cols-4", ["25%", "25%", "25%", "25%"]],
|
|
12
|
+
["section-1-2", "1 : 2", "cols-12", ["33.33%", "66.67%"]],
|
|
13
|
+
["section-2-1", "2 : 1", "cols-21", ["66.67%", "33.33%"]]
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: "section",
|
|
18
|
+
name: "Section",
|
|
19
|
+
icon: "section",
|
|
20
|
+
roles: ["body"],
|
|
21
|
+
accepts: ["column"],
|
|
22
|
+
horizontal: true,
|
|
23
|
+
defaults: () => ({ attributes: { padding: "20px 0px" } }),
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
title: "Section",
|
|
27
|
+
fields: [
|
|
28
|
+
background,
|
|
29
|
+
imageField("attributes.background-url", "Background image", "data.background"),
|
|
30
|
+
{ key: "attributes.full-width", label: "Full width", type: "toggle", on: "full-width", off: "" },
|
|
31
|
+
{ key: "attributes.direction", label: "Column order", type: "segmented", options: [{ value: "ltr", label: "Left to right" }, { value: "rtl", label: "Right to left" }] },
|
|
32
|
+
{ key: "attributes.text-align", label: "Align", type: "segmented", options: ALIGN },
|
|
33
|
+
padding
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
border
|
|
37
|
+
],
|
|
38
|
+
palette: layouts.map(([key, label, icon, widths]) => ({ key, label, icon, category: "layout", create: ({ createNode }) => section(createNode, widths) })),
|
|
39
|
+
render: { mjml: (node, { container }) => container("mj-section", node) }
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { align, padding, typography } from "tess_edit/blocks/fields"
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
type: "social",
|
|
5
|
+
name: "Social",
|
|
6
|
+
icon: "social",
|
|
7
|
+
category: "content",
|
|
8
|
+
roles: ["content"],
|
|
9
|
+
defaults: () => ({
|
|
10
|
+
attributes: { mode: "horizontal", align: "center", "icon-size": "24px", padding: "10px 25px" },
|
|
11
|
+
data: {
|
|
12
|
+
elements: [
|
|
13
|
+
{ name: "facebook", href: "https://facebook.com/", label: "" },
|
|
14
|
+
{ name: "instagram", href: "https://instagram.com/", label: "" },
|
|
15
|
+
{ name: "linkedin", href: "https://linkedin.com/", label: "" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}),
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
title: "Social",
|
|
22
|
+
fields: [
|
|
23
|
+
{ key: "data.elements", label: "Links", type: "list", item: [{ name: "name", label: "Network", type: "select", options: ["facebook", "instagram", "linkedin", "x", "youtube", "github", "pinterest", "web"] }, { name: "href", label: "URL" }, { name: "label", label: "Label" }], newItem: { name: "web", href: "https://", label: "" } },
|
|
24
|
+
{ key: "attributes.mode", label: "Mode", type: "segmented", options: [{ value: "horizontal", label: "Horizontal" }, { value: "vertical", label: "Vertical" }] },
|
|
25
|
+
{ key: "attributes.icon-size", label: "Icon size", type: "size" },
|
|
26
|
+
{ key: "attributes.icon-padding", label: "Icon padding", type: "spacing" },
|
|
27
|
+
align,
|
|
28
|
+
padding
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
typography()
|
|
32
|
+
],
|
|
33
|
+
render: {
|
|
34
|
+
mjml(node, { element, attrs, escapeText }) {
|
|
35
|
+
const elements = (node.data.elements || []).map((item) => ` <mj-social-element${attrs({ name: item.name, href: item.href })}>${escapeText(item.label)}</mj-social-element>`)
|
|
36
|
+
return element("mj-social", node, `\n${elements.join("\n")}\n`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
type: "spacer",
|
|
3
|
+
name: "Spacer",
|
|
4
|
+
icon: "spacer",
|
|
5
|
+
category: "content",
|
|
6
|
+
roles: ["content"],
|
|
7
|
+
defaults: () => ({ attributes: { height: "24px" } }),
|
|
8
|
+
fields: [{ title: "Spacer", fields: [{ key: "attributes.height", label: "Height", type: "size" }, { key: "attributes.container-background-color", label: "Background", type: "color" }] }],
|
|
9
|
+
render: { mjml: (node, { element }) => element("mj-spacer", node) }
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { align, padding, typography } from "tess_edit/blocks/fields"
|
|
2
|
+
|
|
3
|
+
const doc = (text) => ({ type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text }] }] })
|
|
4
|
+
|
|
5
|
+
export function plainText(content) {
|
|
6
|
+
if (!content) return ""
|
|
7
|
+
if (content.type === "text") return content.text || ""
|
|
8
|
+
return (content.content || []).map(plainText).join(" ")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
type: "text",
|
|
13
|
+
name: "Text",
|
|
14
|
+
icon: "text",
|
|
15
|
+
roles: ["content"],
|
|
16
|
+
// The canvas edits data.content with Tiptap on double-click.
|
|
17
|
+
richText: "data.content",
|
|
18
|
+
defaults: () => ({ attributes: { padding: "10px 25px" }, data: { content: doc("Write something here. Use {{ variables }} for Liquid.") } }),
|
|
19
|
+
fields: [
|
|
20
|
+
{ title: "Text", fields: [align, padding, { key: "attributes.container-background-color", label: "Background", type: "color" }, { key: "attributes.height", label: "Height", type: "size" }] },
|
|
21
|
+
typography()
|
|
22
|
+
],
|
|
23
|
+
hint: "Double-click the text on the canvas to edit it.",
|
|
24
|
+
label(node) {
|
|
25
|
+
const text = plainText(node.data.content).trim()
|
|
26
|
+
return text ? `Text · ${text.slice(0, 32)}` : "Text"
|
|
27
|
+
},
|
|
28
|
+
palette: [
|
|
29
|
+
{ key: "text", label: "Text", category: "content" },
|
|
30
|
+
{
|
|
31
|
+
key: "heading",
|
|
32
|
+
label: "Heading",
|
|
33
|
+
icon: "heading",
|
|
34
|
+
category: "content",
|
|
35
|
+
create: ({ createNode }) => createNode("text", { attributes: { "font-size": "24px", "font-weight": "700", "line-height": "1.3" }, data: { content: doc("Heading") } })
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
render: { mjml: (node, { element, richText }) => element("mj-text", node, richText(node.data.content)) }
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { background, border, padding } from "tess_edit/blocks/fields"
|
|
2
|
+
import { imageField } from "tess_edit/core/media"
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
type: "wrapper",
|
|
6
|
+
name: "Wrapper",
|
|
7
|
+
icon: "wrapper",
|
|
8
|
+
category: "layout",
|
|
9
|
+
roles: ["wrapper"],
|
|
10
|
+
accepts: ["body"],
|
|
11
|
+
defaults: () => ({ attributes: { padding: "20px 0px", "background-color": "#ffffff" } }),
|
|
12
|
+
fields: [
|
|
13
|
+
{ title: "Wrapper", fields: [background, imageField("attributes.background-url", "Background image", "data.background"), { key: "attributes.full-width", label: "Full width", type: "toggle", on: "full-width", off: "" }, padding] },
|
|
14
|
+
border
|
|
15
|
+
],
|
|
16
|
+
palette: [
|
|
17
|
+
{
|
|
18
|
+
key: "wrapper",
|
|
19
|
+
label: "Wrapper",
|
|
20
|
+
create: ({ createNode }) => createNode("wrapper", { children: [createNode("section", { children: [createNode("column")] })] })
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
render: { mjml: (node, { container }) => container("mj-wrapper", node) }
|
|
24
|
+
}
|