tailmix 0.4.5 → 0.4.7

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.
@@ -0,0 +1,249 @@
1
+ # Cookbook
2
+
3
+ This document contains practical recipes for building common UI components with Tailmix.
4
+
5
+ ## Alert Component
6
+
7
+ Alerts are used to communicate a state that affects the entire system, feature, or page.
8
+
9
+ ### 1. Component Definition
10
+
11
+ Here's the Ruby code for a flexible `AlertComponent`.
12
+
13
+ ```ruby
14
+ # app/components/alert_component.rb
15
+ class AlertComponent
16
+ include Tailmix
17
+ attr_reader :ui, :icon_svg, :message
18
+
19
+ def initialize(intent: :info, message:)
20
+ @ui = tailmix(intent: intent)
21
+ @message = message
22
+ @icon_svg = fetch_icon(intent) # Logic to get the correct SVG icon
23
+ end
24
+
25
+ private
26
+
27
+ def fetch_icon(intent)
28
+ # ...
29
+ end
30
+
31
+ tailmix do
32
+ element :wrapper, "flex items-center p-4 text-sm border rounded-lg" do
33
+ dimension :intent, default: :info do
34
+ variant :info, "text-blue-800 bg-blue-50 border-blue-300"
35
+ variant :success, "text-green-800 bg-green-50 border-green-300"
36
+ variant :warning, "text-yellow-800 bg-yellow-50 border-yellow-300"
37
+ variant :danger, "text-red-800 bg-red-50 border-red-300"
38
+ end
39
+ end
40
+
41
+ element :icon, "flex-shrink-0 w-5 h-5"
42
+ element :message_area, "ml-3"
43
+ end
44
+ end
45
+ ```
46
+
47
+ #### View Usage (ERB)
48
+ Instantiate the component in your controller or view and use the ui helper to render the elements.
49
+
50
+ ```html
51
+ <%# Success Alert %>
52
+ <% success_alert = AlertComponent.new(intent: :success, message: "Your profile has been updated.") %>
53
+
54
+ <div <%= tag.attributes **success_alert.ui.wrapper %>>
55
+ <div <%= tag.attributes **success_alert.ui.icon %>>
56
+ <%= success_alert.icon_svg.html_safe %>
57
+ </div>
58
+ <div <%= tag.attributes **success_alert.ui.message_area %>>
59
+ <%= success_alert.message %>
60
+ </div>
61
+ </div>
62
+
63
+
64
+ <%# Danger Alert %>
65
+ <% danger_alert = AlertComponent.new(intent: :danger, message: "Failed to delete the record.") %>
66
+
67
+ <div <%= tag.attributes **danger_alert.ui.wrapper %>>
68
+ <div <%= tag.attributes **danger_alert.ui.icon %>>
69
+ <%= danger_alert.icon_svg.html_safe %>
70
+ </div>
71
+ <div <%= tag.attributes **danger_alert.ui.message_area %>>
72
+ <%= danger_alert.message %>
73
+ </div>
74
+ </div>
75
+ ```
76
+
77
+ #### View Usage .arb (Ruby Arbre)
78
+
79
+ ```ruby
80
+ # Success Alert
81
+ success_alert = AlertComponent.new(intent: :success, message: "Your profile has been updated.")
82
+ ui = success_alert.ui
83
+
84
+ div ui.wrapper do
85
+ div ui.icon do
86
+ success_alert.icon_svg.html_safe
87
+ end
88
+ div ui.message_area do
89
+ success_alert.message
90
+ end
91
+ end
92
+
93
+ # Danger Alert
94
+ danger_alert = AlertComponent.new(intent: :danger, message: "Failed to delete the record.")
95
+ ui = danger_alert.ui
96
+
97
+ div ui.wrapper do
98
+ div ui.icon do
99
+ danger_alert.icon_svg.html_safe
100
+ end
101
+ div ui.message_area do
102
+ danger_alert.message
103
+ end
104
+ end
105
+ ```
106
+
107
+ ## Badge Component
108
+
109
+ Badges are used for labeling, categorization, or highlighting small pieces of information. This recipe shows how to combine multiple dimensions like `size` and `color`.
110
+
111
+ ### 1. Component Definition
112
+
113
+ ```ruby
114
+ # app/components/badge_component.rb
115
+ class BadgeComponent
116
+ include Tailmix
117
+ attr_reader :ui, :text
118
+
119
+ def initialize(text, color: :gray, size: :sm)
120
+ @ui = tailmix(color: color, size: size)
121
+ @text = text
122
+ end
123
+
124
+ tailmix do
125
+ element :badge, "inline-flex items-center font-medium rounded-full" do
126
+ dimension :color, default: :gray do
127
+ variant :gray, "bg-gray-100 text-gray-600"
128
+ variant :success, "bg-green-100 text-green-700"
129
+ variant :warning, "bg-yellow-100 text-yellow-700"
130
+ variant :danger, "bg-red-100 text-red-700"
131
+ end
132
+
133
+ dimension :size, default: :sm do
134
+ variant :sm, "px-2.5 py-0.5 text-xs"
135
+ variant :md, "px-3 py-1 text-sm"
136
+ end
137
+ end
138
+ end
139
+ end
140
+ ```
141
+
142
+ #### View Usage (ERB)
143
+
144
+ ```html
145
+ <%# A medium-sized success badge %>
146
+ <% success_badge = BadgeComponent.new("Active", color: :success, size: :md) %>
147
+ <span <%= tag.attributes **success_badge.ui.badge %>>
148
+ <%= success_badge.text %>
149
+ </span>
150
+
151
+ <%# A small danger badge %>
152
+ <% danger_badge = BadgeComponent.new("Inactive", color: :danger, size: :sm) %>
153
+ <span <%= tag.attributes **danger_badge.ui.badge %>>
154
+ <%= danger_badge.text %>
155
+ </span>
156
+ ```
157
+
158
+ #### View Usage .arb (Ruby Arbre)
159
+
160
+ ```ruby
161
+ # A medium-sized success badge
162
+ success_badge = BadgeComponent.new("Active", color: :success, size: :md)
163
+ ui = success_badge.ui
164
+
165
+ span ui.badge do
166
+ success_badge.text
167
+ end
168
+
169
+ # A small danger badge
170
+ danger_badge = BadgeComponent.new("Inactive", color: :danger, size: :sm)
171
+ ui = danger_badge.ui
172
+
173
+ span ui.badge do
174
+ danger_badge.text
175
+ end
176
+ ```
177
+
178
+ ## Card Component
179
+
180
+ Cards are flexible containers for content. This recipe shows how to build a component with multiple parts (`header`, `body`, `footer`) using multiple `element` definitions.
181
+
182
+ ### 1. Component Definition
183
+
184
+ ```ruby
185
+ # app/components/card_component.rb
186
+ class CardComponent
187
+ include Tailmix
188
+ attr_reader :ui
189
+
190
+ # We can control the footer's top border with an option
191
+ def initialize(with_divider: true)
192
+ @ui = tailmix(with_divider: with_divider)
193
+ end
194
+
195
+ tailmix do
196
+ element :wrapper, "bg-white border rounded-lg shadow-sm"
197
+ element :header, "p-4 border-b"
198
+ element :body, "p-4"
199
+ element :footer, "p-4 bg-gray-50 rounded-b-lg" do
200
+ dimension :with_divider, default: true do
201
+ variant true, "border-t"
202
+ variant false, "" # No border
203
+ end
204
+ end
205
+ end
206
+ end
207
+ ```
208
+
209
+ #### View Usage (ERB)
210
+
211
+ ```html
212
+ <% card = CardComponent.new %>
213
+
214
+ <div <%= tag.attributes **card.ui.wrapper %>>
215
+ <div <%= tag.attributes **card.ui.header %>>
216
+ <h3 class="text-lg font-medium">Card Title</h3>
217
+ </div>
218
+
219
+ <div <%= tag.attributes **card.ui.body %>>
220
+ <p>This is the main content of the card. It can contain any information you need to display.</p>
221
+ </div>
222
+
223
+ <div <%= tag.attributes **card.ui.footer %>>
224
+ <button type="button">Action Button</button>
225
+ </div>
226
+ </div>
227
+ ```
228
+
229
+ #### View Usage .arb (Ruby Arbre)
230
+
231
+ ```ruby
232
+ # Card
233
+ card = CardComponent.new
234
+ ui = card.ui
235
+
236
+ div ui.wrapper do
237
+ div ui.header do
238
+ h3 "Card Title"
239
+ end
240
+
241
+ div ui.body do
242
+ para "This is the main content of the card. It can contain any information you need to display."
243
+ end
244
+
245
+ div ui.footer do
246
+ button "Action Button"
247
+ end
248
+ end
249
+ ```
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/tailmix"
4
+ require_relative "helpers"
5
+
6
+ class Button
7
+ include Tailmix
8
+
9
+ tailmix do
10
+ element :button do
11
+ dimension :intent, default: :primary do
12
+ variant :primary, "bg-blue-500"
13
+ variant :danger, "bg-red-500"
14
+ end
15
+
16
+ dimension :size, default: :medium do
17
+ variant :medium, "p-4"
18
+ variant :small, "p-2"
19
+ end
20
+
21
+ compound_variant on: { intent: :danger, size: :small } do
22
+ classes "font-bold"
23
+ data special: "true"
24
+ end
25
+ end
26
+ end
27
+
28
+ attr_reader :ui
29
+ def initialize(intent: :primary, size: :medium)
30
+ @ui = tailmix(intent: intent, size: size)
31
+ end
32
+ end
33
+
34
+
35
+ puts "-" * 100
36
+ puts Button.dev.docs
37
+
38
+ # == Tailmix Docs for Button ==
39
+ # Signature: `initialize(intent: :primary, size: :medium)`
40
+ #
41
+ # Dimensions:
42
+ # - intent (default: :primary)
43
+ # - :primary:
44
+ # - classes : "bg-blue-500"
45
+ # - :danger:
46
+ # - classes : "bg-red-500"
47
+ # - size (default: :medium)
48
+ # - :medium:
49
+ # - classes : "p-4"
50
+ # - :small:
51
+ # - classes : "p-2"
52
+ #
53
+ # Compound Variants:
54
+ # - on element `:button`:
55
+ # - on: { intent: :danger, size: :small }
56
+ # - classes : "font-bold"
57
+ # - data: {:special=>"true"}
58
+ #
59
+ # No actions defined.
60
+ #
61
+ # button
62
+ # classes :-> bg-red-500 p-4
63
+ # data :-> {}
64
+ # aria :-> {}
65
+ # tailmix :-> {"data-tailmix-button"=>"intent:danger,size:medium"}
66
+
67
+ not_compound_component = Button.new(intent: :danger, size: :medium)
68
+ print_component_ui(not_compound_component)
69
+ # button
70
+ # classes :-> bg-red-500 p-4
71
+ # data :-> {}
72
+ # aria :-> {}
73
+ # tailmix :-> {"data-tailmix-button"=>"intent:danger,size:medium"}
74
+
75
+ compound_component = Button.new(intent: :danger, size: :small)
76
+ print_component_ui(compound_component)
77
+ # button
78
+ # classes :-> bg-red-500 p-2 font-bold
79
+ # data :-> {"data-special"=>"true"}
80
+ # aria :-> {}
81
+ # tailmix :-> {"data-tailmix-button"=>"intent:danger,size:small"}
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ def stringify_keys(obj)
4
+ case obj
5
+ when Hash
6
+ obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
7
+ when Array
8
+ obj.map { |v| stringify_keys(v) }
9
+ else
10
+ obj
11
+ end
12
+ end
13
+
14
+ def print_component_ui(component_instance)
15
+ component_instance.class.dev.elements.each do |element_name|
16
+ element = component_instance.ui.send(element_name)
17
+ puts element_name
18
+ element.each_attribute do |attribute|
19
+ attribute.each do |key, value|
20
+ puts " #{key} :-> #{value}"
21
+ end
22
+ puts ""
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../lib/tailmix"
4
+ require_relative "helpers"
4
5
 
