warped 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +25 -0
  3. data/Gemfile +0 -2
  4. data/Gemfile.lock +25 -19
  5. data/README.md +116 -270
  6. data/app/assets/config/warped_manifest.js +2 -0
  7. data/app/assets/javascript/warped/controllers/filter_controller.js +76 -0
  8. data/app/assets/javascript/warped/controllers/filters_controller.js +21 -0
  9. data/app/assets/javascript/warped/index.js +2 -0
  10. data/app/assets/stylesheets/warped/application.css +15 -0
  11. data/app/assets/stylesheets/warped/base.css +23 -0
  12. data/app/assets/stylesheets/warped/filters.css +115 -0
  13. data/app/assets/stylesheets/warped/pagination.css +74 -0
  14. data/app/assets/stylesheets/warped/search.css +33 -0
  15. data/app/assets/stylesheets/warped/table.css +114 -0
  16. data/app/views/warped/_actions.html.erb +9 -0
  17. data/app/views/warped/_cell.html.erb +3 -0
  18. data/app/views/warped/_column.html.erb +35 -0
  19. data/app/views/warped/_filters.html.erb +21 -0
  20. data/app/views/warped/_hidden_fields.html.erb +19 -0
  21. data/app/views/warped/_pagination.html.erb +34 -0
  22. data/app/views/warped/_row.html.erb +19 -0
  23. data/app/views/warped/_search.html.erb +21 -0
  24. data/app/views/warped/_table.html.erb +52 -0
  25. data/app/views/warped/filters/_filter.html.erb +40 -0
  26. data/config/importmap.rb +3 -0
  27. data/docs/controllers/FILTERABLE.md +193 -0
  28. data/docs/controllers/PAGEABLE.md +70 -0
  29. data/docs/controllers/README.md +8 -0
  30. data/docs/controllers/SEARCHABLE.md +95 -0
  31. data/docs/controllers/SORTABLE.md +94 -0
  32. data/docs/controllers/TABULATABLE.md +28 -0
  33. data/docs/controllers/views/PARTIALS.md +285 -0
  34. data/docs/jobs/README.md +22 -0
  35. data/docs/services/README.md +81 -0
  36. data/lib/generators/warped/install_generator.rb +1 -1
  37. data/lib/warped/api/filter/base/value.rb +52 -0
  38. data/lib/warped/api/filter/base.rb +84 -0
  39. data/lib/warped/api/filter/boolean.rb +41 -0
  40. data/lib/warped/api/filter/date.rb +26 -0
  41. data/lib/warped/api/filter/date_time.rb +32 -0
  42. data/lib/warped/api/filter/decimal.rb +31 -0
  43. data/lib/warped/api/filter/factory.rb +38 -0
  44. data/lib/warped/api/filter/integer.rb +38 -0
  45. data/lib/warped/api/filter/string.rb +25 -0
  46. data/lib/warped/api/filter/time.rb +25 -0
  47. data/lib/warped/api/filter.rb +14 -0
  48. data/lib/warped/api/sort/value.rb +40 -0
  49. data/lib/warped/api/sort.rb +65 -0
  50. data/lib/warped/controllers/filterable/ui.rb +46 -0
  51. data/lib/warped/controllers/filterable.rb +79 -42
  52. data/lib/warped/controllers/pageable/ui.rb +70 -0
  53. data/lib/warped/controllers/pageable.rb +11 -11
  54. data/lib/warped/controllers/searchable/ui.rb +37 -0
  55. data/lib/warped/controllers/searchable.rb +2 -0
  56. data/lib/warped/controllers/sortable/ui.rb +53 -0
  57. data/lib/warped/controllers/sortable.rb +53 -33
  58. data/lib/warped/controllers/tabulatable/ui.rb +54 -0
  59. data/lib/warped/controllers/tabulatable.rb +13 -27
  60. data/lib/warped/emails/components/align.rb +21 -0
  61. data/lib/warped/emails/components/base.rb +116 -0
  62. data/lib/warped/emails/components/button.rb +58 -0
  63. data/lib/warped/emails/components/divider.rb +15 -0
  64. data/lib/warped/emails/components/heading.rb +65 -0
  65. data/lib/warped/emails/components/layouts/columns.rb +36 -0
  66. data/lib/warped/emails/components/layouts/cta.rb +38 -0
  67. data/lib/warped/emails/components/layouts/main.rb +34 -0
  68. data/lib/warped/emails/components/link.rb +36 -0
  69. data/lib/warped/emails/components/spacer.rb +15 -0
  70. data/lib/warped/emails/components/stepper.rb +104 -0
  71. data/lib/warped/emails/components/table.rb +37 -0
  72. data/lib/warped/emails/components/text.rb +67 -0
  73. data/lib/warped/emails/helpers.rb +26 -0
  74. data/lib/warped/emails/slottable.rb +61 -0
  75. data/lib/warped/emails/styleable.rb +160 -0
  76. data/lib/warped/engine.rb +19 -0
  77. data/lib/warped/queries/filter.rb +32 -12
  78. data/lib/warped/table/action.rb +33 -0
  79. data/lib/warped/table/column.rb +34 -0
  80. data/lib/warped/version.rb +1 -1
  81. data/lib/warped.rb +2 -0
  82. data/warped.gemspec +1 -1
  83. metadata +73 -7
  84. data/lib/warped/emails/.keep +0 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "active_support/core_ext/object/blank"
