tailmix 0.4.6 → 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
@@ -182,16 +183,7 @@ modal = ModalComponent.new(size: :lg, open: true)
182
183
  ui = modal.ui
183
184
 
184
185
 
185
- def stringify_keys(obj)
186
- case obj
187
- when Hash
188
- obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
189
- when Array
190
- obj.map { |v| stringify_keys(v) }
191
- else
192
- obj
193
- end
194
- end
186
+
195
187
 
196
188
  # puts "Definition:"
197
189
  # puts JSON.pretty_generate(stringify_keys(ModalComponent.tailmix_definition.to_h))
@@ -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
@@ -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
@@ -27,12 +29,24 @@ module Tailmix
27
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
@@ -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
 
@@ -7,12 +7,12 @@ module Tailmix
7
7
  def to_h
8
8
  {
9
9
  elements: elements.transform_values(&:to_h),
10
- actions: actions.transform_values(&:to_h)
10
+ actions: actions.transform_values(&:to_h),
11
11
  }
12
12
  end
13
13
  end
14
14
 
15
- Element = Struct.new(:name, :attributes, :dimensions, :stimulus, keyword_init: true) do
15
+ Element = Struct.new(:name, :attributes, :dimensions, :stimulus, :compound_variants, keyword_init: true) do
16
16
  def to_h
17
17
  {
18
18
  name: name,
@@ -29,7 +29,8 @@ module Tailmix
29
29
  end
30
30
  end
31
31
  end,
32
- stimulus: stimulus.to_h
32
+ stimulus: stimulus.to_h,
33
+ compound_variants: compound_variants
33
34
  }
34
35
  end
35
36
  end
@@ -19,6 +19,8 @@ module Tailmix
19
19
 
20
20
  output << generate_dimensions_docs
21
21
  output << ""
22
+ output << generate_compound_variants_docs # <-- Наш новый метод
23
+ output << ""
22
24
  output << generate_actions_docs
23
25
  output << ""
24
26
  output << generate_stimulus_docs
@@ -60,6 +62,35 @@ module Tailmix
60
62
  output.join("\n")
61
63
  end
62
64
 
65
+ def generate_compound_variants_docs
66
+ output = []
67
+
68
+ compound_variants_by_element = @definition.elements.values.select do |el|
69
+ el.compound_variants.any?
70
+ end
71
+
72
+ if compound_variants_by_element.any?
73
+ output << "Compound Variants:"
74
+ compound_variants_by_element.each do |element|
75
+ output << " - on element `:#{element.name}`:"
76
+ element.compound_variants.each do |cv|
77
+ conditions = cv[:on].map { |k, v| "#{k}: :#{v}" }.join(", ")
78
+ output << " - on: { #{conditions} }"
79
+
80
+ modifications = cv[:modifications]
81
+ modifications.class_groups.each do |group|
82
+ label = group[:options][:group] ? "(group: :#{group[:options][:group]})" : ""
83
+ output << " - classes #{label}: \"#{group[:classes].join(' ')}\""
84
+ end
85
+ output << " - data: #{modifications.data.inspect}" if modifications.data.any?
86
+ output << " - aria: #{modifications.aria.inspect}" if modifications.aria.any?
87
+ end
88
+ end
89
+ end
90
+
91
+ output.join("\n")
92
+ end
93
+
63
94
  def generate_actions_docs
64
95
  output = []
65
96
  actions = @definition.actions
@@ -46,7 +46,7 @@ module Tailmix
46
46
  { class: element_def.attributes.classes },
47
47
  element_name: element_def.name,
48
48
  variant_string: variant_string,
49
- )
49
+ )
50
50
 
51
51
  element_def.dimensions.each do |name, dim_def|
52
52
  value = dimensions.fetch(name, dim_def[:default])
@@ -60,6 +60,21 @@ module Tailmix
60
60
  attributes.aria.merge!(variant_def.aria)
61
61
  end
62
62
 
63
+ element_def.compound_variants.each do |cv|
64
+ conditions = cv[:on]
65
+ modifications = cv[:modifications]
66
+
67
+ match = conditions.all? do |key, value|
68
+ dimensions[key] == value
69
+ end
70
+
71
+ if match
72
+ attributes.classes.add(modifications.classes)
73
+ attributes.data.merge!(modifications.data)
74
+ attributes.aria.merge!(modifications.aria)
75
+ end
76
+ end
77
+
63
78
  Stimulus::Compiler.call(
64
79
  definition: element_def.stimulus,
65
80
  data_map: attributes.data,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tailmix
4
- VERSION = "0.4.6"
4
+ VERSION = "0.4.7"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tailmix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Fokin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-25 00:00:00.000000000 Z
11
+ date: 2025-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,10 +52,10 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
- description: Tailmix provides a powerful DSL to define component style schemas, including
56
- variants and parts. It enables clean, co-located style management and offers a rich
57
- runtime API for dynamic class manipulation, perfect for Hotwire/Turbo and utility-first
58
- CSS.
55
+ description: Tailmix provides a powerful DSL to define component attribute schemas,
56
+ including variants, compound variants, and states. It enables clean, co-located
57
+ presentational logic (CSS classes, data attributes, ARIA roles) and offers a rich
58
+ runtime API for dynamic manipulation, perfect for Hotwire/Turbo.
59
59
  email:
60
60
  - alexander.s.fokin@gmail.com
61
61
  executables: []
@@ -73,7 +73,14 @@ files:
73
73
  - app/javascript/tailmix/mutator.js
74
74
  - app/javascript/tailmix/runner.js
75
75
  - app/javascript/tailmix/stimulus_adapter.js
76
+ - docs/01_getting_started.md
77
+ - docs/02_dsl_reference.md
78
+ - docs/03_advanced_usage.md
79
+ - docs/04_client_side_bridge.md
80
+ - docs/05_cookbook.md
76
81
  - examples/_modal_component.arb
82
+ - examples/button.rb
83
+ - examples/helpers.rb
77
84
  - examples/modal_component.rb
78
85
  - lib/generators/tailmix/install_generator.rb
79
86
  - lib/tailmix.rb
@@ -132,5 +139,5 @@ requirements: []
132
139
  rubygems_version: 3.5.22
133
140
  signing_key:
134
141
  specification_version: 4
135
- summary: A declarative class manager for Ruby UI components.
142
+ summary: A declarative, state-driven attribute manager for Ruby UI components.
136
143
  test_files: []