activemail 1.0.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d23d0cbbba6b06d9492800ec108e2156d8abf4714090bb21a7c2b4b0a3383084
4
- data.tar.gz: 224c60368f2c11d1f9aea2cbf8ad63dc4ad67a90f0f48225cfb4487f87e73931
3
+ metadata.gz: 0c521ec014c788f20a2c5c093d6905f36a18388e3c8bbe69fcb664ced5c01ce2
4
+ data.tar.gz: 5d20096c0870cbaee3e0f3bd945d5c9b4121c732370f310fc75ba0fd53cf75f9
5
5
  SHA512:
6
- metadata.gz: 3e01ccdbee061bd112b9d743bcf9899de50f4dccff6c758f69dd041d5070002f7172b89e3dfc333f6db5dedfbba20816c2b09f9c6d17fc5aad34a3a4c5821578
7
- data.tar.gz: 8d9528d101254d8959db9fe2b2c43563305f55eb633b671de1db0787c2f2e37eeb0652df8269cd8bb29a81ed99c16532eff918ec776a71d7a6d5e5148be3c620
6
+ metadata.gz: 5362b815729d1c3ba16c1821e6cbe5e8d2e82554434ea32308d55fa58d601583b5c861246cc47c7745e88d2e1e53edd60360e5ef4ad90807a818335b2e5934f7
7
+ data.tar.gz: 5bc1094caf3250acd3d3bfad1d73c3c4e6f624720b6f6b494d7a459861196c985d747db25c07d0fec9eabf616e39eba2594ad71b056e02c6a60b45180a2b5da1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ 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
+
8
32
  ## [1.0.2] - 2026-06-15
9
33
 
10
34
  ### Fixed
@@ -1,12 +1,13 @@
1
- // Background on both inner cell and link so the whole bulletproof button
2
- // (table.button > td > table > td > a) is filled and clickable.
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
- // Progressive enhancement only — some clients strip border-radius.
20
- .button.radius table td,
21
- .button.radius a {
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
- // CTA mirrors the bulletproof button; colors are also inlined by the component.
26
- .cta table td {
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-background;
44
- border-left: 5px solid $am-border;
45
- color: $am-text;
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
- // Spacer hook — height is set inline per instance; this is a styling anchor.
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
- // Keep the secondary variant's color — the primary rule above would erase it.
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
- // Semantic aliases mapped from the $am-* token vars. All !default so a host
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
- $am-button-radius: 4px !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;
21
+ $am-button-radius: $am-radius-button !default;
20
22
 
21
- // Grid — the gem emits no gutters; we provide them via column padding.
22
- // Width follows config.container_width via the token bridge, so the SCSS grid
23
- // stays aligned with the transpiled markup (ghost tables, column max-width).
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
- %(<table class="#{classes}" #{TABLE_RESET}><tbody><tr><td>),
21
- %(<table #{TABLE_RESET}><tbody><tr><td>#{inner}</td></tr></tbody></table>),
22
- %(</td>#{expander}</tr></tbody></table>)
23
- ].join
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 = 'display:inline-block;text-decoration:none;padding:12px 24px;'
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
- background = ActiveMail.tokens.color!(class?(node, 'secondary') ? :secondary : :primary)
18
- classes = combine_classes(node, 'cta')
19
- anchor = %(<a href="#{escape_attr(node.attr('href'))}"#{target_attribute(node)} style="#{link_style(background)}">#{inner}</a>)
20
- [
21
- %(<table class="#{classes}" #{TABLE_RESET}><tbody><tr><td>),
22
- %(<table #{TABLE_RESET}><tbody><tr><td style="background:#{background};border-radius:4px;">),
23
- "#{anchor}</td></tr></tbody></table></td></tr></tbody></table>"
24
- ].join
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(background: String).returns(String) }
30
- def link_style(background)
31
- 'display:inline-block;text-decoration:none;padding:12px 24px;' \
32
- "background:#{background};color:#{ActiveMail.tokens.color!(:button_text)};font-weight:bold;border-radius:4px;"
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
- "background-color:#{tokens.color!(:background)};border-left:5px solid #{tokens.color!(:border)};" \
28
- "color:#{tokens.color!(:text)};padding:#{tokens.spacing!(:md)};"
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
@@ -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
- @colors = T.let(DEFAULT_COLORS.dup, TokenMap)
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(@colors, name, value)
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(@fonts, name, value)
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(@spacings, name, value)
84
+ access(:spacing, name, value)
69
85
  end
70
86
 
71
- # Strict reads for code that must have the token (e.g. a component's inline
72
- # color): raise rather than interpolating nil into the CSS.
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!(@colors, :color, name)
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!(@fonts, :font, name)
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!(@spacings, :spacing, name)
105
+ fetch!(:spacing, name)
86
106
  end
87
107
 
88
- # Frozen dup: mutating the returned hash must not bypass the DSL setters.
89
- sig { returns(TokenMap) }
90
- def colors
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
- sig { returns(TokenMap) }
95
- def fonts
96
- @fonts.dup.freeze
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 { returns(TokenMap) }
100
- def spacings
101
- @spacings.dup.freeze
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
- lines = scss_lines('color', @colors) + scss_lines('font', @fonts) + scss_lines('spacing', @spacings)
109
- "#{lines.join("\n")}\n"
133
+ ScssSerializer.call(@stores)
110
134
  end
111
135
 
112
136
  private
113
137
 
114
- sig { params(store: TokenMap, kind: Symbol, name: T.any(String, Symbol)).returns(String) }
115
- def fetch!(store, kind, name)
116
- store.fetch(name.to_sym) { raise KeyError, "unknown #{kind} token #{name.inspect}" }
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(store: TokenMap, name: T.any(String, Symbol), value: T.nilable(String)).returns(T.nilable(String)) }
120
- def access(store, name, value)
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 by #to_scss — reject blanks that would yield broken CSS.
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module ActiveMail
5
- VERSION = '1.0.2'
5
+ VERSION = '1.1.0'
6
6
  end
@@ -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 the single source of truth, bridged to SCSS via $am-*.
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`).
@@ -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.colors.size + ActiveMail.tokens.fonts.size + ActiveMail.tokens.spacings.size} tokens to #{path}"
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.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Advitam
@@ -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