activemail 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/app/assets/stylesheets/activemail/_components.scss +22 -15
- data/app/assets/stylesheets/activemail/_dark.scss +4 -1
- data/app/assets/stylesheets/activemail/_settings.scss +14 -6
- data/lib/activemail/components/base.rb +15 -0
- data/lib/activemail/components/button.rb +8 -7
- data/lib/activemail/components/cta.rb +26 -13
- data/lib/activemail/components/info_box.rb +8 -3
- data/lib/activemail/tokens/button_style.rb +29 -0
- data/lib/activemail/tokens/scss_serializer.rb +22 -0
- data/lib/activemail/tokens.rb +64 -44
- data/lib/activemail/version.rb +1 -1
- data/lib/generators/activemail/templates/initializer.rb +18 -1
- data/lib/tasks/activemail.rake +1 -1
- metadata +4 -2
- /data/app/helpers/{activemail → active_mail}/styles_helper.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c521ec014c788f20a2c5c093d6905f36a18388e3c8bbe69fcb664ced5c01ce2
|
|
4
|
+
data.tar.gz: 5d20096c0870cbaee3e0f3bd945d5c9b4121c732370f310fc75ba0fd53cf75f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5362b815729d1c3ba16c1821e6cbe5e8d2e82554434ea32308d55fa58d601583b5c861246cc47c7745e88d2e1e53edd60360e5ef4ad90807a818335b2e5934f7
|
|
7
|
+
data.tar.gz: 5bc1094caf3250acd3d3bfad1d73c3c4e6f624720b6f6b494d7a459861196c985d747db25c07d0fec9eabf616e39eba2594ad71b056e02c6a60b45180a2b5da1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ 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
|
+
## [1.1.0] - 2026-06-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `radius` token group (`button`/`box`) and `Tokens#radius`/`#radius!` accessors.
|
|
13
|
+
- `Tokens#button_style(variant)` resolver and the `Tokens::ButtonStyle` value
|
|
14
|
+
object: button variants (incl. an **outline** secondary) are now token-driven —
|
|
15
|
+
set `<variant>_text`/`<variant>_border` colors instead of forking the component.
|
|
16
|
+
- Box-scoped `info_box_background`/`info_box_border`/`info_box_text` tokens
|
|
17
|
+
(falling back to the page palette) plus a box `border-radius`.
|
|
18
|
+
- `Tokens#load(group: {...})` bulk-loader and a single `Tokens#to_h` snapshot.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `<button>` and `<cta>` share a `bulletproof_button_table` scaffold; `<cta>`
|
|
23
|
+
drops its hardcoded `4px` radius in favor of the `radius` token.
|
|
24
|
+
- The corner radius is now applied by default on `.button`/`.cta` (the opt-in
|
|
25
|
+
`.radius` class is gone); `.button.secondary`/`.cta.secondary` mirror the
|
|
26
|
+
secondary color/border tokens.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- `Tokens#colors`/`#fonts`/`#spacings` readers — use `Tokens#to_h` instead.
|
|
31
|
+
|
|
32
|
+
## [1.0.2] - 2026-06-15
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Moved the framework styles helper to `app/helpers/active_mail/` so the host
|
|
37
|
+
application's Zeitwerk loader resolves it to `ActiveMail::StylesHelper`. Under
|
|
38
|
+
the previous `app/helpers/activemail/` path the default inflector expected
|
|
39
|
+
`Activemail::StylesHelper`, making the engine's `helper ActiveMail::StylesHelper`
|
|
40
|
+
raise `NameError` at boot in any mounting app.
|
|
41
|
+
|
|
8
42
|
## [1.0.1] - 2026-06-15
|
|
9
43
|
|
|
10
44
|
### Changed
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// Background on both
|
|
2
|
-
//
|
|
3
|
-
.button table td
|
|
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 {
|
|
4
5
|
background: $am-button-primary-bg;
|
|
6
|
+
border-radius: $am-button-radius;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
.button a {
|
|
8
10
|
color: $am-button-color;
|
|
9
|
-
background: $am-button-primary-bg;
|
|
10
11
|
font-family: $am-font-family;
|
|
11
12
|
font-weight: bold;
|
|
12
13
|
}
|
|
@@ -14,22 +15,22 @@
|
|
|
14
15
|
.button.secondary table td,
|
|
15
16
|
.button.secondary a {
|
|
16
17
|
background: $am-button-secondary-bg;
|
|
18
|
+
color: $am-button-secondary-color;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
//
|
|
20
|
-
.button.
|
|
21
|
-
|
|
22
|
-
border-radius: $am-button-radius;
|
|
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;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
.cta
|
|
26
|
+
.cta table td,
|
|
27
|
+
.cta a {
|
|
27
28
|
background: $am-button-primary-bg;
|
|
29
|
+
border-radius: $am-button-radius;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
.cta a {
|
|
31
33
|
color: $am-button-color;
|
|
32
|
-
background: $am-button-primary-bg;
|
|
33
34
|
font-family: $am-font-family;
|
|
34
35
|
font-weight: bold;
|
|
35
36
|
}
|
|
@@ -37,12 +38,18 @@
|
|
|
37
38
|
.cta.secondary table td,
|
|
38
39
|
.cta.secondary a {
|
|
39
40
|
background: $am-button-secondary-bg;
|
|
41
|
+
color: $am-button-secondary-color;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.cta.secondary table td {
|
|
45
|
+
border: 1px solid $am-button-secondary-border;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
.info-box td {
|
|
43
|
-
background-color: $am-
|
|
44
|
-
border-left: 5px solid $am-border;
|
|
45
|
-
|
|
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;
|
|
46
53
|
padding: $am-spacing-md;
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -52,7 +59,7 @@
|
|
|
52
59
|
padding: $am-spacing-md;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
//
|
|
62
|
+
// Height is set inline per instance; this rule is just a styling anchor.
|
|
56
63
|
.spacer {
|
|
57
64
|
width: 100%;
|
|
58
65
|
}
|
|
@@ -45,12 +45,14 @@ $am-dark-link: $am-color-primary !default;
|
|
|
45
45
|
color: $am-color-button-text !important;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
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.
|
|
49
50
|
.button.secondary table td,
|
|
50
51
|
.button.secondary a,
|
|
51
52
|
.cta.secondary table td,
|
|
52
53
|
.cta.secondary a {
|
|
53
54
|
background: $am-color-secondary !important;
|
|
55
|
+
color: $am-button-secondary-color !important;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
// Panels keep their inline background unless overridden here, which would
|
|
@@ -111,6 +113,7 @@ $am-dark-link: $am-color-primary !default;
|
|
|
111
113
|
[data-ogsc] .cta.secondary table td,
|
|
112
114
|
[data-ogsc] .cta.secondary a {
|
|
113
115
|
background: $am-color-secondary !important;
|
|
116
|
+
color: $am-button-secondary-color !important;
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
[data-ogsc] .callout-inner,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// app can pre-declare any value before importing the framework.
|
|
1
|
+
// All !default so a host app can pre-declare any value before importing the framework.
|
|
3
2
|
@import "activemail/activemail_tokens";
|
|
4
3
|
|
|
5
4
|
// Typography
|
|
@@ -16,10 +15,19 @@ $am-border: $am-color-border !default;
|
|
|
16
15
|
$am-button-primary-bg: $am-color-primary !default;
|
|
17
16
|
$am-button-secondary-bg: $am-color-secondary !default;
|
|
18
17
|
$am-button-color: $am-color-button-text !default;
|
|
19
|
-
|
|
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;
|
|
21
|
+
$am-button-radius: $am-radius-button !default;
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// 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-box-radius: $am-radius-box !default;
|
|
28
|
+
|
|
29
|
+
// Grid — the gem emits no gutters; we provide them via column padding. Width
|
|
30
|
+
// follows config.container_width via the token bridge so the SCSS grid stays
|
|
31
|
+
// aligned with the transpiled markup (ghost tables, column max-width).
|
|
24
32
|
$am-container-width: $am-grid-container-width !default;
|
|
25
33
|
$am-gutter: $am-spacing-sm !default;
|
|
@@ -36,6 +36,8 @@ module ActiveMail
|
|
|
36
36
|
# Layout tables: presentation role (a11y) and zeroed legacy spacing.
|
|
37
37
|
TABLE_RESET = 'role="presentation" border="0" cellpadding="0" cellspacing="0"'
|
|
38
38
|
|
|
39
|
+
BUTTON_PADDING = 'padding:12px 24px;'
|
|
40
|
+
|
|
39
41
|
sig { params(core: ::ActiveMail::Core).void }
|
|
40
42
|
def initialize(core)
|
|
41
43
|
@core = core
|
|
@@ -105,6 +107,19 @@ module ActiveMail
|
|
|
105
107
|
node.attributes['target'] ? %( target="#{escape_attr(node.attributes['target'])}") : ''
|
|
106
108
|
end
|
|
107
109
|
|
|
110
|
+
# Outlook-safe nested-table structure kept in one place for <button> and <cta>.
|
|
111
|
+
sig do
|
|
112
|
+
params(outer_classes: String, inner: String, cell_style: String, outer_extra: String).returns(String)
|
|
113
|
+
end
|
|
114
|
+
def bulletproof_button_table(outer_classes:, inner:, cell_style: '', outer_extra: '')
|
|
115
|
+
cell = cell_style.empty? ? '<td>' : %(<td style="#{cell_style}">)
|
|
116
|
+
[
|
|
117
|
+
%(<table class="#{outer_classes}" #{TABLE_RESET}><tbody><tr><td>),
|
|
118
|
+
%(<table #{TABLE_RESET}><tbody><tr>#{cell}#{inner}</td></tr></tbody></table>),
|
|
119
|
+
%(</td>#{outer_extra}</tr></tbody></table>)
|
|
120
|
+
].join
|
|
121
|
+
end
|
|
122
|
+
|
|
108
123
|
sig { returns(Integer) }
|
|
109
124
|
def column_count
|
|
110
125
|
core.column_count
|
|
@@ -14,13 +14,14 @@ module ActiveMail
|
|
|
14
14
|
inner = anchor(node, inner, expand) if node.attr('href')
|
|
15
15
|
inner = "<center>#{inner}</center>" if expand
|
|
16
16
|
|
|
17
|
-
classes = combine_classes(node, 'button')
|
|
18
17
|
expander = expand ? '<td class="expander"></td>' : ''
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
# CSS-driven by design: colors come from .button rules, not inline tokens —
|
|
19
|
+
# a distinct robustness model from <cta>, which inlines its palette.
|
|
20
|
+
bulletproof_button_table(
|
|
21
|
+
outer_classes: combine_classes(node, 'button'),
|
|
22
|
+
inner: inner,
|
|
23
|
+
outer_extra: expander
|
|
24
|
+
)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
private
|
|
@@ -30,7 +31,7 @@ module ActiveMail
|
|
|
30
31
|
target = target_attribute(node)
|
|
31
32
|
extra = expand ? ' align="center" class="float-center"' : ''
|
|
32
33
|
# Padding on the <a> makes the whole button a clickable target.
|
|
33
|
-
link_style =
|
|
34
|
+
link_style = "display:inline-block;text-decoration:none;#{BUTTON_PADDING}"
|
|
34
35
|
attrs = %(#{pass_through_attributes(node)}href="#{escape_attr(node.attr('href'))}"#{target}#{extra})
|
|
35
36
|
%(<a #{attrs}#{style_attribute(node, link_style)}>#{inner}</a>)
|
|
36
37
|
end
|
|
@@ -6,30 +6,43 @@ require_relative 'base'
|
|
|
6
6
|
module ActiveMail
|
|
7
7
|
module Components
|
|
8
8
|
# Colors read from tokens at transform time (runtime config), not load-time constants.
|
|
9
|
+
# Styles are inlined so the button survives clients that strip <style> (Gmail mobile…).
|
|
9
10
|
class Cta < Base
|
|
10
11
|
extend T::Sig
|
|
11
12
|
|
|
12
|
-
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
13
|
+
sig { override.overridable.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
13
14
|
def transform(node, inner)
|
|
14
15
|
# A CTA without a link is an authoring bug — surface it at render time.
|
|
15
16
|
raise ArgumentError, '<cta> requires an href attribute' if node.attr('href').to_s.strip.empty?
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
style = ActiveMail.tokens.button_style(class?(node, 'secondary') ? :secondary : :primary)
|
|
19
|
+
anchor = %(<a href="#{escape_attr(node.attr('href'))}"#{target_attribute(node)} ) +
|
|
20
|
+
%(style="#{link_style(style)}">#{inner}</a>)
|
|
21
|
+
bulletproof_button_table(
|
|
22
|
+
outer_classes: combine_classes(node, 'cta'),
|
|
23
|
+
inner: anchor,
|
|
24
|
+
cell_style: cell_style(style)
|
|
25
|
+
)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
private
|
|
28
29
|
|
|
29
|
-
sig { params(
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
sig { params(style: ActiveMail::Tokens::ButtonStyle).returns(String) }
|
|
31
|
+
def cell_style(style)
|
|
32
|
+
"background:#{style.background};border-radius:#{style.radius};#{border_css(style)}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# No border here: it lives on the cell only — a repeated <a> border doubles the line.
|
|
36
|
+
sig { params(style: ActiveMail::Tokens::ButtonStyle).returns(String) }
|
|
37
|
+
def link_style(style)
|
|
38
|
+
"display:inline-block;text-decoration:none;#{BUTTON_PADDING}" \
|
|
39
|
+
"background:#{style.background};color:#{style.color};font-weight:bold;" \
|
|
40
|
+
"border-radius:#{style.radius};"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(style: ActiveMail::Tokens::ButtonStyle).returns(String) }
|
|
44
|
+
def border_css(style)
|
|
45
|
+
style.border ? "border:1px solid #{style.border};" : ''
|
|
33
46
|
end
|
|
34
47
|
end
|
|
35
48
|
end
|
|
@@ -9,7 +9,7 @@ module ActiveMail
|
|
|
9
9
|
class InfoBox < Base
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
-
sig { override.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
12
|
+
sig { override.overridable.params(node: Nokogiri::XML::Node, inner: String).returns(String) }
|
|
13
13
|
def transform(node, inner)
|
|
14
14
|
classes = combine_classes(node, 'info-box')
|
|
15
15
|
[
|
|
@@ -21,11 +21,16 @@ module ActiveMail
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
# Box-scoped tokens (fall back to page-level ones) so a host can give the box
|
|
25
|
+
# a distinct surface without colliding with :background/:border/:text.
|
|
24
26
|
sig { returns(String) }
|
|
25
27
|
def cell_style
|
|
26
28
|
tokens = ActiveMail.tokens
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
background = tokens.color(:info_box_background) || tokens.color!(:background)
|
|
30
|
+
border = tokens.color(:info_box_border) || tokens.color!(:border)
|
|
31
|
+
text = tokens.color(:info_box_text) || tokens.color!(:text)
|
|
32
|
+
"background-color:#{background};border-left:5px solid #{border};border-radius:#{tokens.radius!(:box)};" \
|
|
33
|
+
"color:#{text};padding:#{tokens.spacing!(:md)};"
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
36
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module ActiveMail
|
|
7
|
+
class Tokens
|
|
8
|
+
# `border` nil means "no outline".
|
|
9
|
+
class ButtonStyle < T::Struct
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
const :background, String
|
|
13
|
+
const :color, String
|
|
14
|
+
const :radius, String
|
|
15
|
+
const :border, T.nilable(String)
|
|
16
|
+
|
|
17
|
+
# color falls back to :button_text; border is opt-in (a "<variant>_border" token).
|
|
18
|
+
sig { params(tokens: ActiveMail::Tokens, variant: T.any(String, Symbol)).returns(ButtonStyle) }
|
|
19
|
+
def self.from(tokens, variant)
|
|
20
|
+
new(
|
|
21
|
+
background: tokens.color!(variant),
|
|
22
|
+
color: tokens.color("#{variant}_text") || tokens.color!(:button_text),
|
|
23
|
+
radius: tokens.radius!(:button),
|
|
24
|
+
border: tokens.color("#{variant}_border")
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module ActiveMail
|
|
7
|
+
class Tokens
|
|
8
|
+
# !default lets a host pre-declare overrides upstream. Values are emitted
|
|
9
|
+
# verbatim (trusted, app-controlled input) — not escaped.
|
|
10
|
+
module ScssSerializer
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(stores: T::Hash[Symbol, TokenMap]).returns(String) }
|
|
14
|
+
def self.call(stores)
|
|
15
|
+
lines = stores.flat_map do |group, store|
|
|
16
|
+
store.map { |name, value| "$am-#{group}-#{name.to_s.tr('_', '-')}: #{value} !default;" }
|
|
17
|
+
end
|
|
18
|
+
"#{lines.join("\n")}\n"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/activemail/tokens.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'sorbet-runtime'
|
|
5
|
+
require_relative 'tokens/button_style'
|
|
6
|
+
require_relative 'tokens/scss_serializer'
|
|
5
7
|
|
|
6
8
|
module ActiveMail
|
|
7
|
-
# Design-tokens registry: the single Ruby source of truth, bridged to SCSS by #to_scss.
|
|
8
9
|
class Tokens
|
|
9
10
|
extend T::Sig
|
|
10
11
|
|
|
@@ -44,96 +45,115 @@ module ActiveMail
|
|
|
44
45
|
TokenMap
|
|
45
46
|
)
|
|
46
47
|
|
|
48
|
+
DEFAULT_RADII = T.let(
|
|
49
|
+
{
|
|
50
|
+
button: '4px',
|
|
51
|
+
box: '4px'
|
|
52
|
+
}.freeze,
|
|
53
|
+
TokenMap
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Open/Closed: adding a group here is the only change needed to wire it everywhere.
|
|
57
|
+
GROUPS = T.let(
|
|
58
|
+
{
|
|
59
|
+
color: DEFAULT_COLORS,
|
|
60
|
+
font: DEFAULT_FONTS,
|
|
61
|
+
spacing: DEFAULT_SPACINGS,
|
|
62
|
+
radius: DEFAULT_RADII
|
|
63
|
+
}.freeze,
|
|
64
|
+
T::Hash[Symbol, TokenMap]
|
|
65
|
+
)
|
|
66
|
+
|
|
47
67
|
sig { void }
|
|
48
68
|
def initialize
|
|
49
|
-
@
|
|
50
|
-
@fonts = T.let(DEFAULT_FONTS.dup, TokenMap)
|
|
51
|
-
@spacings = T.let(DEFAULT_SPACINGS.dup, TokenMap)
|
|
69
|
+
@stores = T.let(GROUPS.transform_values(&:dup), T::Hash[Symbol, TokenMap])
|
|
52
70
|
end
|
|
53
71
|
|
|
54
|
-
# value given → set, omitted → get. Open registry: any key is accepted (define
|
|
55
|
-
# custom tokens for custom components); color!/font!/spacing! fail loud on read.
|
|
56
72
|
sig { params(name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
|
|
57
73
|
def color(name, value = nil)
|
|
58
|
-
access(
|
|
74
|
+
access(:color, name, value)
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
sig { params(name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
|
|
62
78
|
def font(name, value = nil)
|
|
63
|
-
access(
|
|
79
|
+
access(:font, name, value)
|
|
64
80
|
end
|
|
65
81
|
|
|
66
82
|
sig { params(name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
|
|
67
83
|
def spacing(name, value = nil)
|
|
68
|
-
access(
|
|
84
|
+
access(:spacing, name, value)
|
|
69
85
|
end
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
sig { params(name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
|
|
88
|
+
def radius(name, value = nil)
|
|
89
|
+
access(:radius, name, value)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Raise rather than interpolating nil into the CSS.
|
|
73
93
|
sig { params(name: T.any(String, Symbol)).returns(String) }
|
|
74
94
|
def color!(name)
|
|
75
|
-
fetch!(
|
|
95
|
+
fetch!(:color, name)
|
|
76
96
|
end
|
|
77
97
|
|
|
78
98
|
sig { params(name: T.any(String, Symbol)).returns(String) }
|
|
79
99
|
def font!(name)
|
|
80
|
-
fetch!(
|
|
100
|
+
fetch!(:font, name)
|
|
81
101
|
end
|
|
82
102
|
|
|
83
103
|
sig { params(name: T.any(String, Symbol)).returns(String) }
|
|
84
104
|
def spacing!(name)
|
|
85
|
-
fetch!(
|
|
105
|
+
fetch!(:spacing, name)
|
|
86
106
|
end
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@colors.dup.freeze
|
|
108
|
+
sig { params(name: T.any(String, Symbol)).returns(String) }
|
|
109
|
+
def radius!(name)
|
|
110
|
+
fetch!(:radius, name)
|
|
92
111
|
end
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
# Frozen snapshot: mutating the result can't bypass the DSL setters.
|
|
114
|
+
sig { returns(T::Hash[Symbol, TokenMap]) }
|
|
115
|
+
def to_h
|
|
116
|
+
@stores.transform_values { |store| store.dup.freeze }.freeze
|
|
97
117
|
end
|
|
98
118
|
|
|
99
|
-
sig {
|
|
100
|
-
def
|
|
101
|
-
|
|
119
|
+
sig { params(groups: TokenMap).void }
|
|
120
|
+
def load(**groups)
|
|
121
|
+
groups.each do |group, values|
|
|
122
|
+
values.each { |name, value| access(group, name, value) }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig { params(variant: T.any(String, Symbol)).returns(ButtonStyle) }
|
|
127
|
+
def button_style(variant)
|
|
128
|
+
ButtonStyle.from(self, variant)
|
|
102
129
|
end
|
|
103
130
|
|
|
104
|
-
# SCSS bridge: !default lets power-users pre-declare overrides upstream.
|
|
105
|
-
# Values are emitted verbatim (trusted, app-controlled input) — not escaped.
|
|
106
131
|
sig { returns(String) }
|
|
107
132
|
def to_scss
|
|
108
|
-
|
|
109
|
-
"#{lines.join("\n")}\n"
|
|
133
|
+
ScssSerializer.call(@stores)
|
|
110
134
|
end
|
|
111
135
|
|
|
112
136
|
private
|
|
113
137
|
|
|
114
|
-
sig { params(
|
|
115
|
-
def
|
|
116
|
-
|
|
138
|
+
sig { params(group: Symbol).returns(TokenMap) }
|
|
139
|
+
def store_for(group)
|
|
140
|
+
@stores.fetch(group) { raise KeyError, "unknown token group #{group.inspect}" }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
sig { params(group: Symbol, name: T.any(String, Symbol)).returns(String) }
|
|
144
|
+
def fetch!(group, name)
|
|
145
|
+
store_for(group).fetch(name.to_sym) { raise KeyError, "unknown #{group} token #{name.inspect}" }
|
|
117
146
|
end
|
|
118
147
|
|
|
119
|
-
sig { params(
|
|
120
|
-
def access(
|
|
148
|
+
sig { params(group: Symbol, name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
|
|
149
|
+
def access(group, name, value)
|
|
150
|
+
store = store_for(group)
|
|
121
151
|
key = name.to_sym
|
|
122
152
|
return store[key] if value.nil?
|
|
123
|
-
# Emitted verbatim into SCSS
|
|
153
|
+
# Emitted verbatim into SCSS — reject blanks that would yield broken CSS.
|
|
124
154
|
raise ArgumentError, "token #{key} value must not be blank" if value.strip.empty?
|
|
125
155
|
|
|
126
156
|
store[key] = value
|
|
127
157
|
end
|
|
128
|
-
|
|
129
|
-
sig { params(group: String, store: TokenMap).returns(T::Array[String]) }
|
|
130
|
-
def scss_lines(group, store)
|
|
131
|
-
store.map { |name, value| "$am-#{group}-#{scss_name(name)}: #{value} !default;" }
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
sig { params(name: Symbol).returns(String) }
|
|
135
|
-
def scss_name(name)
|
|
136
|
-
name.to_s.tr('_', '-')
|
|
137
|
-
end
|
|
138
158
|
end
|
|
139
159
|
end
|
data/lib/activemail/version.rb
CHANGED
|
@@ -20,11 +20,28 @@ ActiveMail.configure do |config|
|
|
|
20
20
|
# How malformed markup is handled: :warn (default), :ignore, or :raise.
|
|
21
21
|
# config.on_parse_error = :warn
|
|
22
22
|
|
|
23
|
-
# Design tokens
|
|
23
|
+
# Design tokens. Configure literals here (the bridge only flows Ruby → SCSS).
|
|
24
24
|
# config.tokens.color :primary, "#2a9d8f"
|
|
25
25
|
# config.tokens.color :secondary, "#264653"
|
|
26
26
|
# config.tokens.font :heading, "Georgia, serif"
|
|
27
27
|
# config.tokens.spacing :lg, "32px"
|
|
28
|
+
# config.tokens.radius :button, "6px" # <cta>/.button corner radius
|
|
29
|
+
# config.tokens.radius :box, "8px" # <info-box> corner radius
|
|
30
|
+
#
|
|
31
|
+
# Outline secondary button (filled by default): give the secondary variant its
|
|
32
|
+
# own text + border instead of just a fill.
|
|
33
|
+
# config.tokens.color :secondary, "#ffffff"
|
|
34
|
+
# config.tokens.color :secondary_text, "#0f4447"
|
|
35
|
+
# config.tokens.color :secondary_border, "rgba(15, 68, 71, 0.6)"
|
|
36
|
+
#
|
|
37
|
+
# Box-scoped colors (fall back to :background/:border/:text when unset).
|
|
38
|
+
# config.tokens.color :info_box_background, "#fff7ef"
|
|
39
|
+
#
|
|
40
|
+
# Or configure everything in one block:
|
|
41
|
+
# config.tokens.load(
|
|
42
|
+
# color: { primary: "#2a9d8f", secondary_text: "#0f4447" },
|
|
43
|
+
# radius: { button: "6px", box: "8px" }
|
|
44
|
+
# )
|
|
28
45
|
|
|
29
46
|
# Register components (built-ins like ActiveMail::Components::Cta, or your own
|
|
30
47
|
# Components::* from `rails g activemail:component`).
|
data/lib/tasks/activemail.rake
CHANGED
|
@@ -11,7 +11,7 @@ namespace :activemail do
|
|
|
11
11
|
path = args[:path] || 'app/assets/stylesheets/activemail/_activemail_tokens.scss'
|
|
12
12
|
FileUtils.mkdir_p(File.dirname(path))
|
|
13
13
|
File.write(path, ActiveMail.scss_variables)
|
|
14
|
-
puts "Wrote #{ActiveMail.tokens.
|
|
14
|
+
puts "Wrote #{ActiveMail.tokens.to_h.values.sum(&:size)} tokens to #{path}"
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activemail
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Advitam
|
|
@@ -70,7 +70,7 @@ files:
|
|
|
70
70
|
- app/assets/stylesheets/activemail/_settings.scss
|
|
71
71
|
- app/assets/stylesheets/activemail/_utilities.scss
|
|
72
72
|
- app/assets/stylesheets/activemail/activemail.scss
|
|
73
|
-
- app/helpers/
|
|
73
|
+
- app/helpers/active_mail/styles_helper.rb
|
|
74
74
|
- app/views/layouts/activemail/_footer.html.inky-erb
|
|
75
75
|
- app/views/layouts/activemail/_head.html.inky-erb
|
|
76
76
|
- app/views/layouts/activemail/mailer.html.inky-erb
|
|
@@ -110,6 +110,8 @@ files:
|
|
|
110
110
|
- lib/activemail/rails/engine.rb
|
|
111
111
|
- lib/activemail/rails/template_handler.rb
|
|
112
112
|
- lib/activemail/tokens.rb
|
|
113
|
+
- lib/activemail/tokens/button_style.rb
|
|
114
|
+
- lib/activemail/tokens/scss_serializer.rb
|
|
113
115
|
- lib/activemail/version.rb
|
|
114
116
|
- lib/generators/activemail/component_generator.rb
|
|
115
117
|
- lib/generators/activemail/install_generator.rb
|
|
File without changes
|