activemail 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -1
- data/README.md +44 -17
- data/UPGRADING.md +66 -0
- data/app/assets/stylesheets/activemail/_dark.scss +17 -8
- data/app/assets/stylesheets/activemail/_grid.scss +38 -14
- data/app/assets/stylesheets/activemail/_settings.scss +1 -0
- data/app/views/layouts/activemail/_document_head.html.erb +18 -0
- data/app/views/layouts/activemail/mailer.html.inky-erb +1 -17
- data/lib/activemail/components/button_renderer.rb +8 -4
- data/lib/activemail/components/center.rb +8 -1
- data/lib/activemail/components/columns.rb +32 -8
- data/lib/activemail/components/container.rb +2 -2
- data/lib/activemail/inliner/premailer/document.rb +100 -0
- data/lib/activemail/inliner/premailer.rb +3 -2
- data/lib/activemail/quality/builtin_message_rules.rb +58 -0
- data/lib/activemail/quality/builtin_rules.rb +41 -30
- data/lib/activemail/quality/configuration.rb +31 -16
- data/lib/activemail/quality/guard.rb +34 -14
- data/lib/activemail/quality/minitest.rb +24 -11
- data/lib/activemail/quality/output_filter.rb +24 -0
- data/lib/activemail/quality/render_all.rb +4 -4
- data/lib/activemail/quality/rspec.rb +11 -6
- data/lib/activemail/quality/rule_exemptions.rb +49 -0
- data/lib/activemail/tokens/defaults.rb +2 -1
- data/lib/activemail/version.rb +1 -1
- data/lib/generators/activemail/templates/mailer_layout.html.inky-erb +3 -17
- data/lib/generators/activemail/templates/mailer_layout.html.inky-haml +3 -9
- data/lib/generators/activemail/templates/mailer_layout.html.inky-slim +3 -8
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec710853bef9007bed632f4224ed565c37014a0a8749917b921d2fb1172ee4f2
|
|
4
|
+
data.tar.gz: 6b9bd91c3e8590a65cc7d8389dda58290b2ebe2e228ad20f400d3e371cd00213
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07e90ca61f098953fd6c76ff0eb57d77b6165c9a597fec90362c878cfea84d7e0c017f8d0284dad025e555714cace1f93558845aefce50b7a9f1d75bab8af273
|
|
7
|
+
data.tar.gz: ee42a0bcb06d56a651495c293a776cfed17642e09ae7e30220b9dc5db1d2a93c86d2bbbb894e52057aecc6afb97c91f659d188ec10908dfe16dc629ba188920c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,65 @@ 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.
|
|
8
|
+
## [2.1.1] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `<center>` adds `align="center"` and `.float-center` to its direct children and
|
|
13
|
+
nested menu items again, as in 2.0: since 2.1.0 a centered table, image or
|
|
14
|
+
paragraph stayed left-aligned and paragraphs got their default margins back.
|
|
15
|
+
|
|
16
|
+
## [2.1.0] - 2026-10-19
|
|
17
|
+
|
|
18
|
+
Behaviour changes to review before upgrading: see [UPGRADING.md](UPGRADING.md).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `container_gutter` spacing token (16px) and `$am-container-gutter`: the first
|
|
23
|
+
and last column of a row carry it as an outer, border-box, inlined padding;
|
|
24
|
+
nested columns do not repeat it.
|
|
25
|
+
- `<title>` in the layout head, set to the message subject (new
|
|
26
|
+
`layouts/activemail/_document_head` partial).
|
|
27
|
+
- Quality rules, on by default and disableable: `title`, `anchor_href`,
|
|
28
|
+
`placeholder` (`BuiltinRules::PLACEHOLDER`), `unsupported_color`,
|
|
29
|
+
`text_entities`.
|
|
30
|
+
- `Quality::Configuration#allow_declined` and the `allow_declined:` option of
|
|
31
|
+
`Guard#violations`, `Guard#valid?`, `assert_email_quality` and
|
|
32
|
+
`be_a_valid_email`.
|
|
33
|
+
- `Quality::Configuration#rule_exemptions` (by preview name or `preview#email`) and
|
|
34
|
+
the `skip:` option of `Guard#violations`, `Guard#valid?`, `assert_email_quality`
|
|
35
|
+
and `be_a_valid_email`: exempt a preview from named rules, built-in or custom,
|
|
36
|
+
without disabling them for every preview.
|
|
37
|
+
- `mso-padding-alt` on the button cell so Outlook keeps the padding.
|
|
38
|
+
- Below the container width, stacked columns take the container gutter on both
|
|
39
|
+
sides in clients that keep media queries.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- A nested `<columns>` ghost cell and `max-width` are computed against the
|
|
44
|
+
content box of the enclosing columns (their share minus their padding)
|
|
45
|
+
instead of the container.
|
|
46
|
+
- The MSO ghost cell of a column wraps it in its own `<table><tr>` (the `<th>`
|
|
47
|
+
used to sit directly in a `<td>`).
|
|
48
|
+
- Conditional comments use `<!--[if mso]>` instead of `<!--[if mso | IE]>`.
|
|
49
|
+
- `<button>` and `<cta>`: the fill and the radius live on the cell only; the
|
|
50
|
+
`<a>` carries `display:inline-block`, the body font and a `1.25` line height.
|
|
51
|
+
The dark-mode fill targets the cell.
|
|
52
|
+
- `<center>` renders a `<center align="center" class="float-center">` and no
|
|
53
|
+
longer rewrites its children (reverted in 2.1.1).
|
|
54
|
+
- A mailer that declines to send (`NullMail`) is a `declined` violation unless
|
|
55
|
+
allowed.
|
|
56
|
+
- The generated layouts render the same `_document_head`, `_head` and `_footer`
|
|
57
|
+
partials as the gem layout.
|
|
58
|
+
- With Premailer the parsed framework CSS is cached per stylesheet fingerprint
|
|
59
|
+
(a FIFO of 16) instead of being re-parsed for every email.
|
|
60
|
+
|
|
61
|
+
### Removed
|
|
62
|
+
|
|
63
|
+
- The `.columns.first.last` SCSS rule: a lone column now takes both container
|
|
64
|
+
gutters through `.first` and `.last`.
|
|
65
|
+
|
|
66
|
+
## [2.0.0] - 2026-10-16
|
|
9
67
|
|
|
10
68
|
Breaking release: see [UPGRADING.md](UPGRADING.md).
|
|
11
69
|
|
|
@@ -191,5 +249,8 @@ toolkit for Rails.
|
|
|
191
249
|
- Rails `>= 7.1` (tested up to 8.1).
|
|
192
250
|
- Nokogiri `>= 1.16`.
|
|
193
251
|
|
|
252
|
+
[2.1.1]: https://github.com/AdVitam/activemail/compare/v2.1.0...v2.1.1
|
|
253
|
+
[2.1.0]: https://github.com/AdVitam/activemail/compare/v2.0.0...v2.1.0
|
|
254
|
+
[2.0.0]: https://github.com/AdVitam/activemail/compare/v1.2.3...v2.0.0
|
|
194
255
|
[1.0.1]: https://github.com/AdVitam/activemail/compare/v1.0.0...v1.0.1
|
|
195
256
|
[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', '~> 2.
|
|
45
|
+
gem 'activemail', '~> 2.1'
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
```bash
|
|
@@ -136,7 +136,9 @@ Override at three levels, cheapest first:
|
|
|
136
136
|
3. **`bin/rails g activemail:views`** — eject the default layout + partials
|
|
137
137
|
(`app/views/layouts/activemail/*`); a same-named file in your app wins by
|
|
138
138
|
Rails path precedence. Put your logo/header/footer here — those are the app's
|
|
139
|
-
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.
|
|
140
142
|
|
|
141
143
|
Dark mode ships on: a `<style>` block keys off `prefers-color-scheme: dark` (Apple
|
|
142
144
|
Mail/iOS) and Outlook's `[data-ogsc]`, with surfaces derived from your tokens.
|
|
@@ -147,7 +149,9 @@ Email clients (Gmail mobile, Orange.fr) strip `<style>`, so critical CSS must be
|
|
|
147
149
|
inlined. When a mailer renders an inky HTML template, ActiveMail transpiles the
|
|
148
150
|
whole body (view + layout) and runs the configured inliner once. Text parts,
|
|
149
151
|
attachments and non-ActiveMail emails are never touched; your `<style>`
|
|
150
|
-
enhancement block (media queries, dark mode) is preserved.
|
|
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.
|
|
151
155
|
|
|
152
156
|
The layout embeds the compiled stylesheets with `activemail_inline_styles`
|
|
153
157
|
(Sprockets or Propshaft); pass your own on top of the framework's:
|
|
@@ -182,11 +186,11 @@ Fluid-hybrid wrapper, capped at `container_width`, wrapped in an MSO ghost table
|
|
|
182
186
|
```
|
|
183
187
|
|
|
184
188
|
```html
|
|
185
|
-
<!--[if mso
|
|
189
|
+
<!--[if mso]><table role="presentation" align="center" width="600">...<![endif]-->
|
|
186
190
|
<table class="container" align="center" style="width:100%;max-width:600px;margin:0 auto;">
|
|
187
191
|
<tbody><tr><td>...</td></tr></tbody>
|
|
188
192
|
</table>
|
|
189
|
-
<!--[if mso
|
|
193
|
+
<!--[if mso]></td></tr></table><![endif]-->
|
|
190
194
|
```
|
|
191
195
|
|
|
192
196
|
### `<row>` / `<columns>`
|
|
@@ -201,16 +205,24 @@ media-query enhancement.
|
|
|
201
205
|
```
|
|
202
206
|
|
|
203
207
|
Column widths are computed as `container_width × large / column_count`, capped
|
|
204
|
-
at `container_width
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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`).
|
|
208
219
|
|
|
209
220
|
### `<button>`
|
|
210
221
|
|
|
211
222
|
Bulletproof button styled inline from the tokens (`Tokens#button_style`): the
|
|
212
|
-
padding lives on the `<a>` so the whole control is clickable
|
|
213
|
-
|
|
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;
|
|
214
226
|
`expand` makes it full width; without `href` the label renders as a `<span>`.
|
|
215
227
|
|
|
216
228
|
```html
|
|
@@ -220,8 +232,8 @@ border sits on the cell only. `class="secondary"` selects the secondary variant;
|
|
|
220
232
|
```html
|
|
221
233
|
<table class="secondary button"><tbody><tr><td>
|
|
222
234
|
<table><tbody><tr>
|
|
223
|
-
<td style="background:#264653;border-radius:4px;">
|
|
224
|
-
<a href="#" style="display:inline-block;text-decoration:none;padding:12px 24px;
|
|
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>
|
|
225
237
|
</td>
|
|
226
238
|
</tr></tbody></table>
|
|
227
239
|
</td></tr></tbody></table>
|
|
@@ -258,7 +270,7 @@ border sits on the cell only. `class="secondary"` selects the secondary variant;
|
|
|
258
270
|
|
|
259
271
|
- `<wrapper class="header">` → `<table class="header wrapper" align="center" style="width:100%;">` with a `.wrapper-inner` cell.
|
|
260
272
|
- `<h-line>` → a full-width single-cell table drawn as a 1px rule in the `border` color.
|
|
261
|
-
- `<center>` adds `align="center"` and `.float-center` to its
|
|
273
|
+
- `<center>` renders a `<center align="center" class="float-center">` and adds `align="center"` and `.float-center` to its direct children and nested menu items.
|
|
262
274
|
|
|
263
275
|
### `<raw>`
|
|
264
276
|
|
|
@@ -367,8 +379,18 @@ expect(rendered_html).to be_a_valid_email
|
|
|
367
379
|
|
|
368
380
|
The Guard checks byte size (Gmail clips ~102 KB), parse errors,
|
|
369
381
|
`role="presentation"` on every table, `alt` on every image, `lang` on full
|
|
370
|
-
documents, a single `<html>`/`<body>`, missing translations,
|
|
371
|
-
|
|
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.
|
|
372
394
|
|
|
373
395
|
```ruby
|
|
374
396
|
signature = ActiveMail::Quality::Rule.html(:single_signature) do |doc, _html|
|
|
@@ -377,6 +399,11 @@ end
|
|
|
377
399
|
|
|
378
400
|
ActiveMail::Quality.configure do |c|
|
|
379
401
|
c.required_previews = %w[welcome_mailer#welcome]
|
|
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
|
+
}
|
|
380
407
|
c.guard = ActiveMail::Quality::Guard.new(max_bytes: 90_000, disable: %i[img_alt], rules: [signature])
|
|
381
408
|
c.locales = %i[fr en] # or ->(preview) { [...] }; each preview gets params[:locale]
|
|
382
409
|
c.formats = %i[html text]
|
data/UPGRADING.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
# Upgrading to ActiveMail 2.1
|
|
2
|
+
|
|
3
|
+
## Gemfile
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gem 'activemail', '~> 2.1'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Outer gutter
|
|
10
|
+
|
|
11
|
+
Rows now keep content off the screen edge: the first and last column of a row
|
|
12
|
+
get a 16px (`container_gutter`) border-box padding on their outer side, inlined
|
|
13
|
+
like every other framework rule. Columns still sum to `container_width`; the
|
|
14
|
+
text area is 16px narrower on each side.
|
|
15
|
+
|
|
16
|
+
- To keep edge-to-edge columns: `config.tokens.spacing :container_gutter, '0px'`
|
|
17
|
+
(or `$am-container-gutter: 0`).
|
|
18
|
+
- Content you padded by hand to avoid the edge now has the padding twice:
|
|
19
|
+
remove it.
|
|
20
|
+
- A nested row does not repeat the outer gutter. The `.columns.first.last` SCSS
|
|
21
|
+
rule is gone; ejected `_grid.scss` copies (`activemail:styles`) must be
|
|
22
|
+
re-ejected.
|
|
23
|
+
|
|
24
|
+
## Outlook markup
|
|
25
|
+
|
|
26
|
+
- Conditional comments are `<!--[if mso]>` (`mso` alone targets Outlook
|
|
27
|
+
for Windows). Tests matching `[if mso | IE]` must change.
|
|
28
|
+
- Each column ghost cell wraps its column in a `<table><tr>`. A nested column
|
|
29
|
+
is sized against the content box of its enclosing column: `large="6"` in a
|
|
30
|
+
lone `large="6"` is 134px (300px minus two 16px gutters, halved), not 300px.
|
|
31
|
+
|
|
32
|
+
## Buttons
|
|
33
|
+
|
|
34
|
+
- The fill and the radius are on the cell, not on the `<a>`. The `<a>` carries
|
|
35
|
+
`display:inline-block`, `font-family` (the `body` font token) and
|
|
36
|
+
`line-height:1.25`; the cell gets `mso-padding-alt`.
|
|
37
|
+
- A dark-mode override of the button fill must target `table td`, not `a`.
|
|
38
|
+
|
|
39
|
+
## Layouts
|
|
40
|
+
|
|
41
|
+
The `<head>` of the default layout moved to `layouts/activemail/_document_head`,
|
|
42
|
+
with `<title><%= message.subject %></title>`. The generated layouts
|
|
43
|
+
(`activemail:install`, `--haml`, `--slim`) render that partial and the `_head`
|
|
44
|
+
and `_footer` partials: regenerate yours or add the `<title>`. The partial reads
|
|
45
|
+
`message`, so it only renders inside a mailer view.
|
|
46
|
+
|
|
47
|
+
## Quality
|
|
48
|
+
|
|
49
|
+
New built-in rules flag emails that passed before: `title`, `anchor_href`,
|
|
50
|
+
`placeholder`, `unsupported_color`, `text_entities`. Disable any with
|
|
51
|
+
`Guard.new(disable: [...])`.
|
|
52
|
+
|
|
53
|
+
A `NullMail` (a mailer that returned without sending) is now a `declined`
|
|
54
|
+
violation, in `Guard`, `render_all`, `assert_email_quality`,
|
|
55
|
+
`assert_quality_for_all_previews` and `be_a_valid_email`. List the previews that
|
|
56
|
+
decline on purpose:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
ActiveMail::Quality.configure { |c| c.allow_declined = %w[reminder_mailer#maybe_skipped] }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or pass `allow_declined: true` to `Guard#violations`, `Guard#valid?`,
|
|
63
|
+
`assert_email_quality` and `be_a_valid_email`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
1
67
|
# Upgrading to ActiveMail 2.0
|
|
2
68
|
|
|
3
69
|
## Gemfile
|
|
@@ -32,25 +32,30 @@
|
|
|
32
32
|
color: $am-dark-link !important;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// The fill lives on the cell alone: a filled link would square off its rounded corners.
|
|
35
36
|
.button table td,
|
|
37
|
+
.cta table td {
|
|
38
|
+
background: $am-button-primary-bg !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
.button a,
|
|
37
42
|
.button span,
|
|
38
|
-
.cta table td,
|
|
39
43
|
.cta a,
|
|
40
44
|
.cta span {
|
|
41
|
-
background: $am-button-primary-bg !important;
|
|
42
45
|
color: $am-button-primary-color !important;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// Restore the secondary fill AND text — the primary rule above would be unreadable
|
|
46
49
|
// on a light secondary fill.
|
|
47
50
|
.button.secondary table td,
|
|
51
|
+
.cta.secondary table td {
|
|
52
|
+
background: $am-button-secondary-bg !important;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
.button.secondary a,
|
|
49
56
|
.button.secondary span,
|
|
50
|
-
.cta.secondary table td,
|
|
51
57
|
.cta.secondary a,
|
|
52
58
|
.cta.secondary span {
|
|
53
|
-
background: $am-button-secondary-bg !important;
|
|
54
59
|
color: $am-button-secondary-color !important;
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -104,22 +109,26 @@
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
[data-ogsc] .button table td,
|
|
112
|
+
[data-ogsc] .cta table td {
|
|
113
|
+
background: $am-button-primary-bg !important;
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
[data-ogsc] .button a,
|
|
108
117
|
[data-ogsc] .button span,
|
|
109
|
-
[data-ogsc] .cta table td,
|
|
110
118
|
[data-ogsc] .cta a,
|
|
111
119
|
[data-ogsc] .cta span {
|
|
112
|
-
background: $am-button-primary-bg !important;
|
|
113
120
|
color: $am-button-primary-color !important;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
[data-ogsc] .button.secondary table td,
|
|
124
|
+
[data-ogsc] .cta.secondary table td {
|
|
125
|
+
background: $am-button-secondary-bg !important;
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
[data-ogsc] .button.secondary a,
|
|
118
129
|
[data-ogsc] .button.secondary span,
|
|
119
|
-
[data-ogsc] .cta.secondary table td,
|
|
120
130
|
[data-ogsc] .cta.secondary a,
|
|
121
131
|
[data-ogsc] .cta.secondary span {
|
|
122
|
-
background: $am-button-secondary-bg !important;
|
|
123
132
|
color: $am-button-secondary-color !important;
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
@use "settings" as *;
|
|
2
2
|
|
|
3
|
-
// Gutters are
|
|
4
|
-
// none, and they must not depend on media queries (stripped by Gmail mobile).
|
|
3
|
+
// Gutters are plain padding, inlined: Gmail mobile strips media queries.
|
|
5
4
|
|
|
6
5
|
.container {
|
|
7
6
|
width: 100%;
|
|
@@ -17,11 +16,8 @@
|
|
|
17
16
|
width: 100%;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// box-sizing keeps the gutter inside the inline max-width — without it a 300px
|
|
23
|
-
// 2-col cell + 8px padding becomes 316px and the row overflows ~616px, wrapping
|
|
24
|
-
// in clients that honor inline padding.
|
|
19
|
+
// border-box keeps each gutter inside the inline max-width, else the row overflows and wraps;
|
|
20
|
+
// the container takes no padding for the same reason.
|
|
25
21
|
.columns {
|
|
26
22
|
box-sizing: border-box;
|
|
27
23
|
padding-left: $am-gutter;
|
|
@@ -29,23 +25,51 @@
|
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
.columns.first {
|
|
32
|
-
padding-left:
|
|
28
|
+
padding-left: $am-container-gutter;
|
|
33
29
|
}
|
|
34
30
|
|
|
35
31
|
.columns.last {
|
|
36
|
-
padding-right:
|
|
32
|
+
padding-right: $am-container-gutter;
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
// A
|
|
40
|
-
.columns.first
|
|
35
|
+
// A nested row already sits inside its parent column's padding.
|
|
36
|
+
.columns .columns.first {
|
|
41
37
|
padding-left: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.columns .columns.last {
|
|
42
41
|
padding-right: 0;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
//
|
|
46
|
-
// direct children of .row — the real markup nests them as <th class="columns">
|
|
47
|
-
// under tbody > tr — so this must be a descendant selector, not a child one.
|
|
44
|
+
// Descendant selector: columns render as <th> under tbody > tr, not as children of .row.
|
|
48
45
|
.row.collapse .columns {
|
|
49
46
|
padding-left: 0;
|
|
50
47
|
padding-right: 0;
|
|
51
48
|
}
|
|
49
|
+
|
|
50
|
+
// Below the container width every full row stacks, so each top-level column edge is an outer one.
|
|
51
|
+
// Nested columns may still sit side by side and keep the regular gutters. Gmail strips this query.
|
|
52
|
+
@media only screen and (max-width: $am-breakpoint - 1px) {
|
|
53
|
+
.columns {
|
|
54
|
+
padding-left: $am-container-gutter !important;
|
|
55
|
+
padding-right: $am-container-gutter !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.columns .columns {
|
|
59
|
+
padding-left: $am-gutter !important;
|
|
60
|
+
padding-right: $am-gutter !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.columns .columns.first {
|
|
64
|
+
padding-left: 0 !important;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.columns .columns.last {
|
|
68
|
+
padding-right: 0 !important;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.row.collapse .columns {
|
|
72
|
+
padding-left: 0 !important;
|
|
73
|
+
padding-right: 0 !important;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -34,6 +34,7 @@ $am-box-radius: $am-radius-box !default;
|
|
|
34
34
|
// aligned with the transpiled markup (ghost tables, column max-width).
|
|
35
35
|
$am-container-width: $am-grid-container-width !default;
|
|
36
36
|
$am-gutter: $am-spacing-sm !default;
|
|
37
|
+
$am-container-gutter: $am-spacing-container-gutter !default;
|
|
37
38
|
$am-breakpoint: $am-container-width !default;
|
|
38
39
|
|
|
39
40
|
// Dark mode
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<head>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
5
|
+
<meta name="color-scheme" content="light dark">
|
|
6
|
+
<meta name="supported-color-schemes" content="light dark">
|
|
7
|
+
<title><%= message.subject %></title>
|
|
8
|
+
<!--[if mso]>
|
|
9
|
+
<noscript>
|
|
10
|
+
<xml>
|
|
11
|
+
<o:OfficeDocumentSettings>
|
|
12
|
+
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
13
|
+
</o:OfficeDocumentSettings>
|
|
14
|
+
</xml>
|
|
15
|
+
</noscript>
|
|
16
|
+
<![endif]-->
|
|
17
|
+
<%= activemail_inline_styles %>
|
|
18
|
+
</head>
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="<%= I18n.locale %>" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
3
|
-
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<meta name="x-apple-disable-message-reformatting">
|
|
7
|
-
<meta name="color-scheme" content="light dark">
|
|
8
|
-
<meta name="supported-color-schemes" content="light dark">
|
|
9
|
-
<!--[if mso]>
|
|
10
|
-
<noscript>
|
|
11
|
-
<xml>
|
|
12
|
-
<o:OfficeDocumentSettings>
|
|
13
|
-
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
14
|
-
</o:OfficeDocumentSettings>
|
|
15
|
-
</xml>
|
|
16
|
-
</noscript>
|
|
17
|
-
<![endif]-->
|
|
18
|
-
<%= activemail_inline_styles %>
|
|
19
|
-
</head>
|
|
3
|
+
<%= render "layouts/activemail/document_head" %>
|
|
20
4
|
|
|
21
5
|
<body style="margin:0;padding:0;word-spacing:normal;">
|
|
22
6
|
<div role="article" aria-roledescription="email" lang="<%= I18n.locale %>" style="width:100%;">
|
|
@@ -14,6 +14,8 @@ module ActiveMail
|
|
|
14
14
|
abstract!
|
|
15
15
|
|
|
16
16
|
CONTROL_PADDING = 'padding:12px 24px;'
|
|
17
|
+
MSO_CELL_PADDING = 'mso-padding-alt:12px 24px;'
|
|
18
|
+
LINE_HEIGHT = '1.25'
|
|
17
19
|
|
|
18
20
|
sig { abstract.returns(String) }
|
|
19
21
|
def root_class; end
|
|
@@ -38,7 +40,8 @@ module ActiveMail
|
|
|
38
40
|
LINK_ATTRIBUTES
|
|
39
41
|
end
|
|
40
42
|
|
|
41
|
-
# Padding on the control makes the whole button a clickable target
|
|
43
|
+
# Padding on the control makes the whole button a clickable target; Outlook Word
|
|
44
|
+
# ignores it there, so the cell repeats it as `mso-padding-alt`.
|
|
42
45
|
sig { params(node: Nokogiri::XML::Node, inner: String, style: ActiveMail::Tokens::ButtonStyle, expand: T::Boolean).returns(String) }
|
|
43
46
|
def control(node, inner, style, expand)
|
|
44
47
|
css = control_style(style, expand)
|
|
@@ -48,17 +51,18 @@ module ActiveMail
|
|
|
48
51
|
anchor(node, inner, before: extra, after: style_attribute(node, css))
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
#
|
|
54
|
+
# Fill, corners and outline live on the cell only: repeating them on the control would double them.
|
|
52
55
|
sig { params(style: ActiveMail::Tokens::ButtonStyle, expand: T::Boolean).returns(String) }
|
|
53
56
|
def cell_style(style, expand)
|
|
54
57
|
border = style.border ? "border:1px solid #{style.border};" : ''
|
|
55
|
-
"background:#{style.background};border-radius:#{style.radius};#{border}#{'text-align:center;' if expand}"
|
|
58
|
+
"background:#{style.background};border-radius:#{style.radius};#{border}#{MSO_CELL_PADDING}#{'text-align:center;' if expand}"
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
sig { params(style: ActiveMail::Tokens::ButtonStyle, expand: T::Boolean).returns(String) }
|
|
59
62
|
def control_style(style, expand)
|
|
60
63
|
"display:#{expand ? 'block' : 'inline-block'};text-decoration:none;#{CONTROL_PADDING}" \
|
|
61
|
-
"
|
|
64
|
+
"font-family:#{escape_attr(core.config.tokens.font!(:body))};line-height:#{LINE_HEIGHT};" \
|
|
65
|
+
"color:#{style.color};font-weight:bold;"
|
|
62
66
|
end
|
|
63
67
|
end
|
|
64
68
|
end
|
|
@@ -15,7 +15,14 @@ module ActiveMail
|
|
|
15
15
|
child['class'] = combine_classes(child, 'float-center')
|
|
16
16
|
end
|
|
17
17
|
node.css('.menu-item').each { |item| item['class'] = combine_classes(item, 'float-center') }
|
|
18
|
-
%(<center#{root_attributes(node, classes:
|
|
18
|
+
%(<center#{root_attributes(node, classes: 'float-center')} align="center">#{inner}</center>)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
sig { override.returns(T::Array[String]) }
|
|
24
|
+
def consumed_attributes
|
|
25
|
+
['align']
|
|
19
26
|
end
|
|
20
27
|
end
|
|
21
28
|
end
|
|
@@ -11,12 +11,12 @@ module ActiveMail
|
|
|
11
11
|
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
12
|
def transform(node, inner)
|
|
13
13
|
small, large = column_sizes(node)
|
|
14
|
-
width_px = ghost_width(large)
|
|
14
|
+
width_px = ghost_width(node, large)
|
|
15
15
|
|
|
16
16
|
[
|
|
17
|
-
%(<!--[if mso
|
|
17
|
+
%(<!--[if mso]><td width="#{width_px}" valign="top"><table #{TABLE_RESET} width="#{width_px}"><tr><![endif]-->),
|
|
18
18
|
column_markup(node, inner, column_classes(node, small, large), width_px, expander(node, large)),
|
|
19
|
-
'<!--[if mso
|
|
19
|
+
'<!--[if mso]></tr></table></td><![endif]-->'
|
|
20
20
|
].join
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -65,11 +65,35 @@ module ActiveMail
|
|
|
65
65
|
%(<th class="expander"></th>)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
sig { params(large: Integer).returns(Integer) }
|
|
71
|
-
def ghost_width(large)
|
|
72
|
-
|
|
68
|
+
# Columns are transformed leaves first, so the enclosing <columns> are still raw nodes whose
|
|
69
|
+
# sizes and positions are readable. A nested row shares the content box of its parent column.
|
|
70
|
+
sig { params(node: Nokogiri::XML::Node, large: Integer).returns(Integer) }
|
|
71
|
+
def ghost_width(node, large)
|
|
72
|
+
available = node.ancestors('columns').reverse.reduce(Rational(container_width)) do |width, ancestor|
|
|
73
|
+
share_of(width, column_sizes(ancestor).last) - horizontal_padding(ancestor)
|
|
74
|
+
end
|
|
75
|
+
share_of(available, large).round
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clamped: an oversized `large` would push the MSO ghost cell past its parent and force a wrap in Outlook Word.
|
|
79
|
+
sig { params(width: Rational, share: Integer).returns(Rational) }
|
|
80
|
+
def share_of(width, share)
|
|
81
|
+
width * Rational([share, column_count].min, column_count)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Mirrors _grid.scss, whose padding is inlined on each column.
|
|
85
|
+
sig { params(node: Nokogiri::XML::Node).returns(Rational) }
|
|
86
|
+
def horizontal_padding(node)
|
|
87
|
+
return Rational(0) if node.ancestors('row').any? { |row| class?(row, 'collapse') }
|
|
88
|
+
|
|
89
|
+
siblings = column_siblings(node)
|
|
90
|
+
outer_side = node.ancestors('columns').empty? ? spacing(:container_gutter) : Rational(0)
|
|
91
|
+
[siblings.first, siblings.last].sum(Rational(0)) { |edge| edge == node ? outer_side : spacing(:sm) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sig { params(name: Symbol).returns(Rational) }
|
|
95
|
+
def spacing(name)
|
|
96
|
+
Rational(core.config.tokens.spacing!(name).delete_suffix('px'))
|
|
73
97
|
end
|
|
74
98
|
|
|
75
99
|
sig { params(node: Nokogiri::XML::Node, inner: String, classes: String, width_px: Integer, expander: String).returns(String) }
|
|
@@ -14,10 +14,10 @@ module ActiveMail
|
|
|
14
14
|
attributes = root_attributes(node, classes: 'container', layout_style: "width:100%;max-width:#{width}px;margin:0 auto;")
|
|
15
15
|
# Outlook Word ignores max-width; the MSO ghost table pins the width.
|
|
16
16
|
[
|
|
17
|
-
%(<!--[if mso
|
|
17
|
+
%(<!--[if mso]><table #{TABLE_RESET} align="center" width="#{width}"><tr><td><![endif]-->),
|
|
18
18
|
%(<table#{attributes} #{TABLE_RESET} align="center">),
|
|
19
19
|
%(<tbody><tr><td>#{inner}</td></tr></tbody></table>),
|
|
20
|
-
'<!--[if mso
|
|
20
|
+
'<!--[if mso]></td></tr></table><![endif]-->'
|
|
21
21
|
].join
|
|
22
22
|
end
|
|
23
23
|
|