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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -2
  3. data/README.md +141 -56
  4. data/UPGRADING.md +185 -0
  5. data/app/assets/stylesheets/activemail/_components.scss +33 -42
  6. data/app/assets/stylesheets/activemail/_dark.scss +44 -24
  7. data/app/assets/stylesheets/activemail/_grid.scss +40 -14
  8. data/app/assets/stylesheets/activemail/_settings.scss +23 -10
  9. data/app/assets/stylesheets/activemail/_utilities.scss +3 -1
  10. data/app/assets/stylesheets/activemail/activemail.scss +6 -5
  11. data/app/helpers/active_mail/styles_helper.rb +14 -15
  12. data/app/views/layouts/activemail/_document_head.html.erb +18 -0
  13. data/app/views/layouts/activemail/mailer.html.inky-erb +1 -22
  14. data/lib/activemail/asset_pipeline/base.rb +47 -0
  15. data/lib/activemail/asset_pipeline/null.rb +19 -0
  16. data/lib/activemail/asset_pipeline/propshaft.rb +19 -0
  17. data/lib/activemail/asset_pipeline/sprockets.rb +39 -0
  18. data/lib/activemail/asset_pipeline.rb +35 -0
  19. data/lib/activemail/components/base.rb +35 -39
  20. data/lib/activemail/components/block_grid.rb +11 -4
  21. data/lib/activemail/components/button.rb +5 -28
  22. data/lib/activemail/components/button_renderer.rb +69 -0
  23. data/lib/activemail/components/callout.rb +4 -5
  24. data/lib/activemail/components/center.rb +10 -10
  25. data/lib/activemail/components/columns.rb +58 -15
  26. data/lib/activemail/components/container.rb +13 -6
  27. data/lib/activemail/components/cta.rb +9 -33
  28. data/lib/activemail/components/h_line.rb +3 -5
  29. data/lib/activemail/components/info_box.rb +6 -12
  30. data/lib/activemail/components/menu.rb +3 -4
  31. data/lib/activemail/components/menu_item.rb +10 -3
  32. data/lib/activemail/components/row.rb +3 -4
  33. data/lib/activemail/components/spacer.rb +16 -12
  34. data/lib/activemail/components/table_component.rb +23 -0
  35. data/lib/activemail/components/wrapper.rb +10 -4
  36. data/lib/activemail/configuration.rb +43 -57
  37. data/lib/activemail/errors.rb +15 -0
  38. data/lib/activemail/inliner/base.rb +2 -10
  39. data/lib/activemail/inliner/null.rb +0 -5
  40. data/lib/activemail/inliner/premailer/document.rb +100 -0
  41. data/lib/activemail/inliner/premailer.rb +3 -2
  42. data/lib/activemail/inliner/roadie.rb +1 -1
  43. data/lib/activemail/inliner.rb +33 -0
  44. data/lib/activemail/parse_error_reporter.rb +5 -4
  45. data/lib/activemail/pipeline.rb +26 -0
  46. data/lib/activemail/quality/builtin_message_rules.rb +58 -0
  47. data/lib/activemail/quality/builtin_rules.rb +165 -0
  48. data/lib/activemail/quality/configuration.rb +103 -8
  49. data/lib/activemail/quality/guard.rb +61 -83
  50. data/lib/activemail/quality/minitest.rb +56 -27
  51. data/lib/activemail/quality/output_dir.rb +52 -0
  52. data/lib/activemail/quality/output_filter.rb +24 -0
  53. data/lib/activemail/quality/preview_renderer.rb +46 -33
  54. data/lib/activemail/quality/render_all.rb +95 -39
  55. data/lib/activemail/quality/rspec.rb +14 -13
  56. data/lib/activemail/quality/rule.rb +47 -0
  57. data/lib/activemail/quality/rule_exemptions.rb +49 -0
  58. data/lib/activemail/rails/engine.rb +13 -13
  59. data/lib/activemail/rails/rendering.rb +51 -0
  60. data/lib/activemail/rails/template_handler.rb +3 -1
  61. data/lib/activemail/tokens/button_style.rb +1 -2
  62. data/lib/activemail/tokens/defaults.rb +78 -0
  63. data/lib/activemail/tokens/scss_serializer.rb +4 -4
  64. data/lib/activemail/tokens.rb +19 -58
  65. data/lib/activemail/version.rb +1 -1
  66. data/lib/activemail.rb +89 -61
  67. data/lib/generators/activemail/templates/component.rb.tt +2 -3
  68. data/lib/generators/activemail/templates/initializer.rb +0 -3
  69. data/lib/generators/activemail/templates/mailer_layout.html.inky-erb +3 -19
  70. data/lib/generators/activemail/templates/mailer_layout.html.inky-haml +3 -11
  71. data/lib/generators/activemail/templates/mailer_layout.html.inky-slim +3 -10
  72. data/lib/tasks/activemail.rake +4 -5
  73. metadata +22 -4
  74. data/lib/activemail/components/inky.rb +0 -18
  75. data/lib/activemail/inliner/interceptor.rb +0 -51
  76. data/lib/activemail/rails/compiled_stylesheet.rb +0 -62
