activemail 1.2.3 → 2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -2
- data/README.md +141 -56
- data/UPGRADING.md +185 -0
- data/app/assets/stylesheets/activemail/_components.scss +33 -42
- data/app/assets/stylesheets/activemail/_dark.scss +44 -24
- data/app/assets/stylesheets/activemail/_grid.scss +40 -14
- data/app/assets/stylesheets/activemail/_settings.scss +23 -10
- data/app/assets/stylesheets/activemail/_utilities.scss +3 -1
- data/app/assets/stylesheets/activemail/activemail.scss +6 -5
- data/app/helpers/active_mail/styles_helper.rb +14 -15
- data/app/views/layouts/activemail/_document_head.html.erb +18 -0
- data/app/views/layouts/activemail/mailer.html.inky-erb +1 -22
- data/lib/activemail/asset_pipeline/base.rb +47 -0
- data/lib/activemail/asset_pipeline/null.rb +19 -0
- data/lib/activemail/asset_pipeline/propshaft.rb +19 -0
- data/lib/activemail/asset_pipeline/sprockets.rb +39 -0
- data/lib/activemail/asset_pipeline.rb +35 -0
- data/lib/activemail/components/base.rb +35 -39
- data/lib/activemail/components/block_grid.rb +11 -4
- data/lib/activemail/components/button.rb +5 -28
- data/lib/activemail/components/button_renderer.rb +69 -0
- data/lib/activemail/components/callout.rb +4 -5
- data/lib/activemail/components/center.rb +10 -10
- data/lib/activemail/components/columns.rb +58 -15
- data/lib/activemail/components/container.rb +13 -6
- data/lib/activemail/components/cta.rb +9 -33
- data/lib/activemail/components/h_line.rb +3 -5
- data/lib/activemail/components/info_box.rb +6 -12
- data/lib/activemail/components/menu.rb +3 -4
- data/lib/activemail/components/menu_item.rb +10 -3
- data/lib/activemail/components/row.rb +3 -4
- data/lib/activemail/components/spacer.rb +16 -12
- data/lib/activemail/components/table_component.rb +23 -0
- data/lib/activemail/components/wrapper.rb +10 -4
- data/lib/activemail/configuration.rb +43 -57
- data/lib/activemail/errors.rb +15 -0
- data/lib/activemail/inliner/base.rb +2 -10
- data/lib/activemail/inliner/null.rb +0 -5
- data/lib/activemail/inliner/premailer/document.rb +100 -0
- data/lib/activemail/inliner/premailer.rb +3 -2
- data/lib/activemail/inliner/roadie.rb +1 -1
- data/lib/activemail/inliner.rb +33 -0
- data/lib/activemail/parse_error_reporter.rb +5 -4
- data/lib/activemail/pipeline.rb +26 -0
- data/lib/activemail/quality/builtin_message_rules.rb +58 -0
- data/lib/activemail/quality/builtin_rules.rb +165 -0
- data/lib/activemail/quality/configuration.rb +103 -8
- data/lib/activemail/quality/guard.rb +61 -83
- data/lib/activemail/quality/minitest.rb +56 -27
- data/lib/activemail/quality/output_dir.rb +52 -0
- data/lib/activemail/quality/output_filter.rb +24 -0
- data/lib/activemail/quality/preview_renderer.rb +46 -33
- data/lib/activemail/quality/render_all.rb +95 -39
- data/lib/activemail/quality/rspec.rb +14 -13
- data/lib/activemail/quality/rule.rb +47 -0
- data/lib/activemail/quality/rule_exemptions.rb +49 -0
- data/lib/activemail/rails/engine.rb +13 -13
- data/lib/activemail/rails/rendering.rb +51 -0
- data/lib/activemail/rails/template_handler.rb +3 -1
- data/lib/activemail/tokens/button_style.rb +1 -2
- data/lib/activemail/tokens/defaults.rb +78 -0
- data/lib/activemail/tokens/scss_serializer.rb +4 -4
- data/lib/activemail/tokens.rb +19 -58
- data/lib/activemail/version.rb +1 -1
- data/lib/activemail.rb +89 -61
- data/lib/generators/activemail/templates/component.rb.tt +2 -3
- data/lib/generators/activemail/templates/initializer.rb +0 -3
- data/lib/generators/activemail/templates/mailer_layout.html.inky-erb +3 -19
- data/lib/generators/activemail/templates/mailer_layout.html.inky-haml +3 -11
- data/lib/generators/activemail/templates/mailer_layout.html.inky-slim +3 -10
- data/lib/tasks/activemail.rake +4 -5
- metadata +22 -4
- data/lib/activemail/components/inky.rb +0 -18
- data/lib/activemail/inliner/interceptor.rb +0 -51
- data/lib/activemail/rails/compiled_stylesheet.rb +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3dc3ba94cb3bbe53858813d40a054636f6b445a39fa7c52fcd0a8839e1fb6af0
|
|
4
|
+
data.tar.gz: 0a9f87dd6be3ed968f7a24d0110e0a59d52074d0a582ee8dd69ee430bf126784
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71a238775cdcfee13478fded9dee238e7abc374bd481ff0a8e4995616f329d6490efe939b0fc4bd4d8ae4fb8ab2b8622f44b162aa89c94fb1152570d0f1a8a9b
|
|
7
|
+
data.tar.gz: ef6f072e79dd25da4149fd47db1abbc33b0c97afd198835c97d36b5f1624e23e2245affef488fc931b323ba3e1a9842a45f3177c4b14abf1740d9121d351da42
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,110 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
Behaviour changes to review before upgrading: see [UPGRADING.md](UPGRADING.md).
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `container_gutter` spacing token (16px) and `$am-container-gutter`: the first
|
|
15
|
+
and last column of a row carry it as an outer, border-box, inlined padding;
|
|
16
|
+
nested columns do not repeat it.
|
|
17
|
+
- `<title>` in the layout head, set to the message subject (new
|
|
18
|
+
`layouts/activemail/_document_head` partial).
|
|
19
|
+
- Quality rules, on by default and disableable: `title`, `anchor_href`,
|
|
20
|
+
`placeholder` (`BuiltinRules::PLACEHOLDER`), `unsupported_color`,
|
|
21
|
+
`text_entities`.
|
|
22
|
+
- `Quality::Configuration#allow_declined` and the `allow_declined:` option of
|
|
23
|
+
`Guard#violations`, `Guard#valid?`, `assert_email_quality` and
|
|
24
|
+
`be_a_valid_email`.
|
|
25
|
+
- `Quality::Configuration#rule_exemptions` (by preview name or `preview#email`) and
|
|
26
|
+
the `skip:` option of `Guard#violations`, `Guard#valid?`, `assert_email_quality`
|
|
27
|
+
and `be_a_valid_email`: exempt a preview from named rules, built-in or custom,
|
|
28
|
+
without disabling them for every preview.
|
|
29
|
+
- `mso-padding-alt` on the button cell so Outlook keeps the padding.
|
|
30
|
+
- Below the container width, stacked columns take the container gutter on both
|
|
31
|
+
sides in clients that keep media queries.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- A nested `<columns>` ghost cell and `max-width` are computed against the
|
|
36
|
+
content box of the enclosing columns (their share minus their padding)
|
|
37
|
+
instead of the container.
|
|
38
|
+
- The MSO ghost cell of a column wraps it in its own `<table><tr>` (the `<th>`
|
|
39
|
+
used to sit directly in a `<td>`).
|
|
40
|
+
- Conditional comments use `<!--[if mso]>` instead of `<!--[if mso | IE]>`.
|
|
41
|
+
- `<button>` and `<cta>`: the fill and the radius live on the cell only; the
|
|
42
|
+
`<a>` carries `display:inline-block`, the body font and a `1.25` line height.
|
|
43
|
+
The dark-mode fill targets the cell.
|
|
44
|
+
- `<center>` renders a `<center align="center" class="float-center">` and no
|
|
45
|
+
longer rewrites its children.
|
|
46
|
+
- A mailer that declines to send (`NullMail`) is a `declined` violation unless
|
|
47
|
+
allowed.
|
|
48
|
+
- The generated layouts render the same `_document_head`, `_head` and `_footer`
|
|
49
|
+
partials as the gem layout.
|
|
50
|
+
- With Premailer the parsed framework CSS is cached per stylesheet fingerprint
|
|
51
|
+
(a FIFO of 16) instead of being re-parsed for every email.
|
|
52
|
+
|
|
53
|
+
### Removed
|
|
54
|
+
|
|
55
|
+
- The `.columns.first.last` SCSS rule: a lone column now takes both container
|
|
56
|
+
gutters through `.first` and `.last`.
|
|
57
|
+
|
|
58
|
+
## [2.0.0] - 2026-10-16
|
|
59
|
+
|
|
60
|
+
Breaking release: see [UPGRADING.md](UPGRADING.md).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Emails rendered by a real Rails app shipped without the framework CSS: the
|
|
65
|
+
stylesheet lookup probed `config.assets` (an `OrderedOptions` that answers
|
|
66
|
+
every `respond_to?` with `nil`) and the `<link>` fallback was stripped by
|
|
67
|
+
Premailer. Assets are now read from the actual Sprockets environment/manifest
|
|
68
|
+
or Propshaft load path.
|
|
69
|
+
- Propshaft hosts no longer risk a boot crash on `assets.precompile`.
|
|
70
|
+
- `<raw>` content is no longer transformed when the layout re-rendered the view,
|
|
71
|
+
and an author's literal `###RAW0###` is left intact.
|
|
72
|
+
- The word "doctype" in content and ` `-like characters inside `<style>`
|
|
73
|
+
are no longer rewritten.
|
|
74
|
+
- Numeric attributes are parsed in strict base 10 (`"010"` was read as 8).
|
|
75
|
+
- `<cta>`, `<info-box>`, `<spacer>` and `<block-grid>` keep `id`, `aria-*`,
|
|
76
|
+
`data-*` and the author's `style`.
|
|
77
|
+
- Column widths only count sibling columns (a sibling `<p>` skewed them).
|
|
78
|
+
- The secondary button and info-box tokens now reach the stylesheet and dark
|
|
79
|
+
mode (1.1.0 announced it, but the SCSS ignored them).
|
|
80
|
+
- `.h-line`, `.block-grid.up-N`, `.menu`, `.expander` had no CSS.
|
|
81
|
+
- `render_all` refuses an `output_dir` outside the app and only deletes its own
|
|
82
|
+
files (it used to `rm_rf` the whole directory).
|
|
83
|
+
- The responsive breakpoint follows `container_width` instead of a fixed 596px.
|
|
84
|
+
- Code annotations are registered for the real `inky-*` extensions.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- One transpilation and one inlining pass per email, hooked on
|
|
89
|
+
`ActionMailer::Base#render_to_body`; text parts, attachments and
|
|
90
|
+
non-ActiveMail emails are untouched.
|
|
91
|
+
- `activemail_inline_styles(*paths)` embeds host stylesheets too and raises
|
|
92
|
+
`ActiveMail::StylesheetNotFound` instead of warning.
|
|
93
|
+
- `<button>` and `<cta>` share one bulletproof, token-styled, inlined rendering.
|
|
94
|
+
- SCSS uses the module system (`@use`/`@forward`, `sass:color`, `sass:math`).
|
|
95
|
+
- Transpilation is linear in the document size (200 nested rows: ×3.7,
|
|
96
|
+
2,000 raw blocks: ×35) and reuses a memoized `Core`.
|
|
97
|
+
- `Quality::Guard` is extensible with `Quality::Rule` and gains
|
|
98
|
+
`translation_missing`, `single_root`, `text_part_present`, `text_part_plain`.
|
|
99
|
+
- `Quality` gains `locales`, `formats`, `around_each` and `output_filters`.
|
|
100
|
+
- Errors share the `ActiveMail::Error` base class.
|
|
101
|
+
|
|
102
|
+
### Removed
|
|
103
|
+
|
|
104
|
+
- `ActiveMail::Inliner::Interceptor` and `config.register_inline_interceptor`.
|
|
105
|
+
- `ActiveMail::CompiledStylesheet` (replaced by `ActiveMail::AssetPipeline`).
|
|
106
|
+
- The `<inky>` tag.
|
|
107
|
+
- `IGNORED_ON_PASSTHROUGH`, `combine_attributes`, `pass_through_attributes`,
|
|
108
|
+
`bulletproof_button_table` and `BUTTON_PADDING`.
|
|
109
|
+
- `stylesheet_link_tag` from the layouts.
|
|
110
|
+
- The manual `workflow_dispatch` release trigger.
|
|
111
|
+
|
|
8
112
|
## [1.2.3] - 2026-10-09
|
|
9
113
|
|
|
10
114
|
### Changed
|
|
@@ -61,8 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
61
165
|
- `<button>` and `<cta>` share a `bulletproof_button_table` scaffold; `<cta>`
|
|
62
166
|
drops its hardcoded `4px` radius in favor of the `radius` token.
|
|
63
167
|
- The corner radius is now applied by default on `.button`/`.cta` (the opt-in
|
|
64
|
-
`.radius` class is gone)
|
|
65
|
-
secondary color/border tokens.
|
|
168
|
+
`.radius` class is gone).
|
|
66
169
|
|
|
67
170
|
### Removed
|
|
68
171
|
|
|
@@ -138,5 +241,7 @@ toolkit for Rails.
|
|
|
138
241
|
- Rails `>= 7.1` (tested up to 8.1).
|
|
139
242
|
- Nokogiri `>= 1.16`.
|
|
140
243
|
|
|
244
|
+
[2.1.0]: https://github.com/AdVitam/activemail/compare/v2.0.0...v2.1.0
|
|
245
|
+
[2.0.0]: https://github.com/AdVitam/activemail/compare/v1.2.3...v2.0.0
|
|
141
246
|
[1.0.1]: https://github.com/AdVitam/activemail/compare/v1.0.0...v1.0.1
|
|
142
247
|
[1.0.0]: https://github.com/AdVitam/activemail/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -35,14 +35,14 @@ renders consistently from Apple Mail to Outlook (Word engine) to Gmail mobile.
|
|
|
35
35
|
- **Automatic CSS inlining** via a pluggable adapter (premailer by default, roadie
|
|
36
36
|
optional, or your own).
|
|
37
37
|
- **Generators** to install, eject views/styles, and scaffold components.
|
|
38
|
-
- **Opt-in quality layer**: a Guard (size, `role`, `alt`, `lang`), preview
|
|
38
|
+
- **Opt-in quality layer**: a Guard (size, `role`, `alt`, `lang`, `title`, links, placeholders), preview
|
|
39
39
|
renderer, Minitest assertions + RSpec matcher, and a `render_all` rake task.
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
43
|
```ruby
|
|
44
44
|
# Gemfile
|
|
45
|
-
gem 'activemail', '~> 1
|
|
45
|
+
gem 'activemail', '~> 2.1'
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
```bash
|
|
@@ -55,7 +55,8 @@ framework stylesheet. It works zero-config; customize only what you want.
|
|
|
55
55
|
|
|
56
56
|
Name a mailer view `welcome.html.inky` (or compose with another engine:
|
|
57
57
|
`welcome.html.inky-erb`, `.inky-slim`, `.inky-haml`) and it is transpiled on
|
|
58
|
-
render.
|
|
58
|
+
render. Each email is transpiled and its CSS inlined exactly once, when the
|
|
59
|
+
mailer renders it (view and layout together).
|
|
59
60
|
|
|
60
61
|
## Configuration
|
|
61
62
|
|
|
@@ -70,15 +71,12 @@ ActiveMail.configure do |config|
|
|
|
70
71
|
# CSS inliner: :premailer (default), :roadie, :null, or a custom
|
|
71
72
|
# ActiveMail::Inliner::Base subclass/instance.
|
|
72
73
|
config.inliner = :premailer
|
|
73
|
-
# Set false if another inliner (e.g. premailer-rails) already runs on mailers;
|
|
74
|
-
# `config.inliner = :null` also fully short-circuits ActiveMail's interceptor.
|
|
75
|
-
config.register_inline_interceptor = true
|
|
76
74
|
|
|
77
75
|
# Design tokens (see below).
|
|
78
76
|
config.tokens.color :primary, '#2a9d8f'
|
|
79
77
|
|
|
80
78
|
# Custom components (see "Custom components").
|
|
81
|
-
config.register_component '
|
|
79
|
+
config.register_component 'divider', Divider
|
|
82
80
|
end
|
|
83
81
|
```
|
|
84
82
|
|
|
@@ -106,7 +104,11 @@ ActiveMail.tokens.color(:primary) # => "#2a9d8f"
|
|
|
106
104
|
```
|
|
107
105
|
|
|
108
106
|
Defaults are neutral (a calm teal `primary`, near-black `text`, white
|
|
109
|
-
`background`, …) and fully overridable.
|
|
107
|
+
`background`, …) and fully overridable. Optional tokens fall back to a base
|
|
108
|
+
token: `primary_text`/`secondary_text` → `button_text`, `primary_border`/
|
|
109
|
+
`secondary_border` → none, `info_box_background`/`_border`/`_text` →
|
|
110
|
+
`background`/`border`/`text`. Ruby components and the stylesheet read the same
|
|
111
|
+
resolved values. Under Sprockets the SCSS bridge is a
|
|
110
112
|
preprocessed partial; under Propshaft run `rake activemail:tokens:export` to
|
|
111
113
|
materialize a static `_activemail_tokens.scss`.
|
|
112
114
|
|
|
@@ -118,13 +120,25 @@ hooks (`.button`, `.cta`, `.callout`, `.spacer`, …), utilities, and dark mode.
|
|
|
118
120
|
|
|
119
121
|
Override at three levels, cheapest first:
|
|
120
122
|
|
|
121
|
-
1. **Tokens** (Ruby) — covers most theming.
|
|
123
|
+
1. **Tokens** (Ruby) — covers most theming. Any framework variable can also be
|
|
124
|
+
configured from your own stylesheet with the Sass module system:
|
|
125
|
+
|
|
126
|
+
```scss
|
|
127
|
+
// app/assets/stylesheets/emails/emails.scss
|
|
128
|
+
@use "activemail/activemail" with (
|
|
129
|
+
$am-button-secondary-color: #264653,
|
|
130
|
+
$am-breakpoint: 640px
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
122
134
|
2. **`bin/rails g activemail:styles`** — eject the SCSS partials into your app to
|
|
123
135
|
edit them; your copies shadow the gem's.
|
|
124
136
|
3. **`bin/rails g activemail:views`** — eject the default layout + partials
|
|
125
137
|
(`app/views/layouts/activemail/*`); a same-named file in your app wins by
|
|
126
138
|
Rails path precedence. Put your logo/header/footer here — those are the app's
|
|
127
|
-
identity, not the gem's.
|
|
139
|
+
identity, not the gem's. The `<head>` lives in `_document_head` (metas,
|
|
140
|
+
`<title>` set to the message subject, Outlook settings, inlined styles) and
|
|
141
|
+
is shared by the default layout and the generated one.
|
|
128
142
|
|
|
129
143
|
Dark mode ships on: a `<style>` block keys off `prefers-color-scheme: dark` (Apple
|
|
130
144
|
Mail/iOS) and Outlook's `[data-ogsc]`, with surfaces derived from your tokens.
|
|
@@ -132,9 +146,24 @@ Mail/iOS) and Outlook's `[data-ogsc]`, with surfaces derived from your tokens.
|
|
|
132
146
|
## CSS inlining
|
|
133
147
|
|
|
134
148
|
Email clients (Gmail mobile, Orange.fr) strip `<style>`, so critical CSS must be
|
|
135
|
-
inlined.
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
inlined. When a mailer renders an inky HTML template, ActiveMail transpiles the
|
|
150
|
+
whole body (view + layout) and runs the configured inliner once. Text parts,
|
|
151
|
+
attachments and non-ActiveMail emails are never touched; your `<style>`
|
|
152
|
+
enhancement block (media queries, dark mode) is preserved. With Premailer the
|
|
153
|
+
parsed framework CSS is cached per stylesheet, so it is parsed once, not once per
|
|
154
|
+
email.
|
|
155
|
+
|
|
156
|
+
The layout embeds the compiled stylesheets with `activemail_inline_styles`
|
|
157
|
+
(Sprockets or Propshaft); pass your own on top of the framework's:
|
|
158
|
+
|
|
159
|
+
```erb
|
|
160
|
+
<%= activemail_inline_styles %> <%# framework only %>
|
|
161
|
+
<%= activemail_inline_styles "emails/emails" %> <%# your stylesheet @use-ing the framework %>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A missing or empty asset raises `ActiveMail::StylesheetNotFound`: an email is
|
|
165
|
+
never sent unstyled. An inliner failure raises `ActiveMail::Inliner::Error`
|
|
166
|
+
(the adapter's exception is kept as `#cause`).
|
|
138
167
|
|
|
139
168
|
```ruby
|
|
140
169
|
config.inliner = :premailer # default, hard dependency
|
|
@@ -157,11 +186,11 @@ Fluid-hybrid wrapper, capped at `container_width`, wrapped in an MSO ghost table
|
|
|
157
186
|
```
|
|
158
187
|
|
|
159
188
|
```html
|
|
160
|
-
<!--[if mso
|
|
189
|
+
<!--[if mso]><table role="presentation" align="center" width="600">...<![endif]-->
|
|
161
190
|
<table class="container" align="center" style="width:100%;max-width:600px;margin:0 auto;">
|
|
162
191
|
<tbody><tr><td>...</td></tr></tbody>
|
|
163
192
|
</table>
|
|
164
|
-
<!--[if mso
|
|
193
|
+
<!--[if mso]></td></tr></table><![endif]-->
|
|
165
194
|
```
|
|
166
195
|
|
|
167
196
|
### `<row>` / `<columns>`
|
|
@@ -176,21 +205,39 @@ media-query enhancement.
|
|
|
176
205
|
```
|
|
177
206
|
|
|
178
207
|
Column widths are computed as `container_width × large / column_count`, capped
|
|
179
|
-
at `container_width
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
208
|
+
at `container_width`. A nested `<columns>` is sized against its enclosing column
|
|
209
|
+
(a `large="6"` inside a `large="6"` is 150px in a 600px container). Keep the
|
|
210
|
+
`large` sizes of a row summing to at most `column_count`, otherwise the ghost
|
|
211
|
+
cells will wrap in Outlook.
|
|
212
|
+
|
|
213
|
+
Gutters are border-box padding inlined on the columns: `gutter` (8px) on each
|
|
214
|
+
side between two columns, and `container_gutter` (16px) on the outer edge of the
|
|
215
|
+
first and last column, so content never touches the screen edge. A nested row
|
|
216
|
+
adds no outer gutter. Both are spacing tokens
|
|
217
|
+
(`config.tokens.spacing :container_gutter, '0px'` restores edge-to-edge columns)
|
|
218
|
+
and SCSS variables (`$am-gutter`, `$am-container-gutter`).
|
|
183
219
|
|
|
184
220
|
### `<button>`
|
|
185
221
|
|
|
186
|
-
Bulletproof button
|
|
187
|
-
|
|
222
|
+
Bulletproof button styled inline from the tokens (`Tokens#button_style`): the
|
|
223
|
+
padding lives on the `<a>` so the whole control is clickable (mirrored on the
|
|
224
|
+
cell with `mso-padding-alt` for Outlook), the fill and radius sit on the cell
|
|
225
|
+
only, and a variant border sits on the cell as well. `class="secondary"` selects the secondary variant;
|
|
226
|
+
`expand` makes it full width; without `href` the label renders as a `<span>`.
|
|
188
227
|
|
|
189
228
|
```html
|
|
190
|
-
<button href="#">Go</button>
|
|
229
|
+
<button href="#" class="secondary">Go</button>
|
|
191
230
|
```
|
|
192
231
|
|
|
193
|
-
|
|
232
|
+
```html
|
|
233
|
+
<table class="secondary button"><tbody><tr><td>
|
|
234
|
+
<table><tbody><tr>
|
|
235
|
+
<td style="background:#264653;border-radius:4px;mso-padding-alt:12px 24px;">
|
|
236
|
+
<a href="#" style="display:inline-block;text-decoration:none;padding:12px 24px;font-family:Arial, sans-serif;line-height:1.25;color:#ffffff;font-weight:bold;">Go</a>
|
|
237
|
+
</td>
|
|
238
|
+
</tr></tbody></table>
|
|
239
|
+
</td></tr></tbody></table>
|
|
240
|
+
```
|
|
194
241
|
|
|
195
242
|
### `<menu>` / `<item>`
|
|
196
243
|
|
|
@@ -222,17 +269,13 @@ clickable. Variant classes (`primary`, `expand`, …) are preserved.
|
|
|
222
269
|
### `<wrapper>`, `<h-line>`, `<center>`
|
|
223
270
|
|
|
224
271
|
- `<wrapper class="header">` → `<table class="header wrapper" align="center" style="width:100%;">` with a `.wrapper-inner` cell.
|
|
225
|
-
- `<h-line>` → a full-width single-cell table
|
|
226
|
-
- `<center>`
|
|
227
|
-
|
|
228
|
-
### `<inky>`
|
|
229
|
-
|
|
230
|
-
Renders a bare `<tr>`, useful inside hand-written tables.
|
|
272
|
+
- `<h-line>` → a full-width single-cell table drawn as a 1px rule in the `border` color.
|
|
273
|
+
- `<center>` renders a `<center align="center" class="float-center">` around its content and leaves the content untouched.
|
|
231
274
|
|
|
232
275
|
### `<raw>`
|
|
233
276
|
|
|
234
277
|
Anything between `<raw>` and `</raw>` is passed through untouched (multi-line
|
|
235
|
-
supported). Raw blocks cannot be nested.
|
|
278
|
+
supported), including through the layout. Raw blocks cannot be nested.
|
|
236
279
|
|
|
237
280
|
```html
|
|
238
281
|
<raw><% liquid_or_mso_conditional %></raw>
|
|
@@ -240,28 +283,28 @@ supported). Raw blocks cannot be nested.
|
|
|
240
283
|
|
|
241
284
|
### Token-driven built-ins: `<cta>` and `<info-box>`
|
|
242
285
|
|
|
243
|
-
Brand-neutral, token-styled components
|
|
244
|
-
by default** (so they never collide with your tags). Register them to use:
|
|
245
|
-
|
|
246
|
-
```ruby
|
|
247
|
-
config.register_component 'cta', ActiveMail::Components::Cta
|
|
248
|
-
config.register_component 'info-box', ActiveMail::Components::InfoBox
|
|
249
|
-
```
|
|
286
|
+
Brand-neutral, token-styled components, registered by default.
|
|
250
287
|
|
|
251
288
|
```html
|
|
252
289
|
<cta href="https://example.com">Go</cta>
|
|
253
290
|
<cta href="#" class="secondary">Also go</cta>
|
|
254
291
|
```
|
|
255
292
|
|
|
256
|
-
`<cta>` renders
|
|
257
|
-
|
|
293
|
+
`<cta>` renders exactly like `<button>` with a `cta` class; it raises if `href`
|
|
294
|
+
is missing. `<info-box>` is a callout styled from the `info_box_*` tokens.
|
|
295
|
+
|
|
296
|
+
Every component keeps the author's `class` and `style` (merged, never
|
|
297
|
+
duplicated) and passes `id`, `aria-*`, `data-*` and other attributes through to
|
|
298
|
+
its root element.
|
|
258
299
|
|
|
259
300
|
## Custom components
|
|
260
301
|
|
|
261
302
|
Register your own tag with a class that inherits from
|
|
262
303
|
`ActiveMail::Components::Base` and implements `#transform(node, inner)`. You get
|
|
263
|
-
the matched Nokogiri node (
|
|
264
|
-
|
|
304
|
+
the matched Nokogiri node (read its attributes) and `inner`, an opaque slot
|
|
305
|
+
standing for the already-transformed children: interpolate it exactly once where
|
|
306
|
+
the children belong, never parse or alter it (dropping or duplicating it raises
|
|
307
|
+
`ActiveMail::Error`). Return the replacement markup string. The generator scaffolds one:
|
|
265
308
|
|
|
266
309
|
```bash
|
|
267
310
|
bin/rails g activemail:component Divider
|
|
@@ -272,19 +315,21 @@ The generator namespaces the class as `Components::Divider` (under
|
|
|
272
315
|
A minimal hand-written equivalent:
|
|
273
316
|
|
|
274
317
|
```ruby
|
|
275
|
-
class Divider < ActiveMail::Components::
|
|
276
|
-
def transform(node,
|
|
277
|
-
|
|
278
|
-
%(<table class="#{klass}" #{TABLE_RESET} style="width:100%;"><tbody><tr><td></td></tr></tbody></table>)
|
|
318
|
+
class Divider < ActiveMail::Components::TableComponent
|
|
319
|
+
def transform(node, inner)
|
|
320
|
+
%(<table#{root_attributes(node, classes: 'divider', layout_style: 'width:100%;')} #{TABLE_RESET}><tbody><tr><td>#{inner}</td></tr></tbody></table>)
|
|
279
321
|
end
|
|
280
322
|
end
|
|
281
323
|
|
|
282
324
|
ActiveMail.configuration.register_component('divider', Divider)
|
|
283
325
|
```
|
|
284
326
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
327
|
+
`root_attributes` emits the merged `class`, the merged `style` and every
|
|
328
|
+
pass-through attribute; override `consumed_attributes` to list the attributes
|
|
329
|
+
your component reads itself (they are not passed through). `TableComponent`
|
|
330
|
+
already consumes the `TABLE_RESET` attributes (`role`, `border`, …) of a layout
|
|
331
|
+
table root; inherit from `Base` for any other root element. Other helpers:
|
|
332
|
+
`class?`, `escape_attr`, `column_count`, `container_width`.
|
|
288
333
|
|
|
289
334
|
Per-instance overrides (including replacing a built-in tag) are also possible:
|
|
290
335
|
|
|
@@ -314,9 +359,15 @@ class MailerTest < ActiveSupport::TestCase
|
|
|
314
359
|
include ActiveMail::Quality::Minitest
|
|
315
360
|
|
|
316
361
|
test 'welcome email is sound' do
|
|
317
|
-
assert_email_quality(
|
|
362
|
+
assert_email_quality(WelcomeMailer.welcome(user)) # a message, or an HTML string
|
|
318
363
|
end
|
|
319
364
|
end
|
|
365
|
+
|
|
366
|
+
class EmailPreviewsTest < ActiveSupport::TestCase
|
|
367
|
+
include ActiveMail::Quality::Minitest
|
|
368
|
+
|
|
369
|
+
assert_quality_for_all_previews # one test per preview email and locale
|
|
370
|
+
end
|
|
320
371
|
```
|
|
321
372
|
|
|
322
373
|
```ruby
|
|
@@ -326,19 +377,52 @@ require 'activemail/quality/rspec'
|
|
|
326
377
|
expect(rendered_html).to be_a_valid_email
|
|
327
378
|
```
|
|
328
379
|
|
|
329
|
-
The Guard checks byte size (Gmail clips ~102 KB),
|
|
330
|
-
table, `alt` on every image,
|
|
331
|
-
|
|
380
|
+
The Guard checks byte size (Gmail clips ~102 KB), parse errors,
|
|
381
|
+
`role="presentation"` on every table, `alt` on every image, `lang` on full
|
|
382
|
+
documents, a single `<html>`/`<body>`, missing translations, a non-blank
|
|
383
|
+
`<title>`, an `href` on every link, unresolved `{{placeholders}}`, CSS colors
|
|
384
|
+
clients ignore (`rgb(… %)`, `hsl()`, `color-mix()`, `var()`), and — given a
|
|
385
|
+
message — a plain text part free of HTML entities. A mailer that declines to
|
|
386
|
+
send (`NullMail`) is a violation unless it is listed in `allow_declined`
|
|
387
|
+
(`assert_email_quality(mail, allow_declined: true)` or
|
|
388
|
+
`be_a_valid_email(allow_declined: true)` for one call). Built-in rules can be
|
|
389
|
+
disabled and your own added; a single email can also be exempt from a rule
|
|
390
|
+
(`assert_email_quality(html, skip: %i[img_alt])`, `be_a_valid_email(skip: …)`, or
|
|
391
|
+
`rule_exemptions` below) while every other rule still applies to it. A name the
|
|
392
|
+
guard does not know raises an `ArgumentError` before any preview renders; a
|
|
393
|
+
disabled rule is a known name.
|
|
332
394
|
|
|
333
395
|
```ruby
|
|
396
|
+
signature = ActiveMail::Quality::Rule.html(:single_signature) do |doc, _html|
|
|
397
|
+
'signature rendered twice' if doc.css('.signature').size > 1
|
|
398
|
+
end
|
|
399
|
+
|
|
334
400
|
ActiveMail::Quality.configure do |c|
|
|
335
401
|
c.required_previews = %w[welcome_mailer#welcome]
|
|
336
|
-
c.
|
|
402
|
+
c.allow_declined = %w[reminder_mailer#maybe_skipped] # previews allowed to return a NullMail
|
|
403
|
+
c.rule_exemptions = { # a preview name covers all its emails
|
|
404
|
+
'legacy_mailer' => %i[img_alt],
|
|
405
|
+
'welcome_mailer#welcome' => %i[single_signature]
|
|
406
|
+
}
|
|
407
|
+
c.guard = ActiveMail::Quality::Guard.new(max_bytes: 90_000, disable: %i[img_alt], rules: [signature])
|
|
408
|
+
c.locales = %i[fr en] # or ->(preview) { [...] }; each preview gets params[:locale]
|
|
409
|
+
c.formats = %i[html text]
|
|
410
|
+
c.around_each = lambda do |_preview, _email, _locale, &render|
|
|
411
|
+
ActiveRecord::Base.transaction { render.call; raise ActiveRecord::Rollback } # previews leave no data behind
|
|
412
|
+
end
|
|
413
|
+
c.output_filters = [[/\h{8}-\h{4}-\h{4}-\h{4}-\h{12}/, '<uuid>']] # applied to written files only
|
|
414
|
+
c.output_dir = 'tmp/email_previews' # relative, inside Rails.root
|
|
337
415
|
end
|
|
338
416
|
```
|
|
339
417
|
|
|
340
|
-
`
|
|
341
|
-
|
|
418
|
+
`Rule.html` blocks receive the parsed document and the raw HTML; `Rule.message`
|
|
419
|
+
blocks receive the `Mail::Message`. Return `nil`, a message or an array of
|
|
420
|
+
messages.
|
|
421
|
+
|
|
422
|
+
`rake activemail:emails:render_all` renders every ActionMailer preview (each
|
|
423
|
+
locale and format) to `output_dir` for visual diffing and fails on any guard
|
|
424
|
+
violation among `required_previews`. It only replaces its own `.html`/`.txt`
|
|
425
|
+
files.
|
|
342
426
|
|
|
343
427
|
## Email-client compatibility policy
|
|
344
428
|
|
|
@@ -356,7 +440,8 @@ Targets the real-world client landscape as of 2026:
|
|
|
356
440
|
## Programmatic use
|
|
357
441
|
|
|
358
442
|
```ruby
|
|
359
|
-
ActiveMail
|
|
443
|
+
ActiveMail.configuration.core.transpile('<container><row><columns>Hi</columns></row></container>')
|
|
444
|
+
ActiveMail::Pipeline.call(source) # transpile + inline
|
|
360
445
|
ActiveMail::Core.new(column_count: 24, container_width: 480).transpile(source)
|
|
361
446
|
```
|
|
362
447
|
|