emjay 0.1.0

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +158 -0
  4. data/lib/emjay/body_component.rb +142 -0
  5. data/lib/emjay/component.rb +61 -0
  6. data/lib/emjay/components/body/mj_accordion.rb +99 -0
  7. data/lib/emjay/components/body/mj_accordion_element.rb +127 -0
  8. data/lib/emjay/components/body/mj_accordion_text.rb +123 -0
  9. data/lib/emjay/components/body/mj_accordion_title.rb +171 -0
  10. data/lib/emjay/components/body/mj_body.rb +70 -0
  11. data/lib/emjay/components/body/mj_button.rb +198 -0
  12. data/lib/emjay/components/body/mj_carousel.rb +410 -0
  13. data/lib/emjay/components/body/mj_carousel_image.rb +188 -0
  14. data/lib/emjay/components/body/mj_column.rb +287 -0
  15. data/lib/emjay/components/body/mj_divider.rb +120 -0
  16. data/lib/emjay/components/body/mj_group.rb +196 -0
  17. data/lib/emjay/components/body/mj_hero.rb +382 -0
  18. data/lib/emjay/components/body/mj_image.rb +188 -0
  19. data/lib/emjay/components/body/mj_navbar.rb +187 -0
  20. data/lib/emjay/components/body/mj_navbar_link.rb +129 -0
  21. data/lib/emjay/components/body/mj_raw.rb +34 -0
  22. data/lib/emjay/components/body/mj_section.rb +442 -0
  23. data/lib/emjay/components/body/mj_social.rb +174 -0
  24. data/lib/emjay/components/body/mj_social_element.rb +272 -0
  25. data/lib/emjay/components/body/mj_spacer.rb +57 -0
  26. data/lib/emjay/components/body/mj_table.rb +113 -0
  27. data/lib/emjay/components/body/mj_text.rb +100 -0
  28. data/lib/emjay/components/body/mj_wrapper.rb +56 -0
  29. data/lib/emjay/components/head/mj_attributes.rb +38 -0
  30. data/lib/emjay/components/head/mj_breakpoint.rb +28 -0
  31. data/lib/emjay/components/head/mj_font.rb +24 -0
  32. data/lib/emjay/components/head/mj_head.rb +20 -0
  33. data/lib/emjay/components/head/mj_html_attributes.rb +33 -0
  34. data/lib/emjay/components/head/mj_preview.rb +24 -0
  35. data/lib/emjay/components/head/mj_style.rb +34 -0
  36. data/lib/emjay/components/head/mj_title.rb +24 -0
  37. data/lib/emjay/global_data.rb +64 -0
  38. data/lib/emjay/head_component.rb +37 -0
  39. data/lib/emjay/helpers/conditional_tag.rb +24 -0
  40. data/lib/emjay/helpers/fonts.rb +34 -0
  41. data/lib/emjay/helpers/gen_random_hex_string.rb +9 -0
  42. data/lib/emjay/helpers/make_lower_breakpoint.rb +17 -0
  43. data/lib/emjay/helpers/media_queries.rb +47 -0
  44. data/lib/emjay/helpers/merge_outlook_conditionals.rb +11 -0
  45. data/lib/emjay/helpers/minify_outlook_conditionals.rb +18 -0
  46. data/lib/emjay/helpers/shorthand_parser.rb +33 -0
  47. data/lib/emjay/helpers/styles.rb +34 -0
  48. data/lib/emjay/helpers/suffix_css_classes.rb +12 -0
  49. data/lib/emjay/helpers/width_parser.rb +26 -0
  50. data/lib/emjay/rails/mail_interceptor.rb +37 -0
  51. data/lib/emjay/rails/template_handler.rb +16 -0
  52. data/lib/emjay/railtie.rb +21 -0
  53. data/lib/emjay/registry.rb +19 -0
  54. data/lib/emjay/renderer.rb +302 -0
  55. data/lib/emjay/skeleton.rb +80 -0
  56. data/lib/emjay/version.rb +5 -0
  57. data/lib/emjay.rb +66 -0
  58. data/llms.txt +130 -0
  59. metadata +129 -0