5
6
  class ModalComponent
6
7
  include Tailmix
@@ -9,8 +10,8 @@ class ModalComponent
9
10
  tailmix do
10
11
  element :base, "fixed inset-0 z-50 flex items-center justify-center" do
11
12
  dimension :open, default: true do
12
- option true, "visible opacity-100"
13
- option false, "invisible opacity-0"
13
+ variant true, "visible opacity-100"
14
+ variant false, "invisible opacity-0"
14
15
  end
15
16
  stimulus.controller("modal").action_payload(:toggle, as: :toggle_data)
16
17
  end
@@ -21,9 +22,12 @@ class ModalComponent
21
22
 
22
23
  element :panel, "relative bg-white rounded-lg shadow-xl transition-transform transform" do
23
24
  dimension :size, default: :md do
24
- option :sm, "w-full max-w-sm p-4"
25
- option :md, "w-full max-w-md p-6"
26
- option :lg, "w-full max-w-lg p-8"
25
+ variant :sm, "w-full max-w-sm p-4" do
26
+ classes "dark:text-slate-400", group: :dark_mode
27
+ classes "one two"
28
+ end
29
+ variant :md, "w-full max-w-md p-6"
30
+ variant :lg, "w-full max-w-lg p-8"
27
31
  end
28
32
  stimulus.context("modal").target("panel")
