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.
@@ -7,25 +7,73 @@ 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,
19
19
  attributes: attributes.to_h,
20
- dimensions: dimensions,
21
- stimulus: stimulus.to_h
20
+ dimensions: dimensions.transform_values do |dimension|
21
+ dimension.transform_values do |value|
22
+ case value
23
+ when Variant
24
+ value.to_h
25
+ when Hash
26
+ value.transform_values { |v| v.respond_to?(:to_h) ? v.to_h : v }
27
+ else
28
+ value
29
+ end
30
+ end
31
+ end,
32
+ stimulus: stimulus.to_h,
33
+ compound_variants: compound_variants
22
34
  }
23
35
  end
24
36
  end
25
37
 
26
- Attributes = Struct.new(:classes, keyword_init: true)
27
- Stimulus = Struct.new(:definitions, keyword_init: true)
28
- Action = Struct.new(:action, :mutations, keyword_init: true)
38
+ Variant = Struct.new(:class_groups, :data, :aria, keyword_init: true) do
39
+ def classes
40
+ class_groups.flat_map { |group| group[:classes] }
41
+ end
42
+
43
+ def to_h
44
+ {
45
+ classes: classes,
46
+ class_groups: class_groups,
47
+ data: data,
48
+ aria: aria
49
+ }
50
+ end
51
+ end
52
+
53
+ Attributes = Struct.new(:classes, keyword_init: true) do
54
+ def to_h
55
+ {
56
+ classes: classes
57
+ }
58
+ end
59
+ end
60
+
61
+ Stimulus = Struct.new(:definitions, keyword_init: true) do
62
+ def to_h
63
+ {
64
+ definitions: definitions
65
+ }
66
+ end
67
+ end
68
+
69
+ Action = Struct.new(:action, :mutations, keyword_init: true) do
70
+ def to_h
71
+ {
72
+ action: action,
73
+ mutations: mutations
74
+ }
75
+ end
76
+ end
29
77
  end
30
78
  end
31
- end
79
+ 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
@@ -43,8 +45,14 @@ module Tailmix
43
45
  all_dimensions.each do |dim_name, config|
44
46
  default_info = config[:default] ? "(default: #{config[:default].inspect})" : ""
45
47
  output << " - #{dim_name} #{default_info}"
46
- config[:options].each do |option_key, option_value|
47
- output << " - #{option_key.inspect}: \"#{option_value.join(' ')}\""
48
+ config[:variants].each do |variant_name, variant_def|
49
+ output << " - #{variant_name.inspect}:"
50
+ variant_def.class_groups.each do |group|
51
+ label = group[:options][:group] ? "(group: :#{group[:options][:group]})" : ""
52
+ output << " - classes #{label}: \"#{group[:classes].join(' ')}\""
53
+ end
54
+ output << " - data: #{variant_def.data.inspect}" if variant_def.data.any?
55
+ output << " - aria: #{variant_def.aria.inspect}" if variant_def.aria.any?
48
56
  end
49
57
  end
50
58
  else
@@ -54,6 +62,35 @@ module Tailmix
54
62
  output.join("\n")
55
63
  end
56
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
+
57
94
  def generate_actions_docs
58
95
  output = []
59
96
  actions = @definition.actions
@@ -21,6 +21,10 @@ module Tailmix
21
21
  def stimulus
22
22
  StimulusGenerator.new(@definition, @component_class.name)
23
23
  end
24
+
25
+ def elements
26
+ @definition.elements.values.map(&:name)
27
+ end
24
28
  end
25
29
  end