@@ -0,0 +1,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/conditional_tag"
6
+ require_relative "../../helpers/gen_random_hex_string"
7
+
8
+ module Emjay
9
+ module Components
10
+ class MjCarousel < BodyComponent
11
+ def self.component_name
12
+ "mj-carousel"
13
+ end
14
+
15
+ def self.default_attributes
16
+ {
17
+ "align" => "center",
18
+ "border-radius" => "6px",
19
+ "icon-width" => "44px",
20
+ "left-icon" => "https://i.imgur.com/xTh3hln.png",
21
+ "right-icon" => "https://i.imgur.com/os7o9kz.png",
22
+ "thumbnails" => "visible",
23
+ "tb-border" => "2px solid transparent",
24
+ "tb-border-radius" => "6px",
25
+ "tb-hover-border-color" => "#fead0d",
26
+ "tb-selected-border-color" => "#cccccc"
27
+ }
28
+ end
29
+
30
+ def self.allowed_attributes
31
+ {
32
+ "align" => "enum(left,center,right)",
33
+ "border-radius" => "string",
34
+ "container-background-color" => "color",
35
+ "icon-width" => "unit(px,%)",
36
+ "left-icon" => "string",
37
+ "padding" => "unit(px,%){1,4}",
38
+ "padding-top" => "unit(px,%)",
39
+ "padding-bottom" => "unit(px,%)",
40
+ "padding-left" => "unit(px,%)",
41
+ "padding-right" => "unit(px,%)",
42
+ "right-icon" => "string",
43
+ "thumbnails" => "enum(visible,hidden,supported)",
44
+ "tb-border" => "string",
45
+ "tb-border-radius" => "string",
46
+ "tb-hover-border-color" => "color",
47
+ "tb-selected-border-color" => "color",
48
+ "tb-width" => "unit(px,%)"
49
+ }
50
+ end
51
+
52
+ def initialize(initial_data = {})
53
+ super
54
+ @carousel_id = GenRandomHexString.call(16)
55
+ end
56
+
57
+ def component_head_style(_breakpoint = nil)
58
+ children = @props[:children] || []
59
+ length = children.length
60
+ return "" if length == 0
61
+
62
+ carousel_css = build_carousel_css(length)
63
+ fallback = build_fallback_css(length)
64
+
65
+ "\n#{carousel_css}\n#{fallback}"
66
+ end
67
+
68
+ def get_styles
69
+ {
70
+ carousel: {
71
+ div: {
72
+ "display" => "table",
73
+ "width" => "100%",
74
+ "table-layout" => "fixed",
75
+ "text-align" => "center",
76
+ "font-size" => "0px"
77
+ },
78
+ table: {
79
+ "caption-side" => "top",
80
+ "display" => "table-caption",
81
+ "table-layout" => "fixed",
82
+ "width" => "100%"
83
+ }
84
+ },
85
+ images: {
86
+ td: {
87
+ "padding" => "0px"
88
+ }
89
+ },
90
+ controls: {
91
+ div: {
92
+ "display" => "none",
93
+ "mso-hide" => "all"
94
+ },
95
+ img: {
96
+ "display" => "block",
97
+ "width" => get_attribute("icon-width"),
98
+ "height" => "auto"
99
+ },
100
+ td: {
101
+ "font-size" => "0px",
102
+ "display" => "none",
103
+ "mso-hide" => "all",
104
+ "padding" => "0px"
105
+ }
106
+ }
107
+ }
108
+ end
109
+
110
+ def get_child_context
111
+ @context.merge(thumbnails: get_attribute("thumbnails"))
112
+ end
113
+
114
+ def render
115
+ children = @props[:children] || []
116
+
117
+ carousel_html = ConditionalTag.mso_conditional_tag(
118
+ render_carousel_content(children),
119
+ negation: true
120
+ )
121
+
122
+ fallback_html = render_fallback(children)
123
+
124
+ <<~HTML
125
+ #{carousel_html}
126
+ #{fallback_html}
127
+ HTML
128
+ end
129
+
130
+ private
131
+
132
+ def thumbnails_width
133
+ children = @props[:children] || []
134
+ return 0 if children.empty?
135
+
136
+ get_attribute("tb-width") ||
137
+ "#{[@context[:container_width].to_f / children.length, 110].min.to_i}px"
138
+ end
139
+
140
+ def render_carousel_content(children)
141
+ div_attrs = html_attributes(class: "mj-carousel")
142
+ content_attrs = html_attributes(
143
+ class: "mj-carousel-content mj-carousel-#{@carousel_id}-content",
144
+ style: "carousel.div"
145
+ )
146
+
147
+ <<~HTML
148
+ <div
149
+ #{div_attrs}
150
+ >
151
+ #{generate_radios(children)}
152
+ <div
153
+ #{content_attrs}
154
+ >
155
+ #{generate_thumbnails(children)}
156
+ #{generate_carousel(children)}
157
+ </div>
158
+ </div>
159
+ HTML
160
+ end
161
+
162
+ def generate_radios(children)
163
+ render_children(children,
164
+ renderer: ->(component) { component.render_radio },
165
+ attributes: {"carouselId" => @carousel_id})
166
+ end
167
+
168
+ def generate_thumbnails(children)
169
+ return "" unless %w[visible supported].include?(get_attribute("thumbnails"))
170
+
171
+ render_children(children,
172
+ attributes: {
173
+ "tb-border" => get_attribute("tb-border"),
174
+ "tb-border-radius" => get_attribute("tb-border-radius"),
175
+ "tb-width" => thumbnails_width,
176
+ "carouselId" => @carousel_id
177
+ },
178
+ renderer: ->(component) { component.render_thumbnail })
179
+ end
180
+
181
+ def generate_controls(children, direction, icon)
182
+ icon_width = get_attribute("icon-width").to_i
183
+ td_attrs = html_attributes(
184
+ class: "mj-carousel-#{@carousel_id}-icons-cell",
185
+ style: "controls.td"
186
+ )
187
+ div_attrs = html_attributes(
188
+ class: "mj-carousel-#{direction}-icons",
189
+ style: "controls.div"
190
+ )
191
+
192
+ labels = (1..children.length).map do |i|
193
+ label_attrs = html_attributes(
194
+ for: "mj-carousel-#{@carousel_id}-radio-#{i}",
195
+ class: "mj-carousel-#{direction} mj-carousel-#{direction}-#{i}"
196
+ )
197
+ img_attrs = html_attributes(
198
+ src: icon,
199
+ alt: direction,
200
+ style: "controls.img",
201
+ width: icon_width
202
+ )
203
+ <<~LABEL
204
+ <label
205
+ #{label_attrs}
206
+ >
207
+ <img
208
+ #{img_attrs}
209
+ />
210
+ </label>
211
+ LABEL
212
+ end.join
213
+
214
+ <<~HTML
215
+ <td
216
+ #{td_attrs}
217
+ >
218
+ <div
219
+ #{div_attrs}
220
+ >
221
+ #{labels}
222
+ </div>
223
+ </td>
224
+ HTML
225
+ end
226
+
227
+ def generate_images(children)
228
+ td_attrs = html_attributes(style: "images.td")
229
+ div_attrs = html_attributes(class: "mj-carousel-images")
230
+
231
+ images_html = render_children(children,
232
+ attributes: {"border-radius" => get_attribute("border-radius")})
233
+
234
+ <<~HTML
235
+ <td
236
+ #{td_attrs}
237
+ >
238
+ <div
239
+ #{div_attrs}
240
+ >
241
+ #{images_html}
242
+ </div>
243
+ </td>
244
+ HTML
245
+ end
246
+
247
+ def generate_carousel(children)
248
+ table_attrs = html_attributes(
249
+ style: "carousel.table",
250
+ border: "0",
251
+ cellpadding: "0",
252
+ cellspacing: "0",
253
+ width: "100%",
254
+ role: "presentation",
255
+ class: "mj-carousel-main"
256
+ )
257
+
258
+ <<~HTML
259
+ <table
260
+ #{table_attrs}
261
+ >
262
+ <tbody>
263
+ <tr>
264
+ #{generate_controls(children, "previous", get_attribute("left-icon"))}
265
+ #{generate_images(children)}
266
+ #{generate_controls(children, "next", get_attribute("right-icon"))}
267
+ </tr>
268
+ </tbody>
269
+ </table>
270
+ HTML
271
+ end
272
+
273
+ def render_fallback(children)
274
+ return "" if children.empty?
275
+
276
+ ConditionalTag.mso_conditional_tag(
277
+ render_children([children[0]],
278
+ attributes: {"border-radius" => get_attribute("border-radius")})
279
+ )
280
+ end
281
+
282
+ def build_carousel_css(length)
283
+ id = @carousel_id
284
+
285
+ hide_all = (0...length).map { |i|
286
+ ".mj-carousel-#{id}-radio:checked #{"+ * " * i}+ .mj-carousel-content .mj-carousel-image"
287
+ }.join(",")
288
+
289
+ show_selected = (0...length).map { |i|
290
+ ".mj-carousel-#{id}-radio-#{i + 1}:checked #{"+ * " * (length - i - 1)}+ .mj-carousel-content .mj-carousel-image-#{i + 1}"
291
+ }.join(",")
292
+
293
+ next_icons = (0...length).map { |i|
294
+ ".mj-carousel-#{id}-radio-#{i + 1}:checked #{"+ * " * (length - i - 1)}+ .mj-carousel-content .mj-carousel-next-#{((i + (1 % length) + length) % length) + 1}"
295
+ }.join(",")
296
+
297
+ prev_icons = (0...length).map { |i|
298
+ ".mj-carousel-#{id}-radio-#{i + 1}:checked #{"+ * " * (length - i - 1)}+ .mj-carousel-content .mj-carousel-previous-#{((i - (1 % length) + length) % length) + 1}"
299
+ }.join(",")
300
+
301
+ selected_thumbnail = (0...length).map { |i|
302
+ ".mj-carousel-#{id}-radio-#{i + 1}:checked #{"+ * " * (length - i - 1)}+ .mj-carousel-content .mj-carousel-#{id}-thumbnail-#{i + 1}"
303
+ }.join(",")
304
+
305
+ show_thumbnails = (0...length).map { |i|
306
+ ".mj-carousel-#{id}-radio-#{i + 1}:checked #{"+ * " * (length - i - 1)}+ .mj-carousel-content .mj-carousel-#{id}-thumbnail\n "
307
+ }.join(",")
308
+
309
+ hide_on_hover = (0...length).map { |i|
310
+ ".mj-carousel-#{id}-thumbnail:hover #{"+ * " * (length - i - 1)}+ .mj-carousel-main .mj-carousel-image"
311
+ }.join(",")
312
+
313
+ show_on_hover = (0...length).map { |i|
314
+ ".mj-carousel-#{id}-thumbnail-#{i + 1}:hover #{"+ * " * (length - i - 1)}+ .mj-carousel-main .mj-carousel-image-#{i + 1}"
315
+ }.join(",")
316
+
317
+ <<~CSS
318
+ .mj-carousel {
319
+ -webkit-user-select: none;
320
+ -moz-user-select: none;
321
+ user-select: none;
322
+ }
323
+
324
+ .mj-carousel-#{id}-icons-cell {
325
+ display: table-cell !important;
326
+ width: #{get_attribute("icon-width")} !important;
327
+ }
328
+
329
+ .mj-carousel-radio,
330
+ .mj-carousel-next,
331
+ .mj-carousel-previous {
332
+ display: none !important;
333
+ }
334
+
335
+ .mj-carousel-thumbnail,
336
+ .mj-carousel-next,
337
+ .mj-carousel-previous {
338
+ touch-action: manipulation;
339
+ }
340
+
341
+ #{hide_all} {
342
+ display: none !important;
343
+ }
344
+
345
+ #{show_selected} {
346
+ display: block !important;
347
+ }
348
+
349
+ .mj-carousel-previous-icons,
350
+ .mj-carousel-next-icons,
351
+ #{next_icons},
352
+ #{prev_icons} {
353
+ display: block !important;
354
+ }
355
+
356
+ #{selected_thumbnail} {
357
+ border-color: #{get_attribute("tb-selected-border-color")} !important;
358
+ }
359
+
360
+ #{show_thumbnails} {
361
+ display: inline-block !important;
362
+ }
363
+
364
+ .mj-carousel-image img + div,
365
+ .mj-carousel-thumbnail img + div {
366
+ display: none !important;
367
+ }
368
+
369
+ #{hide_on_hover} {
370
+ display: none !important;
371
+ }
372
+
373
+ .mj-carousel-thumbnail:hover {
374
+ border-color: #{get_attribute("tb-hover-border-color")} !important;
375
+ }
376
+
377
+ #{show_on_hover} {
378
+ display: block !important;
379
+ }
380
+ CSS
381
+ end
382
+
383
+ def build_fallback_css(length)
384
+ id = @carousel_id
385
+ <<~CSS
386
+ .mj-carousel noinput { display:block !important; }
387
+ .mj-carousel noinput .mj-carousel-image-1 { display: block !important; }
388
+ .mj-carousel noinput .mj-carousel-arrows,
389
+ .mj-carousel noinput .mj-carousel-thumbnails { display: none !important; }
390
+
391
+ [owa] .mj-carousel-thumbnail { display: none !important; }
392
+
393
+ @media screen yahoo {
394
+ .mj-carousel-#{id}-icons-cell,
395
+ .mj-carousel-previous-icons,
396
+ .mj-carousel-next-icons {
397
+ display: none !important;
398
+ }
399
+
400
+ .mj-carousel-#{id}-radio-1:checked #{"+ *" * (length - 1)}+ .mj-carousel-content .mj-carousel-#{id}-thumbnail-1 {
401
+ border-color: transparent;
402
+ }
403
+ }
404
+ CSS
405
+ end
406
+ end
407
+ end
408
+
409
+ Registry.register(Components::MjCarousel)
410
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../body_component"
4
+ require_relative "../../registry"
5
+ require_relative "../../helpers/suffix_css_classes"
6
+
7
+ module Emjay
8
+ module Components
9
+ class MjCarouselImage < BodyComponent
10
+ def self.component_name
11
+ "mj-carousel-image"
12
+ end
13
+
14
+ def self.ending_tag?
15
+ true
16
+ end
17
+
18
+ def self.default_attributes
19
+ {
20
+ "alt" => "",
21
+ "target" => "_blank"
22
+ }
23
+ end
24
+
25
+ def self.allowed_attributes
26
+ {
27
+ "alt" => "string",
28
+ "href" => "string",
29
+ "rel" => "string",
30
+ "target" => "string",
31
+ "title" => "string",
32
+ "src" => "string",
33
+ "thumbnails-src" => "string",
34
+ "border-radius" => "string",
35
+ "tb-border" => "string",
36
+ "tb-border-radius" => "string"
37
+ }
38
+ end
39
+
40
+ def get_styles
41
+ has_thumbnails_supported = thumbnails_supported?
42
+ {
43
+ images: {
44
+ img: {
45
+ "border-radius" => get_attribute("border-radius"),
46
+ "display" => "block",
47
+ "width" => @context[:container_width],
48
+ "max-width" => "100%",
49
+ "height" => "auto"
50
+ },
51
+ firstImageDiv: {},
52
+ otherImageDiv: {
53
+ "display" => "none",
54
+ "mso-hide" => "all"
55
+ }
56
+ },
57
+ radio: {
58
+ input: {
59
+ "display" => "none",
60
+ "mso-hide" => "all"
61
+ }
62
+ },
63
+ thumbnails: {
64
+ a: {
65
+ "border" => get_attribute("tb-border"),
66
+ "border-radius" => get_attribute("tb-border-radius"),
67
+ "display" => has_thumbnails_supported ? "none" : "inline-block",
68
+ "overflow" => "hidden",
69
+ "width" => get_attribute("tb-width")
70
+ },
71
+ img: {
72
+ "display" => "block",
73
+ "width" => "100%",
74
+ "height" => "auto"
75
+ }
76
+ }
77
+ }
78
+ end
79
+
80
+ def render
81
+ src = get_attribute("src")
82
+ alt = get_attribute("alt")
83
+ href = get_attribute("href")
84
+ rel = get_attribute("rel")
85
+ title = get_attribute("title")
86
+ index = @props[:index] || 0
87
+
88
+ img_attrs = html_attributes(
89
+ title: title,
90
+ src: src,
91
+ alt: alt,
92
+ style: "images.img",
93
+ width: @context[:container_width].to_i,
94
+ border: "0"
95
+ )
96
+ image = "<img\n #{img_attrs} />"
97
+
98
+ css_class = get_attribute("css-class") || ""
99
+ div_style = (index == 0) ? "images.firstImageDiv" : "images.otherImageDiv"
100
+ div_attrs = html_attributes(
101
+ class: "mj-carousel-image mj-carousel-image-#{index + 1} #{css_class}",
102
+ style: div_style
103
+ )
104
+
105
+ content = href ? "<a#{html_attributes(href: href, rel: rel, target: "_blank")}>#{image}</a>" : image
106
+
107
+ <<~HTML
108
+ <div
109
+ #{div_attrs}
110
+ >
111
+ #{content}
112
+ </div>
113
+ HTML
114
+ end
115
+
116
+ def render_radio
117
+ index = @props[:index] || 0
118
+ carousel_id = get_attribute("carouselId")
119
+
120
+ input_attrs = html_attributes(
121
+ class: "mj-carousel-radio mj-carousel-#{carousel_id}-radio mj-carousel-#{carousel_id}-radio-#{index + 1}",
122
+ checked: (index == 0) ? "checked" : nil,
123
+ type: "radio",
124
+ name: "mj-carousel-radio-#{carousel_id}",
125
+ id: "mj-carousel-#{carousel_id}-radio-#{index + 1}",
126
+ style: "radio.input"
127
+ )
128
+
129
+ <<~HTML
130
+ <input
131
+ #{input_attrs}
132
+ />
133
+ HTML
134
+ end
135
+
136
+ def render_thumbnail
137
+ carousel_id = get_attribute("carouselId")
138
+ src = get_attribute("src")
139
+ alt = get_attribute("alt")
140
+ width = get_attribute("tb-width")
141
+ target = get_attribute("target")
142
+ index = @props[:index] || 0
143
+ img_index = index + 1
144
+
145
+ css_class = SuffixCssClasses.call(get_attribute("css-class"), "thumbnail")
146
+
147
+ a_attrs = html_attributes(
148
+ style: "thumbnails.a",
149
+ href: "##{img_index}",
150
+ target: target,
151
+ class: "mj-carousel-thumbnail mj-carousel-#{carousel_id}-thumbnail mj-carousel-#{carousel_id}-thumbnail-#{img_index} #{css_class}"
152
+ )
153
+
154
+ label_attrs = html_attributes(
155
+ for: "mj-carousel-#{carousel_id}-radio-#{img_index}"
156
+ )
157
+
158
+ img_attrs = html_attributes(
159
+ style: "thumbnails.img",
160
+ src: get_attribute("thumbnails-src") || src,
161
+ alt: alt,
162
+ width: width.to_i
163
+ )
164
+
165
+ <<~HTML
166
+ <a
167
+ #{a_attrs}
168
+ >
169
+ <label#{label_attrs}>
170
+ <img
171
+ #{img_attrs}
172
+ />
173
+ </label>
174
+ </a>
175
+ HTML
176
+ end
177
+
178
+ private
179
+
180
+ def thumbnails_supported?
181
+ thumbnails = get_attribute("thumbnails") || @context[:thumbnails]
182
+ thumbnails == "supported"
183
+ end
184
+ end
185
+ end
186
+
187
+ Registry.register(Components::MjCarouselImage)
188
+ end