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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +225 -0
  4. data/Rakefile +3 -0
  5. data/app/assets/tailwind/tess_edit/engine.css +10 -0
  6. data/app/controllers/tess_edit/application_controller.rb +5 -0
  7. data/app/controllers/tess_edit/compilations_controller.rb +10 -0
  8. data/app/helpers/tess_edit/application_helper.rb +4 -0
  9. data/app/javascript/tess_edit/blocks/builtin.js +26 -0
  10. data/app/javascript/tess_edit/blocks/button.js +32 -0
  11. data/app/javascript/tess_edit/blocks/column.js +24 -0
  12. data/app/javascript/tess_edit/blocks/condition.js +15 -0
  13. data/app/javascript/tess_edit/blocks/divider.js +24 -0
  14. data/app/javascript/tess_edit/blocks/fields.js +42 -0
  15. data/app/javascript/tess_edit/blocks/hero.js +60 -0
  16. data/app/javascript/tess_edit/blocks/image.js +29 -0
  17. data/app/javascript/tess_edit/blocks/loop.js +23 -0
  18. data/app/javascript/tess_edit/blocks/navbar.js +49 -0
  19. data/app/javascript/tess_edit/blocks/raw.js +18 -0
  20. data/app/javascript/tess_edit/blocks/root.js +83 -0
  21. data/app/javascript/tess_edit/blocks/section.js +40 -0
  22. data/app/javascript/tess_edit/blocks/social.js +39 -0
  23. data/app/javascript/tess_edit/blocks/spacer.js +10 -0
  24. data/app/javascript/tess_edit/blocks/text.js +39 -0
  25. data/app/javascript/tess_edit/blocks/wrapper.js +24 -0
  26. data/app/javascript/tess_edit/controllers/editor_controller.js +758 -0
  27. data/app/javascript/tess_edit/core/document.js +192 -0
  28. data/app/javascript/tess_edit/core/history.js +46 -0
  29. data/app/javascript/tess_edit/core/lock.js +36 -0
  30. data/app/javascript/tess_edit/core/media.js +54 -0
  31. data/app/javascript/tess_edit/core/registry.js +94 -0
  32. data/app/javascript/tess_edit/core/starter.js +89 -0
  33. data/app/javascript/tess_edit/export/compile.js +28 -0
  34. data/app/javascript/tess_edit/export/mjml.js +116 -0
  35. data/app/javascript/tess_edit/export/richtext.js +111 -0
  36. data/app/javascript/tess_edit/index.js +21 -0
  37. data/app/javascript/tess_edit/ui/blocks_panel.js +44 -0
  38. data/app/javascript/tess_edit/ui/canvas.js +437 -0
  39. data/app/javascript/tess_edit/ui/code_input.js +137 -0
  40. data/app/javascript/tess_edit/ui/dom.js +35 -0
  41. data/app/javascript/tess_edit/ui/icons.js +63 -0
  42. data/app/javascript/tess_edit/ui/inspector.js +377 -0
  43. data/app/javascript/tess_edit/ui/text_editing.js +124 -0
  44. data/app/javascript/tess_edit/ui/tree.js +138 -0
  45. data/config/importmap.rb +6 -0
  46. data/config/routes.rb +3 -0
  47. data/lib/tasks/tess_edit_tasks.rake +4 -0
  48. data/lib/tess_edit/engine.rb +18 -0
  49. data/lib/tess_edit/version.rb +3 -0
  50. data/lib/tess_edit.rb +28 -0
  51. data/vendor/javascript/tess_edit/tiptap.js +147 -0
  52. 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
+ }