26
30
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "definition/context_builder"
4
+ require_relative "definition/merger"
5
+ require_relative "dev/tools"
6
+
7
+ module Tailmix
8
+ # The main DSL for defining component styles and behaviors.
9
+ # This module is extended into any class that includes Tailmix.
10
+ module DSL
11
+ def tailmix(&block)
12
+ child_context = Definition::ContextBuilder.new
13
+ child_context.instance_eval(&block)
14
+ child_definition = child_context.build_definition
15
+
16
+ if superclass.respond_to?(:tailmix_definition) && (parent_definition = superclass.tailmix_definition)
17
+ @tailmix_definition = Definition::Merger.call(parent_definition, child_definition)
18
+ else
19
+ @tailmix_definition = child_definition
20
+ end
21
+ end
22
+
23
+ def tailmix_definition
24
+ @tailmix_definition || raise(Error, "Tailmix definition not found in #{name}")
25
+ end
26
+
27
+ def tailmix_facade_class
28
+ @_tailmix_facade_class ||= Runtime::FacadeBuilder.build(tailmix_definition)
29
+ end
30
+
31
+ def dev
32
+ Dev::Tools.new(self)
33
+ end
34
+ end
35
+ end
@@ -3,18 +3,30 @@
3
3
  require "erb"
4
4
  require_relative "class_list"
5
5
  require_relative "data_map"
6
+ require_relative "selector"
6
7
 
7
8
  module Tailmix
8
9
  module HTML
9
10
  class Attributes < Hash
10
- attr_reader :element_name
11
+ attr_reader :element_name, :variant_string
11
12
 
12
- def initialize(initial_hash = {}, element_name: nil)
13
+ def initialize(initial_hash = {}, element_name: nil, variant_string: "")
13
14
  @element_name = element_name
15
+ @variant_string = variant_string
14
16
  super()
15
- self[:class] = ClassList.new
16
- self[:data] = DataMap.new
17
- merge!(initial_hash)
17
+
18
+ attrs_to_merge = initial_hash.dup
19
+
20
+ initial_classes = attrs_to_merge.delete(:class)
21
+ initial_data = attrs_to_merge.delete(:data)
22
+ initial_aria = attrs_to_merge.delete(:aria)
23
+
24
+ self[:class] = ClassList.new(initial_classes)
25
+ self[:data] = DataMap.new("data", initial_data || {})
26
+ self[:aria] = DataMap.new("aria", initial_aria || {})
27
+ self[:tailmix] = Selector.new(element_name, variant_string)
28
+
29
+ merge!(attrs_to_merge)
18
30
  end
19
31
 
20
32
  def each(&block)
@@ -22,15 +34,14 @@ module Tailmix
22
34
  end
23
35
 
24
36
  def to_h
25
- final_attrs = select { |k, _| !%i[class data].include?(k.to_sym) }
37
+ final_attrs = select { |k, _| !%i[class data aria tailmix].include?(k.to_sym) }
38
+
26
39
  class_string = self[:class].to_s
27
40
  final_attrs[:class] = class_string unless class_string.empty?
28
- final_attrs.merge!(self[:data].to_h)
29
41
 
30
- selector_attr = Tailmix.configuration.element_selector_attribute
31
- if selector_attr && @element_name
32
- final_attrs[selector_attr] = @element_name
33
- end
42
+ final_attrs.merge!(self[:data].to_h)
43
+ final_attrs.merge!(self[:aria].to_h)
44
+ final_attrs.merge!(self[:tailmix].to_h)
34
45
 
35
46
  final_attrs
36
47
  end
@@ -48,10 +59,18 @@ module Tailmix
48
59
  self[:data]
49
60
  end
50
61
 
62
+ def aria
63
+ self[:aria]
64
+ end
65
+
51
66
  def stimulus
52
67
  data.stimulus
53
68
  end
54
69
 
70
+ def tailmix
71
+ self[:tailmix]
72
+ end
73
+
55
74
  def toggle(class_names)
56
75
  classes.toggle(class_names)
57
76
  self
@@ -66,6 +85,10 @@ module Tailmix
66
85
  classes.remove(class_names)
67
86
  self
68
87
  end
88
+
89
+ def each_attribute(&block)
90
+ [ classes: classes, data: data.to_h, aria: aria.to_h, tailmix: tailmix.to_h ].each(&block)
91
+ end
69
92
  end
70
93
  end
71
94
  end
@@ -9,12 +9,14 @@ module Tailmix
9
9
  class DataMap
10
10
  MERGEABLE_LIST_ATTRIBUTES = %i[controller action target].freeze
11
11
 
