tailmix 0.2.0 → 0.4.6

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/README.md +147 -85
  4. data/app/javascript/tailmix/finder.js +15 -0
  5. data/app/javascript/tailmix/index.js +7 -0
  6. data/app/javascript/tailmix/mutator.js +28 -0
  7. data/app/javascript/tailmix/runner.js +7 -0
  8. data/app/javascript/tailmix/stimulus_adapter.js +37 -0
  9. data/examples/_modal_component.arb +36 -0
  10. data/examples/modal_component.rb +209 -0
  11. data/lib/generators/tailmix/install_generator.rb +19 -0
  12. data/lib/tailmix/configuration.rb +13 -0
  13. data/lib/tailmix/definition/context_builder.rb +39 -0
  14. data/lib/tailmix/definition/contexts/action_builder.rb +31 -0
  15. data/lib/tailmix/definition/contexts/actions/element_builder.rb +30 -0
  16. data/lib/tailmix/definition/contexts/attribute_builder.rb +21 -0
  17. data/lib/tailmix/definition/contexts/dimension_builder.rb +34 -0
  18. data/lib/tailmix/definition/contexts/element_builder.rb +41 -0
  19. data/lib/tailmix/definition/contexts/stimulus_builder.rb +101 -0
  20. data/lib/tailmix/definition/contexts/variant_builder.rb +35 -0
  21. data/lib/tailmix/definition/merger.rb +86 -0
  22. data/lib/tailmix/definition/result.rb +78 -0
  23. data/lib/tailmix/definition.rb +11 -0
  24. data/lib/tailmix/dev/docs.rb +88 -0
  25. data/lib/tailmix/dev/stimulus_generator.rb +124 -0
  26. data/lib/tailmix/dev/tools.rb +30 -0
  27. data/lib/tailmix/dsl.rb +35 -0
  28. data/lib/tailmix/engine.rb +17 -0
  29. data/lib/tailmix/html/attributes.rb +94 -0
  30. data/lib/tailmix/html/class_list.rb +79 -0
  31. data/lib/tailmix/html/data_map.rb +97 -0
  32. data/lib/tailmix/html/selector.rb +19 -0
  33. data/lib/tailmix/html/stimulus_builder.rb +65 -0
  34. data/lib/tailmix/runtime/action.rb +51 -0
  35. data/lib/tailmix/runtime/context.rb +74 -0
  36. data/lib/tailmix/runtime/facade_builder.rb +23 -0
  37. data/lib/tailmix/runtime/stimulus/compiler.rb +59 -0
  38. data/lib/tailmix/runtime.rb +14 -0
  39. data/lib/tailmix/version.rb +1 -1
  40. data/lib/tailmix.rb +18 -23
  41. metadata +37 -12
  42. data/examples/interactive_component.rb +0 -42
  43. data/examples/status_badge_component.rb +0 -44
  44. data/lib/tailmix/action.rb +0 -27
  45. data/lib/tailmix/dimension.rb +0 -18
  46. data/lib/tailmix/element.rb +0 -24
  47. data/lib/tailmix/manager.rb +0 -86
  48. data/lib/tailmix/part.rb +0 -39
  49. data/lib/tailmix/resolver.rb +0 -28
  50. data/lib/tailmix/schema.rb +0 -41
  51. data/lib/tailmix/utils.rb +0 -15
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Runtime
5
+ class FacadeBuilder
6
+ def self.build(definition)
7
+ Class.new(Tailmix::Runtime::Context) do
8
+ definition.elements.each_key do |element_name|
9
+ define_method(element_name) do |runtime_dimensions = {}|
10
+ attributes_for(element_name, runtime_dimensions)
11
+ end
12
+ end
13
+
14
+ def inspect
15
+ component_name = @component_instance.class.name || "AnonymousComponent"
16
+ elements_list = @definition.elements.keys.join(", ")
17
+ "#<Tailmix::UI for #{component_name} elements=[#{elements_list}] dimensions=#{@dimensions.inspect}>"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Runtime
5
+ module Stimulus
6
+ class Compiler
7
+
8
+ def self.call(definition:, data_map:, root_definition:, component:)
9
+ (definition.definitions || []).each do |rule|
10
+ builder = data_map.stimulus
11
+
12
+ case rule[:type]
13
+ when :controller
14
+ builder.controller(rule[:name])
15
+ when :action
16
+ action_data = rule[:data]
17
+ controller_name = rule[:controller]
18
+
19
+ action_string = case action_data[:type]
20
+ when :raw
21
+ action_data[:content]
22
+ when :hash
23
+ action_data[:content].map { |event, method| "#{event}->#{controller_name}##{method}" }.join(" ")
24
+ when :tuple
25
+ event, method = action_data[:content]
26
+ "#{event}->#{controller_name}##{method}"
27
+ end
28
+
29
+ builder.context(controller_name).action(action_string)
30
+ when :target
31
+ builder.context(rule[:controller]).target(rule[:name])
32
+ when :value
33
+ source = rule[:source]
34
+
35
+ resolved_value = case source[:type]
36
+ when :literal
37
+ source[:content]
38
+ when :proc
39
+ source[:content].call
40
+ when :method
41
+ component.public_send(source[:content])
42
+ else
43
+ # type code here
44
+ end
45
+
46
+ builder.context(rule[:controller]).value(rule[:name], resolved_value)
47
+
48
+ when :param
49
+ builder.context(rule[:controller]).param(rule[:params])
50
+ when :action_payload
51
+ action = root_definition.actions.fetch(rule[:action_name])
52
+ builder.context(rule[:controller]).value(rule[:value_name], action.to_h)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require_relative "runtime/context"
5
+ require_relative "runtime/facade_builder"
6
+ require_relative "runtime/stimulus/compiler"
7
+ require_relative "html/attributes"
8
+ require_relative "runtime/action"
9
+
10
+
11
+ module Tailmix
12
+ module Runtime
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tailmix
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.6"
5
5
  end