29
33
  end
@@ -81,12 +85,12 @@ class ModalComponent
81
85
  end
82
86
 
83
87
  puts "-" * 100
84
- puts ModalComponent.dev.docs
85
- puts ""
86
- puts "Scaffolds:"
87
- puts ""
88
- puts ModalComponent.dev.stimulus.scaffold
89
- puts ""
88
+ # puts ModalComponent.dev.docs
89
+ # puts ""
90
+ # puts "Scaffolds:"
91
+ # puts ""
92
+ # puts ModalComponent.dev.stimulus.scaffold
93
+ # puts ""
90
94
 
91
95
  # >>>
92
96
  #
@@ -95,12 +99,19 @@ puts ""
95
99
  #
96
100
  # Dimensions:
97
101
  # - open (default: true)
98
- # - true: "visible opacity-100"
99
- # - false: "invisible opacity-0"
102
+ # - true:
103
+ # - classes : "visible opacity-100"
104
+ # - false:
105
+ # - classes : "invisible opacity-0"
100
106
  # - size (default: :md)
101
- # - :sm: "w-full max-w-sm p-4"
102
- # - :md: "w-full max-w-md p-6"
103
- # - :lg: "w-full max-w-lg p-8"
107
+ # - :sm:
108
+ # - classes : "w-full max-w-sm p-4"
109
+ # - classes (group: :dark_mode): "dark:text-slate-400"
110
+ # - classes : "one two"
111
+ # - :md:
112
+ # - classes : "w-full max-w-md p-6"
113
+ # - :lg:
114
+ # - classes : "w-full max-w-lg p-8"
104
115
  #