5
+
6
+ module Warped
7
+ module Controllers
8
+ module Sortable
9
+ module Ui
10
+ extend ActiveSupport::Concern
11
+
12
+ include Sortable
13
+
14
+ included do
15
+ helper_method :attribute_name, :sorted?, :sorted_field?, :sortable_field?, :sort_url_params
16
+ end
17
+
18
+ # @see Sortable#sort
19
+ def sort(...)
20
+ @sorted = true
21
+
22
+ super
23
+ end
24
+
25
+ # @param parameter_name [String]
26
+ # @return [Boolean]
27
+ def sorted_field?(parameter_name)
28
+ current_action_sort_value.parameter_name == parameter_name.to_s
29
+ end
30
+
31
+ # @param parameter_name [String]
32
+ # @return [Boolean] Whether the parameter_name is sortable.
33
+ def sortable_field?(parameter_name)
34
+ current_action_sorts.any? { |sort| sort.parameter_name == parameter_name.to_s }
35
+ end
36
+
37
+ # @return [Boolean] Whether the current action is sorted.
38
+ def sorted?
39
+ @sorted ||= false
40
+ end
41
+
42
+ # @return [Hash] The sort_url_params
43
+ def sort_url_params(**options)
44
+ url_params = {
45
+ sort_key: current_action_sort_value.parameter_name,
46
+ sort_direction: current_action_sort_value.direction
47
+ }
48
+ url_params.merge!(options)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support/concern"
4
4
  require "active_support/core_ext/class/attribute"
5
+ require "active_support/core_ext/object/blank"
5
6
 
6
7
  module Warped
7
8
  module Controllers
@@ -35,7 +36,7 @@ module Warped
35
36
  # class UsersController < ApplicationController
36
37
  # include Sortable
37
38
  #
38
- # sortable_by :name, :created_at, 'accounts.referrals_count' => 'referrals'
39
+ # sortable_by :name, :created_at, 'accounts.referrals_count' => { alias_name: 'referrals' }
39
40
  #
40
41
  # def index
41
42
  # scope = sort(User.joins(:account))
@@ -55,60 +56,79 @@ module Warped
55
56
  extend ActiveSupport::Concern
56
57
 
57
58
  included do
58
- class_attribute :sort_fields, default: []
59
- class_attribute :mapped_sort_fields, default: {}
60
- class_attribute :default_sort_key, default: :id
61
- class_attribute :default_sort_direction, default: :desc
59
+ class_attribute :sorts, default: []
60
+ class_attribute :default_sort, default: Sort.new("id")
61
+ class_attribute :default_sort_direction, default: "desc"
62
+
63
+ attr_reader :current_action_sorts
64
+
65
+ helper_method :current_action_sorts, :current_action_sort_value, :default_sort, :default_sort_direction
66
+
67
+ rescue_from Sort::DirectionError, with: :render_invalid_sort_direction
62
68
  end
63
69
 
64
70
  class_methods do
65
71
  # @param keys [Array<Symbol,String>]
66
72
  # @param mapped_keys [Hash<Symbol,String>]
67
73
  def sortable_by(*keys, **mapped_keys)