data/lib/tailmix.rb CHANGED
@@ -1,38 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "tailmix/version"
4
- require_relative "tailmix/schema"
5
- require_relative "tailmix/resolver"
6
- require_relative "tailmix/manager"
7
- require_relative "tailmix/part"
8
- require_relative "tailmix/dimension"
9
- require_relative "tailmix/element"
10
- require_relative "tailmix/utils"
11
- require_relative "tailmix/action"
4
+ require_relative "tailmix/configuration"
5
+ require_relative "tailmix/dsl"
6
+ require_relative "tailmix/definition"
7
+ require_relative "tailmix/runtime"
12
8
 
13
9
  module Tailmix
14
- def self.included(base)
15
- base.extend(ClassMethods)
16
- base.instance_variable_set(:@tailmix_schema, nil)
10
+ class Error < StandardError; end
17
11
 
18
- base.define_singleton_method(:tailmix_schema) do
19
- @tailmix_schema
20
- end
12
+ class << self
13
+ attr_writer :configuration
21
14
 
22
- base.define_singleton_method(:tailmix_schema=) do |value|
23
- @tailmix_schema = value
15
+ def configuration
16
+ @configuration ||= Configuration.new
24
17
  end
25
- end
26
18
 
27
- module ClassMethods
28
- def tailmix(&block)
29
- self.tailmix_schema = Schema.new(&block)
19
+ def configure
20
+ yield(configuration)
30
21
  end
31
22
  end
32
23
 
33
- private
24
+ def self.included(base)
25
+ base.extend(DSL)
26
+ end
34
27
 
35
- def tailmix(options = {})
36
- Manager.new(self.class.tailmix_schema, options)
28
+ def tailmix(dimensions = {})
29
+ self.class.tailmix_facade_class.new(self, self.class.tailmix_definition, dimensions)
37
30
  end
38
31
  end
32
+
33
+ require_relative "tailmix/engine" if defined?(Rails)
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.2.0
4
+ version: 0.4.6
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-14 00:00:00.000000000 Z
11
+ date: 2025-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -68,17 +68,42 @@ files:
68
68
  - LICENSE.txt
69
69
  - README.md
70
70
  - Rakefile
