activemail 1.2.3 → 2.0.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 +55 -2
- data/README.md +105 -47
- data/UPGRADING.md +112 -0
- data/app/assets/stylesheets/activemail/_components.scss +33 -42
- data/app/assets/stylesheets/activemail/_dark.scss +31 -20
- data/app/assets/stylesheets/activemail/_grid.scss +2 -0
- data/app/assets/stylesheets/activemail/_settings.scss +22 -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/mailer.html.inky-erb +0 -5
- 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 +65 -0
- data/lib/activemail/components/callout.rb +4 -5
- data/lib/activemail/components/center.rb +5 -7
- data/lib/activemail/components/columns.rb +26 -7
- data/lib/activemail/components/container.rb +11 -4
- 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/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_rules.rb +154 -0
- data/lib/activemail/quality/configuration.rb +83 -3
- data/lib/activemail/quality/guard.rb +45 -87
- data/lib/activemail/quality/minitest.rb +41 -25
- data/lib/activemail/quality/output_dir.rb +52 -0
- data/lib/activemail/quality/preview_renderer.rb +46 -33
- data/lib/activemail/quality/render_all.rb +96 -40
- data/lib/activemail/quality/rspec.rb +4 -8
- data/lib/activemail/quality/rule.rb +47 -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 +77 -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 +0 -2
- data/lib/generators/activemail/templates/mailer_layout.html.inky-haml +0 -2
- data/lib/generators/activemail/templates/mailer_layout.html.inky-slim +0 -2
- data/lib/tasks/activemail.rake +4 -5
- metadata +17 -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: c37e5bd4d1c2746462f33f9b221a2734081ec4f89be697f77e22f8402767c879
|
|
4
|
+
data.tar.gz: 69b5e178c7521b8f741a504af3c4d5524373b68b80cbe44af35a38a168e77389
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2611a2f9405fcdb5e41de1f5817b603d3574ccbc5a0607cd45b22ec85f34cf283d3bd8bfdb22786b48e8248b6b941935a126466fb83a5e70c25ce144be41b64b
|
|
7
|
+
data.tar.gz: 7d398eb0eaa80095458a9eb3811aa906f85d212abb6c784166edceeb3249c91f33d3e011efe55b9f4be3fd6e24aadf0246539f0be5f29dc6e9ca28a297475bd9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,60 @@ 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.0.0] - Unreleased
|
|
9
|
+
|
|
10
|
+
Breaking release: see [UPGRADING.md](UPGRADING.md).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Emails rendered by a real Rails app shipped without the framework CSS: the
|
|
15
|
+
stylesheet lookup probed `config.assets` (an `OrderedOptions` that answers
|
|
16
|
+
every `respond_to?` with `nil`) and the `<link>` fallback was stripped by
|
|
17
|
+
Premailer. Assets are now read from the actual Sprockets environment/manifest
|
|
18
|
+
or Propshaft load path.
|
|
19
|
+
- Propshaft hosts no longer risk a boot crash on `assets.precompile`.
|
|
20
|
+
- `<raw>` content is no longer transformed when the layout re-rendered the view,
|
|
21
|
+
and an author's literal `###RAW0###` is left intact.
|
|
22
|
+
- The word "doctype" in content and ` `-like characters inside `<style>`
|
|
23
|
+
are no longer rewritten.
|
|
24
|
+
- Numeric attributes are parsed in strict base 10 (`"010"` was read as 8).
|
|
25
|
+
- `<cta>`, `<info-box>`, `<spacer>` and `<block-grid>` keep `id`, `aria-*`,
|
|
26
|
+
`data-*` and the author's `style`.
|
|
27
|
+
- Column widths only count sibling columns (a sibling `<p>` skewed them).
|
|
28
|
+
- The secondary button and info-box tokens now reach the stylesheet and dark
|
|
29
|
+
mode (1.1.0 announced it, but the SCSS ignored them).
|
|
30
|
+
- `.h-line`, `.block-grid.up-N`, `.menu`, `.expander` had no CSS.
|
|
31
|
+
- `render_all` refuses an `output_dir` outside the app and only deletes its own
|
|
32
|
+
files (it used to `rm_rf` the whole directory).
|
|
33
|
+
- The responsive breakpoint follows `container_width` instead of a fixed 596px.
|
|
34
|
+
- Code annotations are registered for the real `inky-*` extensions.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- One transpilation and one inlining pass per email, hooked on
|
|
39
|
+
`ActionMailer::Base#render_to_body`; text parts, attachments and
|
|
40
|
+
non-ActiveMail emails are untouched.
|
|
41
|
+
- `activemail_inline_styles(*paths)` embeds host stylesheets too and raises
|
|
42
|
+
`ActiveMail::StylesheetNotFound` instead of warning.
|
|
43
|
+
- `<button>` and `<cta>` share one bulletproof, token-styled, inlined rendering.
|
|
44
|
+
- SCSS uses the module system (`@use`/`@forward`, `sass:color`, `sass:math`).
|
|
45
|
+
- Transpilation is linear in the document size (200 nested rows: ×3.7,
|
|
46
|
+
2,000 raw blocks: ×35) and reuses a memoized `Core`.
|
|
47
|
+
- `Quality::Guard` is extensible with `Quality::Rule` and gains
|
|
48
|
+
`translation_missing`, `single_root`, `text_part_present`, `text_part_plain`.
|
|
49
|
+
- `Quality` gains `locales`, `formats`, `around_each` and `output_filters`.
|
|
50
|
+
- Errors share the `ActiveMail::Error` base class.
|
|
51
|
+
|
|
52
|
+
### Removed
|
|
53
|
+
|
|
54
|
+
- `ActiveMail::Inliner::Interceptor` and `config.register_inline_interceptor`.
|
|
55
|
+
- `ActiveMail::CompiledStylesheet` (replaced by `ActiveMail::AssetPipeline`).
|
|
56
|
+
- The `<inky>` tag.
|
|
57
|
+
- `IGNORED_ON_PASSTHROUGH`, `combine_attributes`, `pass_through_attributes`,
|
|
58
|
+
`bulletproof_button_table` and `BUTTON_PADDING`.
|
|
59
|
+
- `stylesheet_link_tag` from the layouts.
|
|
60
|
+
- The manual `workflow_dispatch` release trigger.
|
|
61
|
+
|
|
8
62
|
## [1.2.3] - 2026-10-09
|
|
9
63
|
|
|
10
64
|
### Changed
|
|
@@ -61,8 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
61
115
|
- `<button>` and `<cta>` share a `bulletproof_button_table` scaffold; `<cta>`
|
|
62
116
|
drops its hardcoded `4px` radius in favor of the `radius` token.
|
|
63
117
|
- The corner radius is now applied by default on `.button`/`.cta` (the opt-in
|
|
64
|
-
`.radius` class is gone)
|
|
65
|
-
secondary color/border tokens.
|
|
118
|
+
`.radius` class is gone).
|
|
66
119
|
|
|
67
120
|
### Removed
|
|
68
121
|
|
data/README.md
CHANGED
|
@@ -42,7 +42,7 @@ renders consistently from Apple Mail to Outlook (Word engine) to Gmail mobile.
|
|
|
42
42
|
|
|
43
43
|
```ruby
|
|
44
44
|
# Gemfile
|
|
45
|
-
gem 'activemail', '~>
|
|
45
|
+
gem 'activemail', '~> 2.0'
|
|
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,7 +120,17 @@ 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
|
|
@@ -132,9 +144,22 @@ Mail/iOS) and Outlook's `[data-ogsc]`, with surfaces derived from your tokens.
|
|
|
132
144
|
## CSS inlining
|
|
133
145
|
|
|
134
146
|
Email clients (Gmail mobile, Orange.fr) strip `<style>`, so critical CSS must be
|
|
135
|
-
inlined.
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
inlined. When a mailer renders an inky HTML template, ActiveMail transpiles the
|
|
148
|
+
whole body (view + layout) and runs the configured inliner once. Text parts,
|
|
149
|
+
attachments and non-ActiveMail emails are never touched; your `<style>`
|
|
150
|
+
enhancement block (media queries, dark mode) is preserved.
|
|
151
|
+
|
|
152
|
+
The layout embeds the compiled stylesheets with `activemail_inline_styles`
|
|
153
|
+
(Sprockets or Propshaft); pass your own on top of the framework's:
|
|
154
|
+
|
|
155
|
+
```erb
|
|
156
|
+
<%= activemail_inline_styles %> <%# framework only %>
|
|
157
|
+
<%= activemail_inline_styles "emails/emails" %> <%# your stylesheet @use-ing the framework %>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A missing or empty asset raises `ActiveMail::StylesheetNotFound`: an email is
|
|
161
|
+
never sent unstyled. An inliner failure raises `ActiveMail::Inliner::Error`
|
|
162
|
+
(the adapter's exception is kept as `#cause`).
|
|
138
163
|
|
|
139
164
|
```ruby
|
|
140
165
|
config.inliner = :premailer # default, hard dependency
|
|
@@ -183,14 +208,24 @@ columns for gutters, and keep the `large` sizes of a row summing to at most
|
|
|
183
208
|
|
|
184
209
|
### `<button>`
|
|
185
210
|
|
|
186
|
-
Bulletproof button
|
|
187
|
-
|
|
211
|
+
Bulletproof button styled inline from the tokens (`Tokens#button_style`): the
|
|
212
|
+
padding lives on the `<a>` so the whole control is clickable, and a variant
|
|
213
|
+
border sits on the cell only. `class="secondary"` selects the secondary variant;
|
|
214
|
+
`expand` makes it full width; without `href` the label renders as a `<span>`.
|
|
188
215
|
|
|
189
216
|
```html
|
|
190
|
-
<button href="#">Go</button>
|
|
217
|
+
<button href="#" class="secondary">Go</button>
|
|
191
218
|
```
|
|
192
219
|
|
|
193
|
-
|
|
220
|
+
```html
|
|
221
|
+
<table class="secondary button"><tbody><tr><td>
|
|
222
|
+
<table><tbody><tr>
|
|
223
|
+
<td style="background:#264653;border-radius:4px;">
|
|
224
|
+
<a href="#" style="display:inline-block;text-decoration:none;padding:12px 24px;background:#264653;color:#ffffff;font-weight:bold;border-radius:4px;">Go</a>
|
|
225
|
+
</td>
|
|
226
|
+
</tr></tbody></table>
|
|
227
|
+
</td></tr></tbody></table>
|
|
228
|
+
```
|
|
194
229
|
|
|
195
230
|
### `<menu>` / `<item>`
|
|
196
231
|
|
|
@@ -222,17 +257,13 @@ clickable. Variant classes (`primary`, `expand`, …) are preserved.
|
|
|
222
257
|
### `<wrapper>`, `<h-line>`, `<center>`
|
|
223
258
|
|
|
224
259
|
- `<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
|
|
260
|
+
- `<h-line>` → a full-width single-cell table drawn as a 1px rule in the `border` color.
|
|
226
261
|
- `<center>` adds `align="center"` and `.float-center` to its element children (and `.float-center` to nested menu items).
|
|
227
262
|
|
|
228
|
-
### `<inky>`
|
|
229
|
-
|
|
230
|
-
Renders a bare `<tr>`, useful inside hand-written tables.
|
|
231
|
-
|
|
232
263
|
### `<raw>`
|
|
233
264
|
|
|
234
265
|
Anything between `<raw>` and `</raw>` is passed through untouched (multi-line
|
|
235
|
-
supported). Raw blocks cannot be nested.
|
|
266
|
+
supported), including through the layout. Raw blocks cannot be nested.
|
|
236
267
|
|
|
237
268
|
```html
|
|
238
269
|
<raw><% liquid_or_mso_conditional %></raw>
|
|
@@ -240,28 +271,28 @@ supported). Raw blocks cannot be nested.
|
|
|
240
271
|
|
|
241
272
|
### Token-driven built-ins: `<cta>` and `<info-box>`
|
|
242
273
|
|
|
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
|
-
```
|
|
274
|
+
Brand-neutral, token-styled components, registered by default.
|
|
250
275
|
|
|
251
276
|
```html
|
|
252
277
|
<cta href="https://example.com">Go</cta>
|
|
253
278
|
<cta href="#" class="secondary">Also go</cta>
|
|
254
279
|
```
|
|
255
280
|
|
|
256
|
-
`<cta>` renders
|
|
257
|
-
|
|
281
|
+
`<cta>` renders exactly like `<button>` with a `cta` class; it raises if `href`
|
|
282
|
+
is missing. `<info-box>` is a callout styled from the `info_box_*` tokens.
|
|
283
|
+
|
|
284
|
+
Every component keeps the author's `class` and `style` (merged, never
|
|
285
|
+
duplicated) and passes `id`, `aria-*`, `data-*` and other attributes through to
|
|
286
|
+
its root element.
|
|
258
287
|
|
|
259
288
|
## Custom components
|
|
260
289
|
|
|
261
290
|
Register your own tag with a class that inherits from
|
|
262
291
|
`ActiveMail::Components::Base` and implements `#transform(node, inner)`. You get
|
|
263
|
-
the matched Nokogiri node (
|
|
264
|
-
|
|
292
|
+
the matched Nokogiri node (read its attributes) and `inner`, an opaque slot
|
|
293
|
+
standing for the already-transformed children: interpolate it exactly once where
|
|
294
|
+
the children belong, never parse or alter it (dropping or duplicating it raises
|
|
295
|
+
`ActiveMail::Error`). Return the replacement markup string. The generator scaffolds one:
|
|
265
296
|
|
|
266
297
|
```bash
|
|
267
298
|
bin/rails g activemail:component Divider
|
|
@@ -272,19 +303,21 @@ The generator namespaces the class as `Components::Divider` (under
|
|
|
272
303
|
A minimal hand-written equivalent:
|
|
273
304
|
|
|
274
305
|
```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>)
|
|
306
|
+
class Divider < ActiveMail::Components::TableComponent
|
|
307
|
+
def transform(node, inner)
|
|
308
|
+
%(<table#{root_attributes(node, classes: 'divider', layout_style: 'width:100%;')} #{TABLE_RESET}><tbody><tr><td>#{inner}</td></tr></tbody></table>)
|
|
279
309
|
end
|
|
280
310
|
end
|
|
281
311
|
|
|
282
312
|
ActiveMail.configuration.register_component('divider', Divider)
|
|
283
313
|
```
|
|
284
314
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
315
|
+
`root_attributes` emits the merged `class`, the merged `style` and every
|
|
316
|
+
pass-through attribute; override `consumed_attributes` to list the attributes
|
|
317
|
+
your component reads itself (they are not passed through). `TableComponent`
|
|
318
|
+
already consumes the `TABLE_RESET` attributes (`role`, `border`, …) of a layout
|
|
319
|
+
table root; inherit from `Base` for any other root element. Other helpers:
|
|
320
|
+
`class?`, `escape_attr`, `column_count`, `container_width`.
|
|
288
321
|
|
|
289
322
|
Per-instance overrides (including replacing a built-in tag) are also possible:
|
|
290
323
|
|
|
@@ -314,9 +347,15 @@ class MailerTest < ActiveSupport::TestCase
|
|
|
314
347
|
include ActiveMail::Quality::Minitest
|
|
315
348
|
|
|
316
349
|
test 'welcome email is sound' do
|
|
317
|
-
assert_email_quality(
|
|
350
|
+
assert_email_quality(WelcomeMailer.welcome(user)) # a message, or an HTML string
|
|
318
351
|
end
|
|
319
352
|
end
|
|
353
|
+
|
|
354
|
+
class EmailPreviewsTest < ActiveSupport::TestCase
|
|
355
|
+
include ActiveMail::Quality::Minitest
|
|
356
|
+
|
|
357
|
+
assert_quality_for_all_previews # one test per preview email and locale
|
|
358
|
+
end
|
|
320
359
|
```
|
|
321
360
|
|
|
322
361
|
```ruby
|
|
@@ -326,19 +365,37 @@ require 'activemail/quality/rspec'
|
|
|
326
365
|
expect(rendered_html).to be_a_valid_email
|
|
327
366
|
```
|
|
328
367
|
|
|
329
|
-
The Guard checks byte size (Gmail clips ~102 KB),
|
|
330
|
-
table, `alt` on every image,
|
|
331
|
-
|
|
368
|
+
The Guard checks byte size (Gmail clips ~102 KB), parse errors,
|
|
369
|
+
`role="presentation"` on every table, `alt` on every image, `lang` on full
|
|
370
|
+
documents, a single `<html>`/`<body>`, missing translations, and — given a
|
|
371
|
+
message — a plain text part. Built-in rules can be disabled and your own added:
|
|
332
372
|
|
|
333
373
|
```ruby
|
|
374
|
+
signature = ActiveMail::Quality::Rule.html(:single_signature) do |doc, _html|
|
|
375
|
+
'signature rendered twice' if doc.css('.signature').size > 1
|
|
376
|
+
end
|
|
377
|
+
|
|
334
378
|
ActiveMail::Quality.configure do |c|
|
|
335
379
|
c.required_previews = %w[welcome_mailer#welcome]
|
|
336
|
-
c.guard = ActiveMail::Quality::Guard.new(max_bytes: 90_000)
|
|
380
|
+
c.guard = ActiveMail::Quality::Guard.new(max_bytes: 90_000, disable: %i[img_alt], rules: [signature])
|
|
381
|
+
c.locales = %i[fr en] # or ->(preview) { [...] }; each preview gets params[:locale]
|
|
382
|
+
c.formats = %i[html text]
|
|
383
|
+
c.around_each = lambda do |_preview, _email, _locale, &render|
|
|
384
|
+
ActiveRecord::Base.transaction { render.call; raise ActiveRecord::Rollback } # previews leave no data behind
|
|
385
|
+
end
|
|
386
|
+
c.output_filters = [[/\h{8}-\h{4}-\h{4}-\h{4}-\h{12}/, '<uuid>']] # applied to written files only
|
|
387
|
+
c.output_dir = 'tmp/email_previews' # relative, inside Rails.root
|
|
337
388
|
end
|
|
338
389
|
```
|
|
339
390
|
|
|
340
|
-
`
|
|
341
|
-
|
|
391
|
+
`Rule.html` blocks receive the parsed document and the raw HTML; `Rule.message`
|
|
392
|
+
blocks receive the `Mail::Message`. Return `nil`, a message or an array of
|
|
393
|
+
messages.
|
|
394
|
+
|
|
395
|
+
`rake activemail:emails:render_all` renders every ActionMailer preview (each
|
|
396
|
+
locale and format) to `output_dir` for visual diffing and fails on any guard
|
|
397
|
+
violation among `required_previews`. It only replaces its own `.html`/`.txt`
|
|
398
|
+
files.
|
|
342
399
|
|
|
343
400
|
## Email-client compatibility policy
|
|
344
401
|
|
|
@@ -356,7 +413,8 @@ Targets the real-world client landscape as of 2026:
|
|
|
356
413
|
## Programmatic use
|
|
357
414
|
|
|
358
415
|
```ruby
|
|
359
|
-
ActiveMail
|
|
416
|
+
ActiveMail.configuration.core.transpile('<container><row><columns>Hi</columns></row></container>')
|
|
417
|
+
ActiveMail::Pipeline.call(source) # transpile + inline
|
|
360
418
|
ActiveMail::Core.new(column_count: 24, container_width: 480).transpile(source)
|
|
361
419
|
```
|
|
362
420
|
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Upgrading to ActiveMail 2.0
|
|
2
|
+
|
|
3
|
+
## Gemfile
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gem 'activemail', '~> 2.0'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Inlining
|
|
10
|
+
|
|
11
|
+
The delivery interceptor is gone: each email is transpiled and inlined once,
|
|
12
|
+
when the mailer renders an inky HTML template.
|
|
13
|
+
|
|
14
|
+
- Remove `config.register_inline_interceptor` from your initializer.
|
|
15
|
+
- If you ran another inliner (premailer-rails) next to ActiveMail with
|
|
16
|
+
`config.inliner = :null`, drop that inliner and the `:null` setting: ActiveMail
|
|
17
|
+
now inlines the whole body, layout included.
|
|
18
|
+
- Inliner failures still raise `ActiveMail::Inliner::Error` (now a subclass of
|
|
19
|
+
`ActiveMail::Error`).
|
|
20
|
+
|
|
21
|
+
## Layouts
|
|
22
|
+
|
|
23
|
+
Ejected or hand-written layouts must embed the stylesheets with the helper and
|
|
24
|
+
drop `stylesheet_link_tag` (Premailer strips it, so it never styled anything):
|
|
25
|
+
|
|
26
|
+
```erb
|
|
27
|
+
<%# before %>
|
|
28
|
+
<%= stylesheet_link_tag "emails/emails" %>
|
|
29
|
+
<%= activemail_inline_styles %>
|
|
30
|
+
|
|
31
|
+
<%# after %>
|
|
32
|
+
<%= activemail_inline_styles "emails/emails" %>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A missing or empty asset now raises `ActiveMail::StylesheetNotFound` instead of
|
|
36
|
+
logging a warning; inside a view it surfaces as the `#cause` of
|
|
37
|
+
`ActionView::Template::Error`.
|
|
38
|
+
|
|
39
|
+
## SCSS
|
|
40
|
+
|
|
41
|
+
The framework uses the Sass module system. Replace `@import` and variables
|
|
42
|
+
declared before it with `@use … with (…)`; a variable declared before the
|
|
43
|
+
`@use` is no longer read.
|
|
44
|
+
|
|
45
|
+
```scss
|
|
46
|
+
// before
|
|
47
|
+
$am-button-secondary-color: #264653;
|
|
48
|
+
@import "activemail/activemail";
|
|
49
|
+
|
|
50
|
+
// after
|
|
51
|
+
@use "activemail/activemail" with (
|
|
52
|
+
$am-button-secondary-color: #264653
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- Renamed: `$am-button-color` → `$am-button-primary-color`,
|
|
57
|
+
`$am-info-box-bg` → `$am-info-box-background`.
|
|
58
|
+
- Prefer Ruby tokens (`secondary_text`, `secondary_border`, `info_box_*`, …):
|
|
59
|
+
they now reach both the inline styles and the stylesheet, so SCSS overrides of
|
|
60
|
+
the button/info-box variables are rarely needed.
|
|
61
|
+
- `$am-breakpoint` defaults to the container width (600px) instead of 596px.
|
|
62
|
+
|
|
63
|
+
## Buttons
|
|
64
|
+
|
|
65
|
+
`<button>` and `<cta>` share one rendering, styled inline from the tokens.
|
|
66
|
+
|
|
67
|
+
- CSS targeting the old markup (`<center>`, `.expander` cell inside
|
|
68
|
+
`.button.expand`) no longer matches; style buttons through tokens.
|
|
69
|
+
- A dark-mode override must use `!important` to beat the inline styles.
|
|
70
|
+
- A `<button>` without `href` renders its label in a `<span>`.
|
|
71
|
+
|
|
72
|
+
## Components
|
|
73
|
+
|
|
74
|
+
- The `<inky>` tag is removed.
|
|
75
|
+
- `inner` is an opaque slot: interpolate it exactly once and never parse or
|
|
76
|
+
alter it.
|
|
77
|
+
- `IGNORED_ON_PASSTHROUGH`, `combine_attributes`, `pass_through_attributes`,
|
|
78
|
+
`bulletproof_button_table` and `BUTTON_PADDING` are removed. Use
|
|
79
|
+
`root_attributes(node, classes:, layout_style:)` and override the private
|
|
80
|
+
`consumed_attributes` to list the attributes your component reads; a
|
|
81
|
+
component whose root is a `TABLE_RESET` table inherits from
|
|
82
|
+
`ActiveMail::Components::TableComponent`.
|
|
83
|
+
- A component must output `inner` exactly once (or have no children):
|
|
84
|
+
otherwise `ActiveMail::Error` is raised instead of silently dropping content.
|
|
85
|
+
Self-closing `<h-line/>`/`<spacer/>` are not valid HTML4 and swallow their
|
|
86
|
+
following siblings; write `<h-line></h-line>`.
|
|
87
|
+
- `id`, `aria-*`, `data-*` and other attributes now pass through on every
|
|
88
|
+
built-in component. On tables carrying `TABLE_RESET`, the component's
|
|
89
|
+
`role="presentation"` wins over the author's `role`.
|
|
90
|
+
- `ActiveMail::Core.new` only takes keywords (`config:`, `components:`,
|
|
91
|
+
`column_count:`, `container_width:`). For plain transpilation prefer the
|
|
92
|
+
memoized `ActiveMail.configuration.core`.
|
|
93
|
+
- Integer settings (`column_count`, `container_width`) raise `TypeError` for a
|
|
94
|
+
non-Integer and `ArgumentError` for a non-positive value. Numeric attributes
|
|
95
|
+
are parsed in strict base 10.
|
|
96
|
+
|
|
97
|
+
## Quality
|
|
98
|
+
|
|
99
|
+
- `output_dir` must be relative, inside `Rails.root` (symlinks resolved), and
|
|
100
|
+
neither `.` nor contain `..`. `render_all` marks it with an
|
|
101
|
+
`.activemail_previews` file and refuses to clean an existing, non-empty
|
|
102
|
+
directory without that marker: empty your previous output directory once.
|
|
103
|
+
- `PreviewRenderer.render` is removed.
|
|
104
|
+
- `RenderAll.new(root:, config:)` replaces `output_root:`.
|
|
105
|
+
- Rendered files and failures carry the locale when it differs from the
|
|
106
|
+
default (`welcome.fr.html`, `preview#email [fr]`).
|
|
107
|
+
- `Guard#violations` and `assert_email_quality` accept a `Mail::Message` (HTML
|
|
108
|
+
plus message rules) or an HTML string. New built-in rules may flag existing
|
|
109
|
+
emails: `translation_missing`, `translation_missing_text`, `single_root`,
|
|
110
|
+
`html_part_present`, `text_part_present`, `text_part_plain`. Disable any with
|
|
111
|
+
`Guard.new(disable: [...])`.
|
|
112
|
+
- Host-specific checks move to `Guard.new(rules: [Quality::Rule.html(...)])`.
|
|
@@ -1,62 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
.button table td,
|
|
4
|
-
.button a {
|
|
5
|
-
background: $am-button-primary-bg;
|
|
6
|
-
border-radius: $am-button-radius;
|
|
7
|
-
}
|
|
1
|
+
@use "sass:math";
|
|
2
|
+
@use "settings" as *;
|
|
8
3
|
|
|
9
|
-
.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
.expander {
|
|
5
|
+
padding: 0 !important;
|
|
6
|
+
visibility: hidden;
|
|
7
|
+
width: 0;
|
|
13
8
|
}
|
|
14
9
|
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
.info-box td {
|
|
11
|
+
background-color: $am-info-box-background;
|
|
12
|
+
border-left: 5px solid $am-info-box-border;
|
|
13
|
+
border-radius: $am-box-radius;
|
|
14
|
+
color: $am-info-box-text;
|
|
15
|
+
padding: $am-spacing-md;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
.callout-inner {
|
|
19
|
+
background-color: $am-background;
|
|
20
|
+
color: $am-text;
|
|
21
|
+
padding: $am-spacing-md;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
.h-line,
|
|
25
|
+
.h-line th {
|
|
26
|
+
font-size: 1px;
|
|
27
|
+
line-height: 1px;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
font-family: $am-font-family;
|
|
35
|
-
font-weight: bold;
|
|
30
|
+
.h-line {
|
|
31
|
+
border-top: 1px solid $am-border;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
background: $am-button-secondary-bg;
|
|
41
|
-
color: $am-button-secondary-color;
|
|
34
|
+
.menu {
|
|
35
|
+
width: auto !important;
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
.
|
|
45
|
-
|
|
38
|
+
.menu-item {
|
|
39
|
+
padding: $am-spacing-sm $am-spacing-md;
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
.
|
|
49
|
-
|
|
50
|
-
border-left: 5px solid $am-info-box-border;
|
|
51
|
-
border-radius: $am-box-radius;
|
|
52
|
-
color: $am-info-box-text;
|
|
53
|
-
padding: $am-spacing-md;
|
|
42
|
+
.menu-item a {
|
|
43
|
+
text-decoration: none;
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
@for $n from 1 through $am-column-count {
|
|
47
|
+
.block-grid.up-#{$n} > tbody > tr > th,
|
|
48
|
+
.block-grid.up-#{$n} > tbody > tr > td {
|
|
49
|
+
width: math.div(100%, $n);
|
|
50
|
+
}
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
// Height is set inline per instance; this rule is just a styling anchor.
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
// Dark mode: prefers-color-scheme (Apple Mail/iOS) + [data-ogsc] (Outlook
|
|
2
2
|
// Android). Colors derived from tokens — pure black/white crush contrast.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
$am-dark-surface: mix($am-color-text, $am-color-background, 92%) !default;
|
|
6
|
-
$am-dark-panel: mix($am-color-text, $am-color-background, 82%) !default;
|
|
7
|
-
$am-dark-text: mix($am-color-background, $am-color-text, 88%) !default;
|
|
8
|
-
$am-dark-muted: $am-color-muted !default;
|
|
9
|
-
$am-dark-link: $am-color-primary !default;
|
|
4
|
+
@use "settings" as *;
|
|
10
5
|
|
|
11
6
|
@mixin am-dark-rules {
|
|
12
7
|
.body,
|
|
@@ -39,25 +34,33 @@ $am-dark-link: $am-color-primary !default;
|
|
|
39
34
|
|
|
40
35
|
.button table td,
|
|
41
36
|
.button a,
|
|
37
|
+
.button span,
|
|
42
38
|
.cta table td,
|
|
43
|
-
.cta a
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
.cta a,
|
|
40
|
+
.cta span {
|
|
41
|
+
background: $am-button-primary-bg !important;
|
|
42
|
+
color: $am-button-primary-color !important;
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
// Restore the secondary fill AND text — the primary rule above
|
|
49
|
-
//
|
|
45
|
+
// Restore the secondary fill AND text — the primary rule above would be unreadable
|
|
46
|
+
// on a light secondary fill.
|
|
50
47
|
.button.secondary table td,
|
|
51
48
|
.button.secondary a,
|
|
49
|
+
.button.secondary span,
|
|
52
50
|
.cta.secondary table td,
|
|
53
|
-
.cta.secondary a
|
|
54
|
-
|
|
51
|
+
.cta.secondary a,
|
|
52
|
+
.cta.secondary span {
|
|
53
|
+
background: $am-button-secondary-bg !important;
|
|
55
54
|
color: $am-button-secondary-color !important;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
// Panels keep their inline background unless overridden here, which would
|
|
59
58
|
// otherwise leave dark text on a dark background.
|
|
60
|
-
.callout-inner
|
|
59
|
+
.callout-inner {
|
|
60
|
+
background-color: $am-dark-panel !important;
|
|
61
|
+
color: $am-dark-text !important;
|
|
62
|
+
}
|
|
63
|
+
|
|
61
64
|
.info-box,
|
|
62
65
|
.info-box td {
|
|
63
66
|
background-color: $am-dark-panel !important;
|
|
@@ -102,21 +105,29 @@ $am-dark-link: $am-color-primary !default;
|
|
|
102
105
|
|
|
103
106
|
[data-ogsc] .button table td,
|
|
104
107
|
[data-ogsc] .button a,
|
|
108
|
+
[data-ogsc] .button span,
|
|
105
109
|
[data-ogsc] .cta table td,
|
|
106
|
-
[data-ogsc] .cta a
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
[data-ogsc] .cta a,
|
|
111
|
+
[data-ogsc] .cta span {
|
|
112
|
+
background: $am-button-primary-bg !important;
|
|
113
|
+
color: $am-button-primary-color !important;
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
[data-ogsc] .button.secondary table td,
|
|
112
117
|
[data-ogsc] .button.secondary a,
|
|
118
|
+
[data-ogsc] .button.secondary span,
|
|
113
119
|
[data-ogsc] .cta.secondary table td,
|
|
114
|
-
[data-ogsc] .cta.secondary a
|
|
115
|
-
|
|
120
|
+
[data-ogsc] .cta.secondary a,
|
|
121
|
+
[data-ogsc] .cta.secondary span {
|
|
122
|
+
background: $am-button-secondary-bg !important;
|
|
116
123
|
color: $am-button-secondary-color !important;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
[data-ogsc] .callout-inner
|
|
126
|
+
[data-ogsc] .callout-inner {
|
|
127
|
+
background-color: $am-dark-panel !important;
|
|
128
|
+
color: $am-dark-text !important;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
[data-ogsc] .info-box,
|
|
121
132
|
[data-ogsc] .info-box td {
|
|
122
133
|
background-color: $am-dark-panel !important;
|