data/UPGRADING.md ADDED
@@ -0,0 +1,185 @@
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
+ ## Center
40
+
41
+ `<center>` no longer adds `align="center"` and `.float-center` to its children
42
+ (nor to nested menu items): it renders one `<center align="center">` and leaves
43
+ its content as written. Put `class="float-center"` or `align="center"` on the
44
+ children that need it.
45
+
46
+ ## Layouts
47
+
48
+ The `<head>` of the default layout moved to `layouts/activemail/_document_head`,
49
+ with `<title><%= message.subject %></title>`. The generated layouts
50
+ (`activemail:install`, `--haml`, `--slim`) render that partial and the `_head`
51
+ and `_footer` partials: regenerate yours or add the `<title>`. The partial reads
52
+ `message`, so it only renders inside a mailer view.
53
+
54
+ ## Quality
55
+
56
+ New built-in rules flag emails that passed before: `title`, `anchor_href`,
57
+ `placeholder`, `unsupported_color`, `text_entities`. Disable any with
58
+ `Guard.new(disable: [...])`.
59
+
60
+ A `NullMail` (a mailer that returned without sending) is now a `declined`
61
+ violation, in `Guard`, `render_all`, `assert_email_quality`,
62
+ `assert_quality_for_all_previews` and `be_a_valid_email`. List the previews that
63
+ decline on purpose:
64
+
65
+ ```ruby
66
+ ActiveMail::Quality.configure { |c| c.allow_declined = %w[reminder_mailer#maybe_skipped] }
67
+ ```
68
+
69
+ or pass `allow_declined: true` to `Guard#violations`, `Guard#valid?`,
70
+ `assert_email_quality` and `be_a_valid_email`.
71
+
72
+ ---
73
+
74
+ # Upgrading to ActiveMail 2.0
75
+
76
+ ## Gemfile
77
+
78
+ ```ruby
79
+ gem 'activemail', '~> 2.0'
80
+ ```
81
+
82
+ ## Inlining
83
+
84
+ The delivery interceptor is gone: each email is transpiled and inlined once,
85
+ when the mailer renders an inky HTML template.
86
+
87
+ - Remove `config.register_inline_interceptor` from your initializer.
88
+ - If you ran another inliner (premailer-rails) next to ActiveMail with
89
+ `config.inliner = :null`, drop that inliner and the `:null` setting: ActiveMail
90
+ now inlines the whole body, layout included.
91
+ - Inliner failures still raise `ActiveMail::Inliner::Error` (now a subclass of
92
+ `ActiveMail::Error`).
93
+
94
+ ## Layouts
95
+
96
+ Ejected or hand-written layouts must embed the stylesheets with the helper and
97
+ drop `stylesheet_link_tag` (Premailer strips it, so it never styled anything):
98
+
99
+ ```erb
100
+ <%# before %>
101
+ <%= stylesheet_link_tag "emails/emails" %>
102
+ <%= activemail_inline_styles %>
103
+
104
+ <%# after %>
105
+ <%= activemail_inline_styles "emails/emails" %>
106
+ ```
107
+
108
+ A missing or empty asset now raises `ActiveMail::StylesheetNotFound` instead of
109
+ logging a warning; inside a view it surfaces as the `#cause` of
110
+ `ActionView::Template::Error`.
111
+
112
+ ## SCSS
113
+
114
+ The framework uses the Sass module system. Replace `@import` and variables
115
+ declared before it with `@use … with (…)`; a variable declared before the
116
+ `@use` is no longer read.
117
+
118
+ ```scss
119
+ // before
120
+ $am-button-secondary-color: #264653;
121
+ @import "activemail/activemail";
122
+
123
+ // after
124
+ @use "activemail/activemail" with (
125
+ $am-button-secondary-color: #264653
126
+ );
127
+ ```
128
+
129
+ - Renamed: `$am-button-color` → `$am-button-primary-color`,
130
+ `$am-info-box-bg` → `$am-info-box-background`.
131
+ - Prefer Ruby tokens (`secondary_text`, `secondary_border`, `info_box_*`, …):
132
+ they now reach both the inline styles and the stylesheet, so SCSS overrides of
133
+ the button/info-box variables are rarely needed.
134
+ - `$am-breakpoint` defaults to the container width (600px) instead of 596px.
135
+
136
+ ## Buttons
137
+
138
+ `<button>` and `<cta>` share one rendering, styled inline from the tokens.
139
+
140
+ - CSS targeting the old markup (`<center>`, `.expander` cell inside
141
+ `.button.expand`) no longer matches; style buttons through tokens.
142
+ - A dark-mode override must use `!important` to beat the inline styles.
143
+ - A `<button>` without `href` renders its label in a `<span>`.
144
+
145
+ ## Components
146
+
147
+ - The `<inky>` tag is removed.
148
+ - `inner` is an opaque slot: interpolate it exactly once and never parse or
149
+ alter it.
150
+ - `IGNORED_ON_PASSTHROUGH`, `combine_attributes`, `pass_through_attributes`,
151
+ `bulletproof_button_table` and `BUTTON_PADDING` are removed. Use
152
+ `root_attributes(node, classes:, layout_style:)` and override the private
153
+ `consumed_attributes` to list the attributes your component reads; a
154
+ component whose root is a `TABLE_RESET` table inherits from
155
+ `ActiveMail::Components::TableComponent`.
156
+ - A component must output `inner` exactly once (or have no children):
157
+ otherwise `ActiveMail::Error` is raised instead of silently dropping content.
158
+ Self-closing `<h-line/>`/`<spacer/>` are not valid HTML4 and swallow their
159
+ following siblings; write `<h-line></h-line>`.
160
+ - `id`, `aria-*`, `data-*` and other attributes now pass through on every
161
+ built-in component. On tables carrying `TABLE_RESET`, the component's
162
+ `role="presentation"` wins over the author's `role`.
163
+ - `ActiveMail::Core.new` only takes keywords (`config:`, `components:`,
164
+ `column_count:`, `container_width:`). For plain transpilation prefer the
165
+ memoized `ActiveMail.configuration.core`.
166
+ - Integer settings (`column_count`, `container_width`) raise `TypeError` for a
167
+ non-Integer and `ArgumentError` for a non-positive value. Numeric attributes
168
+ are parsed in strict base 10.
169
+
170
+ ## Quality
171
+
172
+ - `output_dir` must be relative, inside `Rails.root` (symlinks resolved), and
173
+ neither `.` nor contain `..`. `render_all` marks it with an
174
+ `.activemail_previews` file and refuses to clean an existing, non-empty
175
+ directory without that marker: empty your previous output directory once.
176
+ - `PreviewRenderer.render` is removed.
177
+ - `RenderAll.new(root:, config:)` replaces `output_root:`.
178
+ - Rendered files and failures carry the locale when it differs from the
179
+ default (`welcome.fr.html`, `preview#email [fr]`).
180
+ - `Guard#violations` and `assert_email_quality` accept a `Mail::Message` (HTML
181
+ plus message rules) or an HTML string. New built-in rules may flag existing
182
+ emails: `translation_missing`, `translation_missing_text`, `single_root`,
183
+ `html_part_present`, `text_part_present`, `text_part_plain`. Disable any with
184
+ `Guard.new(disable: [...])`.
185
+ - Host-specific checks move to `Guard.new(rules: [Quality::Rule.html(...)])`.
@@ -1,62 +1,53 @@
1
- // Background on both cell and link so the whole bulletproof button is filled and
2
- // clickable. Radius by default — progressive enhancement, clients that don't support it ignore it.
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
- .button a {
10
- color: $am-button-color;
11
- font-family: $am-font-family;
12
- font-weight: bold;
4
+ .expander {
5
+ padding: 0 !important;
6
+ visibility: hidden;
7
+ width: 0;
13
8
  }
14
9
 
15
- .button.secondary table td,
16
- .button.secondary a {
17
- background: $am-button-secondary-bg;
18
- color: $am-button-secondary-color;
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
- // Border on the cell only — a repeated <a> border would double the line.
22
- .button.secondary table td {
23
- border: 1px solid $am-button-secondary-border;
18
+ .callout-inner {
19
+ background-color: $am-background;
20
+ color: $am-text;
21
+ padding: $am-spacing-md;
24
22
  }
25
23
 
26
- .cta table td,
27
- .cta a {
28
- background: $am-button-primary-bg;
29
- border-radius: $am-button-radius;
24
+ .h-line,
25
+ .h-line th {
26
+ font-size: 1px;
27
+ line-height: 1px;
30
28
  }
31
29
 
32
- .cta a {
33
- color: $am-button-color;
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
- .cta.secondary table td,
39
- .cta.secondary a {
40
- background: $am-button-secondary-bg;
41
- color: $am-button-secondary-color;
34
+ .menu {
35
+ width: auto !important;
42
36
  }
43
37
 
44
- .cta.secondary table td {
45
- border: 1px solid $am-button-secondary-border;
38
+ .menu-item {
39
+ padding: $am-spacing-sm $am-spacing-md;
46
40
  }
47
41
 
48
- .info-box td {
49
- background-color: $am-info-box-bg;
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
- .callout-inner {
57
- background-color: $am-background;
58
- color: $am-text;
59
- padding: $am-spacing-md;
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
- $am-dark-bg: mix($am-color-text, $am-color-background, 96%) !default;
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,
@@ -37,27 +32,40 @@ $am-dark-link: $am-color-primary !default;
37
32
  color: $am-dark-link !important;
38
33
  }
39
34
 
35
+ // The fill lives on the cell alone: a filled link would square off its rounded corners.
40
36
  .button table td,
37
+ .cta table td {
38
+ background: $am-button-primary-bg !important;
39
+ }
40
+
41
41
  .button a,
42
- .cta table td,
43
- .cta a {
44
- background: $am-color-primary !important;
45
- color: $am-color-button-text !important;
42
+ .button span,
43
+ .cta a,
44
+ .cta span {
45
+ color: $am-button-primary-color !important;
46
46
  }
47
47
 
48
- // Restore the secondary fill AND text — the primary rule above forces button-text,
49
- // which would be unreadable on an outline (light) secondary fill.
48
+ // Restore the secondary fill AND text — the primary rule above would be unreadable
49
+ // on a light secondary fill.
50
50
  .button.secondary table td,
51
+ .cta.secondary table td {
52
+ background: $am-button-secondary-bg !important;
53
+ }
54
+
51
55
  .button.secondary a,
52
- .cta.secondary table td,
53
- .cta.secondary a {
54
- background: $am-color-secondary !important;
56
+ .button.secondary span,
57
+ .cta.secondary a,
58
+ .cta.secondary span {
55
59
  color: $am-button-secondary-color !important;
56
60
  }
57
61
 
58
62
  // Panels keep their inline background unless overridden here, which would
59
63
  // otherwise leave dark text on a dark background.
60
- .callout-inner,
64
+ .callout-inner {
65
+ background-color: $am-dark-panel !important;
66
+ color: $am-dark-text !important;
67
+ }
68
+
61
69
  .info-box,
62
70
  .info-box td {
63
71
  background-color: $am-dark-panel !important;
@@ -101,22 +109,34 @@ $am-dark-link: $am-color-primary !default;
101
109
  }
102
110
 
103
111
  [data-ogsc] .button table td,
112
+ [data-ogsc] .cta table td {
113
+ background: $am-button-primary-bg !important;
114
+ }
115
+
104
116
  [data-ogsc] .button a,
105
- [data-ogsc] .cta table td,
106
- [data-ogsc] .cta a {
107
- background: $am-color-primary !important;
108
- color: $am-color-button-text !important;
117
+ [data-ogsc] .button span,
118
+ [data-ogsc] .cta a,
119
+ [data-ogsc] .cta span {
120
+ color: $am-button-primary-color !important;
109
121
  }
110
122
 
111
123
  [data-ogsc] .button.secondary table td,
124
+ [data-ogsc] .cta.secondary table td {
125
+ background: $am-button-secondary-bg !important;
126
+ }
127
+
112
128
  [data-ogsc] .button.secondary a,
113
- [data-ogsc] .cta.secondary table td,
114
- [data-ogsc] .cta.secondary a {
115
- background: $am-color-secondary !important;
129
+ [data-ogsc] .button.secondary span,
130
+ [data-ogsc] .cta.secondary a,
131
+ [data-ogsc] .cta.secondary span {
116
132
  color: $am-button-secondary-color !important;
117
133
  }
118
134
 
119
- [data-ogsc] .callout-inner,
135
+ [data-ogsc] .callout-inner {
136
+ background-color: $am-dark-panel !important;
137
+ color: $am-dark-text !important;
138
+ }
139
+
120
140
  [data-ogsc] .info-box,
121
141
  [data-ogsc] .info-box td {
122
142
  background-color: $am-dark-panel !important;
@@ -1,5 +1,6 @@
1
- // Gutters are unconditional padding (inlined by the CSS inliner): the gem emits
2
- // none, and they must not depend on media queries (stripped by Gmail mobile).
1
+ @use "settings" as *;
2
+
3
+ // Gutters are plain padding, inlined: Gmail mobile strips media queries.
3
4
 
4
5
  .container {
5
6
  width: 100%;
@@ -15,11 +16,8 @@
15
16
  width: 100%;
16
17
  }
17
18
 
18
- // Gutters: half on each side, outer edges collapsed via first/last so content
19
- // stays flush with the container.
20
- // box-sizing keeps the gutter inside the inline max-width — without it a 300px
21
- // 2-col cell + 8px padding becomes 316px and the row overflows ~616px, wrapping
22
- // 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.
23
21
  .columns {
24
22
  box-sizing: border-box;
25
23
  padding-left: $am-gutter;
@@ -27,23 +25,51 @@
27
25
  }
28
26
 
29
27
  .columns.first {
30
- padding-left: 0;
28
+ padding-left: $am-container-gutter;
31
29
  }
32
30
 
33
31
  .columns.last {
34
- padding-right: 0;
32
+ padding-right: $am-container-gutter;
35
33
  }
36
34
 
37
- // A single full-width column carries no side gutter.
38
- .columns.first.last {
35
+ // A nested row already sits inside its parent column's padding.
36
+ .columns .columns.first {
39
37
  padding-left: 0;
38
+ }
39
+
40
+ .columns .columns.last {
40
41
  padding-right: 0;
41
42
  }
42
43
 
43
- // Collapse removes gutters on demand (e.g. edge-to-edge images). Columns are not
44
- // direct children of .row — the real markup nests them as <th class="columns">
45
- // 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.
46
45
  .row.collapse .columns {
47
46
  padding-left: 0;
48
47
  padding-right: 0;
49
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
+ }
@@ -1,5 +1,8 @@
1
- // All !default so a host app can pre-declare any value before importing the framework.
2
- @import "activemail/activemail_tokens";
1
+ // Sass only lets `with (...)` configure !default variables of forwarded modules:
2
+ // every configurable variable must live here or in the forwarded token bridge.
3
+ @forward "activemail_tokens";
4
+ @use "sass:color";
5
+ @use "activemail_tokens" as *;
3
6
 
4
7
  // Typography
5
8
  $am-font-family: $am-font-body !default;
@@ -11,19 +14,19 @@ $am-background: $am-color-background !default;
11
14
  $am-muted: $am-color-muted !default;
12
15
  $am-border: $am-color-border !default;
13
16
 
14
- // Buttons
17
+ // Buttons — a null border means no border at all.
15
18
  $am-button-primary-bg: $am-color-primary !default;
19
+ $am-button-primary-color: $am-color-primary-text !default;
20
+ $am-button-primary-border: $am-color-primary-border !default;
16
21
  $am-button-secondary-bg: $am-color-secondary !default;
17
- $am-button-color: $am-color-button-text !default;
18
- // Secondary falls back to filled-button defaults; override *-secondary-* for an outline variant.
19
- $am-button-secondary-color: $am-color-button-text !default;
20
- $am-button-secondary-border: transparent !default;
22
+ $am-button-secondary-color: $am-color-secondary-text !default;
23
+ $am-button-secondary-border: $am-color-secondary-border !default;
21
24
  $am-button-radius: $am-radius-button !default;
22
25
 
23
26
  // Info box
24
- $am-info-box-bg: $am-background !default;
25
- $am-info-box-border: $am-border !default;
26
- $am-info-box-text: $am-text !default;
27
+ $am-info-box-background: $am-color-info-box-background !default;
28
+ $am-info-box-border: $am-color-info-box-border !default;
29
+ $am-info-box-text: $am-color-info-box-text !default;
27
30
  $am-box-radius: $am-radius-box !default;
28
31
 
29
32
  // Grid — the gem emits no gutters; we provide them via column padding. Width
@@ -31,3 +34,13 @@ $am-box-radius: $am-radius-box !default;
31
34
  // aligned with the transpiled markup (ghost tables, column max-width).
32
35
  $am-container-width: $am-grid-container-width !default;
33
36
  $am-gutter: $am-spacing-sm !default;
37
+ $am-container-gutter: $am-spacing-container-gutter !default;
38
+ $am-breakpoint: $am-container-width !default;
39
+
40
+ // Dark mode
41
+ $am-dark-bg: color.mix($am-color-text, $am-color-background, 96%) !default;
42
+ $am-dark-surface: color.mix($am-color-text, $am-color-background, 92%) !default;
43
+ $am-dark-panel: color.mix($am-color-text, $am-color-background, 82%) !default;
44
+ $am-dark-text: color.mix($am-color-background, $am-color-text, 88%) !default;
45
+ $am-dark-muted: $am-color-muted !default;
46
+ $am-dark-link: $am-button-primary-bg !default;
@@ -1,3 +1,5 @@
1
+ @use "settings" as *;
2
+
1
3
  // Alignment and visibility utilities.
2
4
 
3
5
  .text-center {
@@ -32,7 +34,7 @@
32
34
  overflow: hidden;
33
35
  }
34
36
 
35
- @media only screen and (min-width: 596px) {
37
+ @media only screen and (min-width: $am-breakpoint) {
36
38
  .show-for-large {
37
39
  display: block !important;
38
40
  font-size: inherit;
@@ -1,11 +1,12 @@
1
1
  // ActiveMail email framework entry point. Critical styles are inlined by the
2
2
  // configured CSS inliner (Gmail mobile strips <style>); only media-query and
3
3
  // dark-mode enhancements stay in the <head>.
4
- @import "activemail/settings";
5
- @import "activemail/grid";
6
- @import "activemail/components";
7
- @import "activemail/utilities";
8
- @import "activemail/dark";
4
+ @forward "settings";
5
+ @use "settings" as *;
6
+ @use "grid";
7
+ @use "components";
8
+ @use "utilities";
9
+ @use "dark";
9
10
 
10
11
  body,
11
12
  table,
@@ -1,29 +1,28 @@
1
1
  # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'activemail/asset_pipeline'
5
+
4
6
  module ActiveMail
5
- # Embeds the compiled framework CSS as a <style> block so the Premailer adapter
6
- # (string-only) inlines it — it can't fetch the stylesheet_link_tag's asset URL.
7
+ # Embeds the compiled CSS as a <style> block: the inliners take an HTML string
8
+ # and cannot fetch the digest URL a stylesheet_link_tag would emit.
7
9
  module StylesHelper
8
- FRAMEWORK_STYLESHEET = 'activemail/activemail.css'
9
-
10
- # '' (not raise) when the asset can't be read — degrades to the link fallback,
11
- # but warns, since the email then ships unstyled.
12
- def activemail_inline_styles
13
- css = activemail_compiled_css
14
- if css.blank?
15
- ActiveMail.log_warning('[activemail] framework stylesheet could not be read from the asset pipeline; ' \
16
- 'email ships without inlined framework CSS')
17
- return ''.html_safe
18
- end
10
+ FRAMEWORK_STYLESHEET = 'activemail/activemail'
19
11
 
12
+ def activemail_inline_styles(*logical_paths)
13
+ paths = logical_paths.empty? ? [FRAMEWORK_STYLESHEET] : logical_paths
14
+ css = paths.map { |path| activemail_compiled_css(path.to_s) }.join("\n")
20
15
  content_tag(:style, css.html_safe, type: 'text/css')
21
16
  end
22
17
 
23
18
  private
24
19
 
25
- def activemail_compiled_css
26
- ActiveMail::CompiledStylesheet.read(FRAMEWORK_STYLESHEET)
20
+ def activemail_compiled_css(logical_path)
21
+ asset = logical_path.end_with?('.css') ? logical_path : "#{logical_path}.css"
22
+ css = ActiveMail::AssetPipeline.current.read(asset)
23
+ return css if css.present?
24
+
25
+ raise ActiveMail::StylesheetNotFound, "stylesheet #{asset.inspect} is missing or empty in the asset pipeline"
27
26
  end
28
27
  end
29
28
  end
@@ -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,27 +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
- <head>
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
- <%# Embedded so the Premailer adapter (HTML-string only) can inline the framework CSS. %>
19
- <%= activemail_inline_styles %>
20
- <%# Fallback for when activemail_inline_styles cannot read the compiled asset %>
21
- <%# (CDN-only host, compile=false without precompile); redundant otherwise, as the %>
22
- <%# embed's rules are idempotent once inlined. %>
23
- <%= stylesheet_link_tag "activemail/activemail", media: "all" %>
24
- </head>
3
+ <%= render "layouts/activemail/document_head" %>
25
4
 
26
5
  <body style="margin:0;padding:0;word-spacing:normal;">
27
6
  <div role="article" aria-roledescription="email" lang="<%= I18n.locale %>" style="width:100%;">