68
- self.sort_fields = keys.map(&:to_s)
69
- self.mapped_sort_fields = mapped_keys.with_indifferent_access
74
+ self.sorts = keys.map do |field|
75
+ Warped::Sort.new(field)
76
+ end
77
+
78
+ self.sorts += mapped_keys.map do |field, opts|
79
+ Warped::Sort.new(field, alias_name: opts[:alias_name])
80
+ end
81
+
82
+ return if self.sorts.any? { |sort| sort.name == default_sort.name }
83
+
84
+ self.sorts.push(default_sort)
70
85
  end
71
86
  end
72
87
 
73
88
  # @param scope [ActiveRecord::Relation] The scope to sort.
74
- # @param sort_key [String, Symbol] The sort key.
75
- # @param sort_direction [String, Symbol] The sort direction.
89
+ # @param sort_conditions [Array<Warped::Sort::Base>|nil] The sort conditions.
76
90
  # @return [ActiveRecord::Relation]
77
- def sort(scope, sort_key: self.sort_key, sort_direction: self.sort_direction)
78
- return scope unless sort_key && sort_direction
79
-
80
- validate_sort_key!
91
+ def sort(scope, sort_conditions: nil)
92
+ action_sorts = sort_conditions.presence || sorts
93
+ @current_action_sorts = action_sorts
81
94
 
82
- Queries::Sort.call(scope, sort_key:, sort_direction:)
95
+ Queries::Sort.call(scope, sort_key: current_action_sort_value.name,
96
+ sort_direction: current_action_sort_value.direction)
83
97
  end
84
98
 
85
- protected
99
+ # @return [Warped::Sort::Value] The current sort value.
100
+ def current_action_sort_value
101
+ @current_action_sort_value ||= begin
102
+ sort_obj = current_action_sorts.find do |sort|
103
+ params[:sort_key] == sort.parameter_name
104
+ end
86
105
 
87
- # @return [Symbol] The sort direction.
88
- def sort_direction
89
- @sort_direction ||= params[:sort_direction] || default_sort_direction
106
+ if sort_obj.present?
107
+ Sort::Value.new(sort_obj, params[:sort_direction] || default_sort_direction)
108
+ else
109
+ Sort::Value.new(default_sort, default_sort_direction)
110
+ end
111
+ end
90
112
  end
91
113
 
92
- def sort_key
93
- @sort_key ||= mapped_sort_fields.key(params[:sort_key]).presence ||
94
- params[:sort_key] ||
95
- default_sort_key
114
+ protected
115
+
116
+ # @param exception [Sort::DirectionError]
117
+ def render_invalid_sort_direction(exception)
118
+ render json: { error: exception.message }, status: :bad_request
96
119
  end
97
120
 
98
121
  private
99
122
 
100
- def validate_sort_key!
101
- return if valid_sort_key?
123
+ # @return [Warped::Sort] The current sort object.
124
+ def current_sort
125
+ @current_sort ||= begin
126
+ sort_obj = sorts.find do |sort|
127
+ params[:sort_key] == sort.parameter_name
128
+ end
102
129
 
103
- possible_values = sort_fields + mapped_sort_fields.values
104
- message = "Invalid sort key: #{sort_key}, must be one of #{possible_values}"
105
- raise ActionController::BadRequest, message
106
- end
107
-
108
- def valid_sort_key?
109
- sort_key == default_sort_key ||
110
- sort_fields.include?(sort_key) ||
111
- mapped_sort_fields[sort_key].present?
130
+ sort_obj.presence || Warped::Sort.new(default_sort_key)
131
+ end
112
132
  end
113
133
  end