105
116
  # Actions:
106
117
  # - :toggle
@@ -167,14 +178,24 @@ puts ""
167
178
 
168
179
 
169
180
 
170
- # modal = ModalComponent.new(size: :lg, open: true)
181
+ modal = ModalComponent.new(size: :lg, open: true)
171
182
  # modal.lock!
172
- # ui = modal.ui
183
+ ui = modal.ui
184
+
185
+
186
+
173
187
 
174
188
  # puts "Definition:"
175
- # pp ModalComponent.tailmix_definition
176
- # pp ModalComponent.tailmix_definition.to_h
177
- # ui.overlay.each do |key, value|
178
- # puts "#{key} :-> #{value.inspect.to_s[0, 75]}..."
179
- # end
189
+ # puts JSON.pretty_generate(stringify_keys(ModalComponent.tailmix_definition.to_h))
180
190
  # ui.action(:lock).apply!
191
+
192
+ ModalComponent.dev.elements.each do |element_name|
193
+ element = ui.send(element_name)
194
+ puts element_name
195
+ element.each_attribute do |attribute|
196
+ attribute.each do |key, value|
197
+ puts " #{key} :-> #{value}"
198
+ end
199
+ puts ""
200
+ end
201
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ # Stores the configuration for the Tailmix gem.
5
+ class Configuration
6
+ attr_accessor :element_selector_attribute, :dev_mode_attributes
7
+
8
+ def initialize
9
+ @element_selector_attribute = nil
10
+ @dev_mode_attributes = defined?(Rails) && Rails.env.development?
11
+ end
12
+ end
13
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "contexts/action_builder"
4
4
  require_relative "contexts/element_builder"