71
- - examples/interactive_component.rb
72
- - examples/status_badge_component.rb
71
+ - app/javascript/tailmix/finder.js
72
+ - app/javascript/tailmix/index.js
73
+ - app/javascript/tailmix/mutator.js
74
+ - app/javascript/tailmix/runner.js
75
+ - app/javascript/tailmix/stimulus_adapter.js
76
+ - examples/_modal_component.arb
77
+ - examples/modal_component.rb
78
+ - lib/generators/tailmix/install_generator.rb
73
79
  - lib/tailmix.rb
74
- - lib/tailmix/action.rb
75
- - lib/tailmix/dimension.rb
76
- - lib/tailmix/element.rb
77
- - lib/tailmix/manager.rb
78
- - lib/tailmix/part.rb
79
- - lib/tailmix/resolver.rb
80
- - lib/tailmix/schema.rb
81
- - lib/tailmix/utils.rb
80
+ - lib/tailmix/configuration.rb
81
+ - lib/tailmix/definition.rb
82
+ - lib/tailmix/definition/context_builder.rb
83
+ - lib/tailmix/definition/contexts/action_builder.rb
84
+ - lib/tailmix/definition/contexts/actions/element_builder.rb
85
+ - lib/tailmix/definition/contexts/attribute_builder.rb
86
+ - lib/tailmix/definition/contexts/dimension_builder.rb
87
+ - lib/tailmix/definition/contexts/element_builder.rb
88
+ - lib/tailmix/definition/contexts/stimulus_builder.rb
89
+ - lib/tailmix/definition/contexts/variant_builder.rb
90
+ - lib/tailmix/definition/merger.rb
91
+ - lib/tailmix/definition/result.rb
92
+ - lib/tailmix/dev/docs.rb
93
+ - lib/tailmix/dev/stimulus_generator.rb
94
+ - lib/tailmix/dev/tools.rb
95
+ - lib/tailmix/dsl.rb
96
+ - lib/tailmix/engine.rb
97
+ - lib/tailmix/html/attributes.rb
98
+ - lib/tailmix/html/class_list.rb
99
+ - lib/tailmix/html/data_map.rb
100
+ - lib/tailmix/html/selector.rb
101
+ - lib/tailmix/html/stimulus_builder.rb
102
+ - lib/tailmix/runtime.rb
103
+ - lib/tailmix/runtime/action.rb
104
+ - lib/tailmix/runtime/context.rb
105
+ - lib/tailmix/runtime/facade_builder.rb
106
+ - lib/tailmix/runtime/stimulus/compiler.rb
82
107
  - lib/tailmix/version.rb
83
108
  - sig/tailmix.rbs