12
- def initialize(initial_data = {})
12
+ def initialize(prefix, initial_data = {})
13
+ @prefix = prefix
13
14
  @data = {}
14
15
  merge!(initial_data)
15
16
  end
16
17
 
17
18
  def stimulus
19
+ raise "Stimulus builder is only available for data attributes" unless @prefix == "data"
18
20
  StimulusBuilder.new(self)
19
21
  end
20
22
 
@@ -26,7 +28,7 @@ module Tailmix
26
28
  key = key.to_sym
27
29
  if value.is_a?(Hash) && @data[key].is_a?(Hash)
28
30
  @data[key].merge!(value)
29
- elsif MERGEABLE_LIST_ATTRIBUTES.include?(key)
31
+ elsif @prefix == "data" && MERGEABLE_LIST_ATTRIBUTES.include?(key)
30
32
  add_to_set(key, value)
31
33
  else
32
34
  @data[key] = value
@@ -67,15 +69,15 @@ module Tailmix
67
69
  end
68
70
 
69
71
  def to_h
70
- flatten_data_hash(@data)
72
+ flatten_data_hash(@data, @prefix)
71
73
  end
72
74
 
73
75
  private
74
76
 
75
- def flatten_data_hash(hash, prefix = "data", accumulator = {})
77
+ def flatten_data_hash(hash, prefix, accumulator = {})
76
78
  hash.each do |key, value|
77
79
  current_key = "#{prefix}-#{key.to_s.tr('_', '-')}"
78
- if key.to_s.end_with?("_value")
80
+ if @prefix == "data" && key.to_s.end_with?("_value")
79
81
  serialized_value = case value
80
82
  when Hash, Array then value.to_json
81
83
  else value
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module HTML
5
+ class Selector
6
+ def initialize(element_name, variant_string)
7
+ @element_name = element_name
8
+ @variant_string = variant_string
9
+ end
10
+
11
+ def to_h
12
+ return {} unless @element_name
13
+
14
+ key = "data-tailmix-#{@element_name}"
15
+ { key => @variant_string }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -38,28 +38,51 @@ module Tailmix
38
38
 
39
39
  def build_attributes_for(element_name, dimensions)
40
40
  element_def = @definition.elements.fetch(element_name)
41
- initial_classes = element_def.attributes.classes
42
- class_list = HTML::ClassList.new(initial_classes)
43
41
 
44
- element_def.dimensions.each do |name, dim|
45
- value = dimensions.fetch(name, dim[:default])
42
+ active_dimensions = dimensions.slice(*element_def.dimensions.keys)
43
+ variant_string = active_dimensions.map { |k, v| "#{k}:#{v}" }.join(",")
44
+
45
+ attributes = HTML::Attributes.new(
46
+ { class: element_def.attributes.classes },
47
+ element_name: element_def.name,
48
+ variant_string: variant_string,
49
+ )
50
+
51
+ element_def.dimensions.each do |name, dim_def|
52
+ value = dimensions.fetch(name, dim_def[:default])
46
53
  next if value.nil?
47
- classes_for_option = dim.fetch(:options, {}).fetch(value, nil)
48
- class_list.add(classes_for_option)
54
+
55
+ variant_def = dim_def.fetch(:variants, {}).fetch(value, nil)
56
+ next unless variant_def
57
+
58
+ attributes.classes.add(variant_def.classes)
59
+ attributes.data.merge!(variant_def.data)
60
+ attributes.aria.merge!(variant_def.aria)
61
+ end
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
49
76
  end
50
77
 
51
- data_map = HTML::DataMap.new
52
78
  Stimulus::Compiler.call(
53
79
  definition: element_def.stimulus,
54
- data_map: data_map,
80
+ data_map: attributes.data,
55
81
  root_definition: @definition,
56
82
  component: @component_instance
57
83
  )
58
84
 
59
- HTML::Attributes.new(
60
- { class: class_list, data: data_map },
61
- element_name: element_def.name
62
- )
85
+ attributes
63
86
  end
64
87
  end
65
88
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tailmix
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.7"
5
5
  end