5
+ require_relative "contexts/variant_builder"
5
6
 
6
7
  module Tailmix
7
8
  module Definition
@@ -31,9 +32,9 @@ module Tailmix
31
32
  def build_definition
32
33
  Definition::Result::Context.new(
33
34
  elements: @elements.transform_values(&:build_definition).freeze,
34
- actions: @actions.transform_values(&:build_definition).freeze
35
+ actions: @actions.transform_values(&:build_definition).freeze,
35
36
  )
36
37
  end
37
38
  end
38
39
  end
39
- end
40
+ end
@@ -10,13 +10,13 @@ module Tailmix
10
10
  @commands = []
11
11
  end
12
12
 
13
- def classes(classes_string, method: @default_method)
13
+ def classes(classes_string, options = {})
14
+ method = options.fetch(:method, @default_method)
14
15
  @commands << { field: :classes, method: method, payload: classes_string }
15
16
  end
16
17
 
17
18
  def data(data_hash)
18
19
  operation = data_hash.delete(:method) || @default_method
19
-
20
20
  @commands << { field: :data, method: operation, payload: data_hash }
21
21
  end
22
22
 
@@ -1,16 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class DimensionBuilder
4
- attr_reader :options
3
+ require_relative "variant_builder"
5
4
 
6
- def initialize(default: nil)
7
- @options = { options: {}, default: default }
8
- end
5
+ module Tailmix
6
+ module Definition
7
+ module Contexts
8
+ class DimensionBuilder
9
+ def initialize(default: nil)
10
+ @variants = {}
11
+ @default = default
12
+ end
13
+
14
+ def variant(name, classes = "", data: {}, aria: {}, &block)
15
+ builder = VariantBuilder.new
16
+ builder.classes(classes) if classes && !classes.empty?
17
+ builder.data(data)
18
+ builder.aria(aria)
19
+
20
+ builder.instance_eval(&block) if block
21
+
22
+ @variants[name] = builder.build_variant
23
+ end
9
24
 
10
- def option(value, classes, default: false)
11
- @options[:options][value] = classes.split
12
- if default && @options[:default].nil?
13
- @options[:default] = value
25
+ def build_dimension
26
+ {
27
+ default: @default,
28
+ variants: @variants.freeze
29
+ }
30
+ end
31
+ end
14
32
  end
15
33
  end
16
34
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "attribute_builder"
4
4
  require_relative "stimulus_builder"
5
5
  require_relative "dimension_builder"
6
+ require_relative "variant_builder"
6
7
 
7
8
  module Tailmix
8
9
  module Definition
@@ -11,6 +12,7 @@ module Tailmix
11
12
  def initialize(name)
12
13
  @name = name
13
14
  @dimensions = {}
15
+ @compound_variants = []
14
16
  end
15
17
 
16
18
  def attributes
@@ -22,17 +24,29 @@ module Tailmix
22
24
  end
23
25
 
24
26
  def dimension(name, default: nil, &block)