84
109
  homepage: https://github.com/alexander-s-f/tailmix
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../lib/tailmix"
4
-
5
- class InteractiveComponent
6
- include Tailmix
7
-
8
- tailmix do
9
- element :container, "p-4 rounded-md"
10
- element :label, "font-bold"
11
-
12
- action :highlight, behavior: :toggle do
13
- element :container, "ring-2 ring-blue-500 bg-blue-50"
14
- element :label, "text-blue-700"
15
- end
16
- end
17
-
18
- attr_reader :classes
19
-
20
- def initialize
21
- @classes = tailmix
22
- end
23
-
24
- def toggle_highlight
25
- @classes.actions.highlight.apply!
26
- end
27
-
28
- def render
29
- # ...
30
- end
31
- end
32
-
33
- component = InteractiveComponent.new
34
- puts "Initial container: '#{component.classes.container}'"
35
- # => Initial container: 'p-4 rounded-md'
36
-
37
- component.toggle_highlight
38
- puts "After highlight: '#{component.classes.container}'"
39
- # => After highlight: 'p-4 rounded-md ring-2 ring-blue-500 bg-blue-50'
40
-
41
- puts "JSON Recipe: #{component.classes.actions.highlight.to_json}"
42
- # => JSON Recipe: {"behavior":"toggle","classes":{"container":"ring-2...","label":"text-blue-700"}}
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../lib/tailmix"
4
-
5
- class StatusBadgeComponent
6
- include Tailmix
7
-
8
- tailmix do
9
- element :badge, "inline-flex items-center font-medium px-2.5 py-0.5 rounded-full" do
10
- size do
11
- option :sm, "text-xs", default: true
12
- option :lg, "text-base"
13
- end
14
- status do
15
- option :success, "bg-green-100 text-green-800", default: true
16
- option :warning, "bg-yellow-100 text-yellow-800"
17
- option :error, "bg-red-100 text-red-800"
18
- end
19
- end
20
- end
21
-
22
- attr_reader :classes
23
-
24
- def initialize(status: :success, size: :sm)
25
- @classes = tailmix(status: status, size: size)
26
- end
27
-
28
- def highlight!
29
- @classes.badge.add("ring-2 ring-offset-2 ring-blue-500")
30
- end
31
-
32
- def render
33
- "<span class='#{@classes.badge}'>Status</span>"
34
- end
35
- end
36
-
37
- badge1 = StatusBadgeComponent.new(status: :error, size: :lg)
38
- puts "Error badge: #{badge1.render}"
39
- # => Error badge: <span class='inline-flex ... text-base bg-red-100 text-red-800'>Status</span>
40
-
41
- badge2 = StatusBadgeComponent.new(status: :success)
42
- badge2.highlight!
43
- puts "Success badge (highlighted): #{badge2.render}"
44
- # => Success badge (highlighted): <span class='inline-flex ... text-xs bg-green-100 text-green-800 ring-2 ring-offset-2 ring-blue-500'>Status</span>
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Tailmix
6
- class Action
7
- def initialize(manager, behavior:, classes_by_part:)
8
- @manager = manager
9
- @behavior = behavior
10
- @classes_by_part = classes_by_part
11
- end
12
-
13
- def apply!
14
- @classes_by_part.each do |part_name, classes|
15
- part_object = @manager.public_send(part_name)
16
- part_object.public_send(@behavior, classes)
17
- end
18
- end
19
-
20
- def to_json(*_args)
21
- {
22
- behavior: @behavior,
23
- classes: @classes_by_part
24
- }.to_json
25
- end
26
- end
27
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tailmix
4
- class Dimension
5
- attr_reader :options, :default_option
6
-
7
- def initialize(&block)
8
- @options = {}
9
- @default_option = nil
10
- instance_eval(&block) if block_given?
11
- end
12
-
13
- def option(name, classes, default: false)
14
- @options[name.to_sym] = classes
15
- @default_option = name.to_sym if default
16
- end
17
- end
18
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "dimension"
4
-
5
- module Tailmix
6
- class Element
7
- attr_reader :base_classes, :dimensions
8
-
9
- def initialize(base_classes, &block)
10
- @base_classes = base_classes
11
- @dimensions = {}
12
- instance_eval(&block) if block_given?
13
- end
14
-
15
- def method_missing(method_name, &block)
16
- dimension_name = method_name.to_sym
17
- @dimensions[dimension_name] = Dimension.new(&block)
18
- end
19
-
20
- def respond_to_missing?(method_name, include_private = false)
21
- true
22
- end
23
- end
24
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "resolver"
4
- require_relative "part"
5
- require_relative "utils"
6
- require_relative "action"
7
-
8
- module Tailmix
9
- class Manager
10
- def initialize(schema, initial_variants = {})
11
- @schema = schema
12
- @current_variants = {}
13
- @part_objects = {}
14
-
15
- defaults = get_defaults_from_schema
16
- combine(Utils.deep_merge(defaults, initial_variants))
17
- end
18
-
19
- def combine(variants_to_apply = {})
20
- @current_variants = Utils.deep_merge(@current_variants, variants_to_apply)
21
- rebuild_parts!
22
- self
23
- end
24
-
25
- def method_missing(method_name, *args, &block)
26
- part_name = method_name.to_sym
27
- if @part_objects.key?(part_name)
28
- @part_objects[part_name]
29
- else
30
- super
31
- end
32
- end
33
-
34
- def respond_to_missing?(method_name, include_private = false)
35
- @part_objects.key?(method_name.to_sym) || super
36
- end
37
-
38
- def actions
39
- @action_proxy ||= ActionProxy.new(self, @schema)
40
- end
41
-
42
- class ActionProxy
43
- def initialize(manager, schema)
44
- @manager = manager
45
- @schema = schema
46
- end
47
-
48
- def method_missing(method_name, *args, &block)
49
- action_name = method_name.to_sym
50
-
51
- if @schema.actions.key?(action_name)
52
- action_definition = @schema.actions[action_name]
53
-
54
- Action.new(@manager, **action_definition)
55
- else
56
- super
57
- end
58
- end
59
-
60
- def respond_to_missing?(method_name, include_private = false)
61
- @schema.actions.key?(method_name.to_sym) || super
62
- end
63
- end
64
-
65
- private
66
-
67
- def rebuild_parts!
68
- resolved_struct = Resolver.call(@schema, @current_variants)
69
- @part_objects = {}
70
-
71
- resolved_struct.to_h.each do |part_name, class_string|
72
- @part_objects[part_name] = Part.new(class_string || "")
73
- end
74
- end
75
-
76
- def get_defaults_from_schema
77
- defaults = {}
78
- @schema.elements.each_value do |element|
79
- element.dimensions.each do |dim_name, dim|
80
- defaults[dim_name] = dim.default_option if dim.default_option
81
- end
82
- end
83
- defaults
84
- end
85
- end
86
- end
data/lib/tailmix/part.rb DELETED
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
-
5
- module Tailmix
6
- class Part
7
- def initialize(class_string)
8
- @classes = Set.new(class_string.to_s.split)
9
- end
10
-
11
- def add(*new_classes)
12
- @classes.merge(process_args(new_classes))
13
- self
14
- end
15
-
16
- def remove(*classes_to_remove)
17
- @classes.subtract(process_args(classes_to_remove))
18
- self
19
- end
20
-
21
- def toggle(*classes_to_toggle)
22
- process_args(classes_to_toggle).each do |cls|
23
- @classes.delete?(cls) || @classes.add(cls)
24
- end
25
- self
26
- end
27
-
28
- def to_s
29
- @classes.to_a.join(" ")
30
- end
31
- alias to_str to_s
32
-
33
- private
34
-
35
- def process_args(args)
36
- args.flat_map { |arg| arg.to_s.split }
37
- end
38
- end
39
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ostruct"
4
-
5
- module Tailmix
6
- module Resolver
7
- def self.call(schema, active_variants = {})
8
- resolved_parts = schema.elements.each_with_object({}) do |(element_name, element), result|
9
- class_list = []
10
-
11
- class_list << element.base_classes
12
-
13
- element.dimensions.each do |dimension_name, dimension|
14
- active_option = active_variants[dimension_name] || dimension.default_option
15
-
16
- if active_option
17
- variant_class = dimension.options[active_option]
18
- class_list << variant_class
19
- end
20
- end
21
-
22
- result[element_name] = class_list.compact.join(" ")
23
- end
24
-
25
- OpenStruct.new(resolved_parts)
26
- end
27
- end
28
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "element"
4
-
5
- module Tailmix
6
- class Schema
7
- attr_reader :elements, :actions
8
-
9
- def initialize(&block)
10
- @elements = {}
11
- @actions = {}
12
- instance_eval(&block) if block_given?
13
- end
14
-
15
- def element(name, base_classes, &block)
16
- @elements[name.to_sym] = Element.new(base_classes, &block)
17
- end
18
-
19
- def action(name, behavior: :toggle, &block)
20
- builder = ActionBuilder.new
21
- builder.instance_eval(&block)
22
-
23
- @actions[name.to_sym] = {
24
- behavior: behavior,
25
- classes_by_part: builder.classes_by_part
26
- }
27
- end
28
-
29
- class ActionBuilder
30
- attr_reader :classes_by_part
31
-
32
- def initialize
33
- @classes_by_part = {}
34
- end
35
-
36
- def element(name, classes)
37
- @classes_by_part[name.to_sym] = classes
38
- end
39
- end
40
- end
41
- end
data/lib/tailmix/utils.rb DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tailmix
4
- module Utils
5
- def self.deep_merge(original_hash, other_hash)
6
- other_hash.each_with_object(original_hash.dup) do |(key, value), result|
7
- if value.is_a?(Hash) && result[key].is_a?(Hash)
8
- result[key] = deep_merge(result[key], value)
9
- else
10
- result[key] = value
11
- end
12
- end
13
- end
14
- end
15
- end