data/lib/tailmix.rb CHANGED
@@ -1,66 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "tailmix/version"
4
+ require_relative "tailmix/configuration"
5
+ require_relative "tailmix/dsl"
4
6
  require_relative "tailmix/definition"
5
7
  require_relative "tailmix/runtime"
6
- require_relative "tailmix/dev/tools"
7
8
 
8
9
  module Tailmix
9
10
  class Error < StandardError; end
10
11
 
11
12
  class << self
12
13
  attr_writer :configuration
13
- end
14
-
15
- def self.configuration
16
- @configuration ||= Configuration.new
17
- end
18
-
19
- def self.configure
20
- yield(configuration)
21
- end
22
-
23
- class Configuration
24
- attr_accessor :element_selector_attribute
25
-
26
- def initialize
27
- element_selector_attribute = nil
28
- end
29
- end
30
-
31
- module ClassMethods
32
- def tailmix(&block)
33
- child_context = Definition::ContextBuilder.new
34
- child_context.instance_eval(&block)
35
- child_definition = child_context.build_definition
36
-
37
- if superclass.respond_to?(:tailmix_definition) && (parent_definition = superclass.tailmix_definition)
38
- @tailmix_definition = Definition::Merger.call(parent_definition, child_definition)
39
- else
40
- @tailmix_definition = child_definition
41
- end
42
- end
43
-
44
- def tailmix_definition
45
- @tailmix_definition || raise(Error, "Tailmix definition not found in #{name}")
46
- end
47
14
 
48
- def tailmix_facade_class
49
- @_tailmix_facade_class ||= Runtime::FacadeBuilder.build(tailmix_definition)
15
+ def configuration
16
+ @configuration ||= Configuration.new
50
17
  end
51
18
 
52
- def dev
53
- Dev::Tools.new(self)
19
+ def configure
20
+ yield(configuration)
54
21
  end
55
22
  end
56
23
 
57
24
  def self.included(base)
58
- base.extend(ClassMethods)
25
+ base.extend(DSL)
59
26
  end
60
27
 
61
- def tailmix(options = {})
62
- facade_class = self.class.tailmix_facade_class
63
- facade_class.new(self, self.class.tailmix_definition, options)
28
+ def tailmix(dimensions = {})
29
+ self.class.tailmix_facade_class.new(self, self.class.tailmix_definition, dimensions)
64
30
  end
65
31
  end
66
32
 
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.5
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-23 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,10 +73,18 @@ 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
87
+ - lib/tailmix/configuration.rb
80
88
  - lib/tailmix/definition.rb
81
89
  - lib/tailmix/definition/context_builder.rb
82
90
  - lib/tailmix/definition/contexts/action_builder.rb
@@ -85,15 +93,18 @@ files:
85
93
  - lib/tailmix/definition/contexts/dimension_builder.rb
86
94
  - lib/tailmix/definition/contexts/element_builder.rb
87
95
  - lib/tailmix/definition/contexts/stimulus_builder.rb
96
+ - lib/tailmix/definition/contexts/variant_builder.rb
88
97
  - lib/tailmix/definition/merger.rb
89
98
  - lib/tailmix/definition/result.rb
90
99
  - lib/tailmix/dev/docs.rb
91
100
  - lib/tailmix/dev/stimulus_generator.rb
92
101
  - lib/tailmix/dev/tools.rb
102
+ - lib/tailmix/dsl.rb
93
103
  - lib/tailmix/engine.rb
94
104
  - lib/tailmix/html/attributes.rb
95
105
  - lib/tailmix/html/class_list.rb
96
106
  - lib/tailmix/html/data_map.rb
107
+ - lib/tailmix/html/selector.rb
97
108
  - lib/tailmix/html/stimulus_builder.rb
98
109
  - lib/tailmix/runtime.rb
99
110
  - lib/tailmix/runtime/action.rb
@@ -128,5 +139,5 @@ requirements: []
128
139
  rubygems_version: 3.5.22
129
140
  signing_key:
130
141
  specification_version: 4
131
- summary: A declarative class manager for Ruby UI components.
142
+ summary: A declarative, state-driven attribute manager for Ruby UI components.
132
143
  test_files: []