25
- dimension = DimensionBuilder.new(default: default)
26
- dimension.instance_eval(&block)
27
- @dimensions[name.to_sym] = dimension.options
27
+ builder = Contexts::DimensionBuilder.new(default: default)
28
+ builder.instance_eval(&block)
29
+ @dimensions[name.to_sym] = builder.build_dimension
28
30
  end
29
31
 
32
+ def compound_variant(on:, &block)
33
+ builder = VariantBuilder.new
34
+ builder.instance_eval(&block)
35
+
36
+ @compound_variants << {
37
+ on: on,
38
+ modifications: builder.build_variant
39
+ }
40
+ end
41
+
42
+
30
43
  def build_definition
31
44
  Definition::Result::Element.new(
32
45
  name: @name,
33
46
  attributes: attributes.build_definition,
34
47
  stimulus: stimulus.build_definition,
35
- dimensions: @dimensions.freeze
48
+ dimensions: @dimensions.freeze,
49
+ compound_variants: @compound_variants.freeze
36
50
  )
37
51
  end
38
52
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Contexts
6
+ class VariantBuilder
7
+ def initialize
8
+ @class_groups = []
9
+ @data = {}
10
+ @aria = {}
11
+ end
12
+
13
+ def classes(class_string, options = {})
14
+ @class_groups << { classes: class_string.to_s.split, options: options }
15
+ end
16
+
17
+ def data(hash)
18
+ @data.merge!(hash)
19
+ end
20
+
21
+ def aria(hash)
22
+ @aria.merge!(hash)
23
+ end
24
+
25
+ def build_variant
26
+ Definition::Result::Variant.new(
27
+ class_groups: @class_groups.freeze,
28
+ data: @data.freeze,
29
+ aria: @aria.freeze
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -47,7 +47,8 @@ module Tailmix
47
47
  name: parent_el.name,
48
48
  attributes: merge_attributes(parent_el.attributes, child_el.attributes),
49
49
  dimensions: merge_dimensions(parent_el.dimensions, child_el.dimensions),
50
- stimulus: merge_stimulus(parent_el.stimulus, child_el.stimulus)
50
+ stimulus: merge_stimulus(parent_el.stimulus, child_el.stimulus),
51
+ compound_variants: parent_el.compound_variants + child_el.compound_variants
51
52
  )
52
53
  end
53
54
 
@@ -63,28 +64,21 @@ module Tailmix
63
64
  Result::Stimulus.new(definitions: combined_definitions)
64
65
  end
65
66
 
66
- private
67
-
68
67
  def merge_dimensions(parent_dims, child_dims)
69
- deep_merge(parent_dims, child_dims) do |key, parent_val, child_val|
70
- if key == :options && parent_val.is_a?(Hash) && child_val.is_a?(Hash)
71
- parent_val.merge(child_val)
72
- else
73
- child_val
74
- end
75
- end
76
- end
68
+ all_keys = parent_dims.keys | child_dims.keys
69
+
70
+ all_keys.each_with_object({}) do |key, merged|
71
+ parent_val = parent_dims[key]
72
+ child_val = child_dims[key]
73
+
74
+ if parent_val && child_val
75
+ merged_variants = parent_val.fetch(:variants, {}).merge(child_val.fetch(:variants, {}))
77
76
 
78
- def deep_merge(parent_hash, child_hash, &block)
79
- child_hash.each_with_object(parent_hash.dup) do |(key, child_val), new_hash|
80
- parent_val = new_hash[key]
77
+ default = child_val.key?(:default) ? child_val[:default] : parent_val[:default]
81
78
 
82
- new_hash[key] = if parent_val.is_a?(Hash) && child_val.is_a?(Hash)
83
- deep_merge(parent_val, child_val, &block)
84
- elsif block_given? && new_hash.key?(key)
85
- block.call(key, parent_val, child_val)
79
+ merged[key] = { default: default, variants: merged_variants }
86
80
  else
87
- child_val
81
+ merged[key] = parent_val || child_val
88
82
  end
89
83
  end
90
84
  end