114
134
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Warped
6
+ module Controllers
7
+ module Tabulatable
8
+ module Ui
9
+ extend ActiveSupport::Concern
10
+
11
+ include Tabulatable
12
+ include Filterable::Ui
13
+ include Pageable::Ui
14
+ include Searchable::Ui
15
+ include Sortable::Ui
16
+
17
+ included do
18
+ helper_method :tabulation, :tabulate_url_params, :tabulate_query
19
+ end
20
+
21
+ # @return [Hash]
22
+ def tabulation
23
+ {
24
+ filters:,
25
+ current_filters:,
26
+ sorts:,
27
+ current_sorts:,
28
+ search_term:,
29
+ search_param:,
30
+ pagination:
31
+ }
32
+ end
33
+
34
+ # @param options [Hash] Additional hash of options to include in the tabulation url_params
35
+ # @return [Hash] The tabulation url_params
36
+ def tabulate_url_params(**options)
37
+ base = paginate_url_params
38
+ base.merge!(search_url_params)
39
+ base.merge!(sort_url_params)
40
+ base.merge!(filter_url_params)
41
+ base.merge!(options)
42
+
43
+ base.tap(&:compact_blank!)
44
+ end
45
+
46
+ # @param options [Hash] Additional hash of options to include in the tabulation query
47
+ # @return [String] The tabulation query string
48
+ def tabulate_query(**options)
49
+ tabulate_url_params(**options).to_query
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -13,12 +13,12 @@ module Warped
13
13
  # class UsersController < ApplicationController
14
14
  # include Tabulatable
15
15
  #
16
- # tabulatable_by :name, :email, :age, 'posts.created_at', 'posts.id' => 'post_id'
16
+ # tabulatable_by :email, :age, 'posts.created_at', 'posts.id' => { alias_name: 'post_id', kind: :integer }
17
17
  #
18
18
  # def index
19
19
  # users = User.left_joins(:posts).group(:id)
20
20
  # users = tabulate(users)
21
- # render json: users, meta: tabulate_info
21
+ # render json: users, meta: tabulation
22
22
  # end
23
23
  # end
24
24
  #
@@ -31,35 +31,32 @@ module Warped
31
31
  # class PostsController < ApplicationController
32
32
  # include Tabulatable
33
33
  #
34
- # tabulatable_by :title, :content, :created_at, user: 'users.name'
35
- # filterable_by :created_at, user: 'users.name'
34
+ # tabulatable_by :title, :content, :created_at, user: { alias_name: 'users.name' }
35
+ # filterable_by :created_at, user: { alias_name: 'users.name' }
36
36
  #
37
37
  # def index
38
38
  # posts = Post.left_joins(:user).group(:id)
39
39
  # posts = tabulate(posts)
40
- # render json: posts, meta: tabulate_info
40
+ # render json: posts, meta: tabulation
41
41
  # end
42
42
  # end
43
43
  module Tabulatable
44
44
  extend ActiveSupport::Concern
45
45
 
46
- included do
47
- include Filterable
48
- include Sortable
49
- include Searchable
50
- include Pageable
46
+ include Filterable
47
+ include Sortable
48
+ include Searchable
49
+ include Pageable
51
50
 
51
+ included do
52
52
  class_attribute :tabulate_fields, default: []
53
- class_attribute :mapped_tabulate_fields, default: []
53
+ class_attribute :mapped_tabulate_fields, default: {}
54
54
  end
55
55
 
56
56
  class_methods do
57
57
  def tabulatable_by(*keys, **mapped_keys)
58
- self.tabulate_fields = keys
59
- self.mapped_tabulate_fields = mapped_keys.to_a
60
-
61
- filterable_by(*keys, **mapped_keys) if filter_fields.empty? && mapped_filter_fields.empty?
62
- sortable_by(*keys, **mapped_keys) if sort_fields.empty? && mapped_sort_fields.empty?
58
+ filterable_by(*keys, **mapped_keys) if filters.empty?
59
+ sortable_by(*keys, **mapped_keys) if sorts.empty?
63
60
  end
64
61
  end
65
62
 
@@ -75,17 +72,6 @@ module Warped
75
72
  scope = sort(scope)
76
73
  paginate(scope)
77
74
  end
78
-
79
- # @return [Hash]
80
- def tabulate_info
81
- {
82
- filters: filter_conditions(*filter_fields, *mapped_filter_fields),
83
- sorts: sort_conditions(*sort_fields, *mapped_sort_fields),
84
- search_term:,
85
- search_param:,
86
- page_info:
87
- }
88
- end
89
75
  end
90
76
  end
