warped 0.1.0 → 1.0.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 (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