91
77
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Align < Base
6
+ variant do
7
+ base { ["text-align: #{@align}"] }
8
+ end
9
+
10
+ def initialize(align: :left)
11
+ super()
12
+ @align = align
13
+ raise ArgumentError, "Invalid alignment: #{align}" unless %i[left center right].include?(align)
14
+ end
15
+
16
+ def template
17
+ tag.div(content, style:)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/delegation"
4
+ require "action_view/helpers"
5
+
6
+ module Warped
7
+ module Emails
8
+ ##
9
+ # Base class for all email components
10
+ #
11
+ # This class provides a number of helper methods for building email components.
12
+ #
13
+ # == Usage
14
+ #
15
+ # To create a new component, you can subclass this class and implement the +template+ method.
16
+ #
17
+ # class MyComponent < Warped::Emails::Base
18
+ # def template
19
+ # content_tag(:div, "Hello, World!")
20
+ # end
21
+ # end
22
+ #
23
+ # You can then render the component using the +render+ method in your mailer views
24
+ #
25
+ # <%= render MyComponent.new %>
26
+ #
27
+ # == Variants
28
+ #
29
+ # Components can define variants using the +variant+ method. Variants are different styles for the component that
30
+ # can be used to change the appearance of the component.
31
+ #
32
+ # class MyComponent < Warped::Emails::Base
33
+ # variant do
34
+ # base { "color: red;" }
35
+ # size do
36
+ # sm { "font-size: 12px;" }
37
+ # md { "font-size: 16px;" }
38
+ # lg { "font-size: 20px;" }
39
+ # end
40
+ # end
41
+ #
42
+ # default_variant size: :md
43
+ #
44
+ # def initialize(size: nil)
45
+ # super()
46
+ # @size = size
47
+ # end
48
+ #
49
+ # def template
50
+ # content_tag(:div, style: style())
51
+ # end
52
+ # end
53
+ #
54
+ # You can then specify the variant to use using the +style+ method in your component
55
+ #
56
+ # <%= render MyComponent.new %> # => <div style="color: red; font-size: 16px;"></div>
57
+ # <%= render MyComponent.new(size: :lg) %> # => <div style="color: red; font-size: 20px;"></div>
58
+ #
59
+ # The variant blocks can return either a string or an array of strings. If an array is returned,
60
+ # the strings will be joined with a semi-colon.
61
+ #
62
+ # The +default_variant+ method can be used to specify the default variant to use if none is specified.
63
+ #
64
+ # The +style+ method can be used to apply the variant styles to the component, or to apply additional styles on
65
+ # top of the variant styles.
66
+ #
67
+ # == Slots
68
+ #
69
+ # Components can define slots using the +slot+ method. Slots are placeholders for content that can be passed
70
+ # in from the outside.
71
+ #
72
+ # class MyComponent < Warped::Emails::Base
73
+ # slot :title
74
+ #
75
+ # def template
76
+ # content_tag(:div, title)
77
+ # end
78
+ # end
79
+ #
80
+ # You can then pass content to the slot using the +with_<slot_name>+ method in your mailer views
81
+ # <%= render MyComponent.new do |my_component| %>
82
+ # <% my_component.with_title("Hello, World!") %>
83
+ # <% end %>
84
+ class Base
85
+ include ActionView::Helpers::CaptureHelper
86
+ include Slottable
87
+ include Styleable
88
+
89
+ attr_reader :view_context
90
+
91
+ delegate :capture, :tag, :content_tag, to: :view_context
92
+ delegate_missing_to :view_context
93
+
94
+ def template
95
+ raise NotImplementedError
96
+ end
97
+
98
+ def content
99
+ @content_block
100
+ end
101
+
102
+ def helpers
103
+ return view_context if view_context.present?
104
+
105
+ raise ArgumentError, "helpers cannot be used during initialization, as it depends on the view context"
106
+ end
107
+
108
+ def render_in(view_context, &block)
109
+ @view_context = view_context
110
+
111
+ @content_block = capture { block.call(self) } if block_given?
112
+ template
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Button < Base
6
+ variant do
7
+ base do
8
+ ["font-weight: 400",
9
+ "color: #333",
10
+ "text-decoration: none",
11
+ "display: inline-block",
12
+ "padding: 10px 20px",
13
+ "border-radius: 4px",
14
+ "border: 1px solid #ccc",
15
+ "background-color: #f4f4f4",
16
+ "color: #fff"]
17
+ end
18
+
19
+ type do
20
+ primary { ["background-color: #3498db", "border-color: #3498db"] }
21
+ success { ["background-color: #2ecc71", "border-color: #2ecc71"] }
22
+ warning { ["background-color: #f39c12", "border-color: #f39c12"] }
23
+ danger { ["background-color: #e74c3c", "border-color: #e74c3c"] }
24
+ end
25
+
26
+ size do
27
+ sm { ["font-size: 13px", "padding: 6px 16px", "line-height: 16px"] }
28
+ md { ["font-size: 16px", "padding: 8px 24px", "line-height: 20px"] }
29
+ lg { ["font-size: 19px", "padding: 16px 28px", "line-height: 24px"] }
30
+ end
31
+ end
32
+
33
+ default_variant type: :primary, size: :md
34
+
35
+ def initialize(text = nil, href, type: :primary, size: :md)
36
+ super()
37
+ @text = text
38
+ @href = href
39
+ @type = type
40
+ @size = size
41
+ end
42
+
43
+ def text
44
+ content.presence || @text
45
+ end
46
+
47
+ def template
48
+ style = style(type:, size:)
49
+
50
+ tag.a(text, href:, style:)
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :href, :type, :size
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Divider < Base
6
+ variant do
7
+ base { "border: none; border-top: 1px solid #ddd; margin: 20px 0;" }
8
+ end
9
+
10
+ def template
11
+ tag.hr(style:)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Heading < Base
6
+ variant do
7
+ base { ["font-weight: 400"] }
8
+
9
+ level do
10
+ h1 { ["font-size: 45px", "line-height: 50px"] }
11
+ h2 { ["font-size: 40px", "line-height: 45px"] }
12
+ h3 { ["font-size: 35px", "line-height: 40px"] }
13
+ h4 { ["font-size: 30px", "line-height: 35px"] }
14
+ h5 { ["font-size: 25px", "line-height: 30px"] }
15
+ h6 { ["font-size: 20px", "line-height: 25px"] }
16
+ end
17
+
18
+ align do
19
+ left { "text-align: left" }
20
+ center { "text-align: center" }
21
+ right { "text-align: right" }
22
+ end
23
+
24
+ color do
25
+ regular { "color: #14181F" }
26
+ placeholder { "color: #8B939F" }
27
+ info { "color: #1C51A4" }
28
+ success { "color: #60830D" }
29
+ warning { "color: #82620F" }
30
+ error { "color: #AB2816" }
31
+ end
32
+
33
+ weight do
34
+ regular { "font-weight: 400" }
35
+ bold { "font-weight: 700" }
36
+ end
37
+ end
38
+
39
+ default_variant level: :h1, align: :center, color: :regular, weight: :regular
40
+
41
+ def initialize(text = nil, level: nil, align: nil, color: nil, weight: nil)
42
+ super()
43
+ @text = text
44
+ @level = level
45
+ @align = align
46
+ @color = color
47
+ @weight = weight
48
+ end
49
+
50
+ def text
51
+ content.presence || @text
52
+ end
53
+
54
+ def template
55
+ style = style(level:, align:, color:, weight:)
56
+
57
+ tag.send(level, text, style:)
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :level, :align, :color, :weight
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ module Layouts
6
+ class Columns < Base
7
+ variant do
8
+ base { ["display: inline"] }
9
+ end
10
+
11
+ variant :col do
12
+ base do
13
+ [
14
+ "width: #{(100 / cols.size.to_f).round(4)}%",
15
+ "display: inline-block",
16
+ "vertical-align: top",
17
+ "text-align: center"
18
+ ]
19
+ end
20
+ end
21
+
22
+ slots_many :cols
23
+
24
+ def template
25
+ tag.div(style:) do
26
+ capture do
27
+ cols.map do |col|
28
+ concat tag.div(col, style: style(:col))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ module Layouts
6
+ class Cta < Base
7
+ variant :card do
8
+ base do
9
+ [
10
+ "border-collapse: unset", "width: 100%", "border-spacing: 0",
11
+ "border-radius: 8px", "border: 2px solid #ddd", "padding: 20px"
12
+ ]
13
+ end
14
+ end
15
+
16
+ slots_one :title
17
+ slots_one :body
18
+ slots_one :button
19
+
20
+ def template
21
+ tag.table(style: style(:card)) do
22
+ tag.tbody do
23
+ tag.tr do
24
+ tag.td(style:) do
25
+ concat title
26
+ concat render(Spacer.new)
27
+ concat body
28
+ concat render(Spacer.new)
29
+ concat button
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end