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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Tailmix
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ def add_javascript
9
+ say "Pinning Tailmix JavaScript", :green
10
+ append_to_file "config/importmap.rb", <<~RUBY
11
+ pin "tailmix", to: "tailmix/index.js"
12
+ RUBY
13
+
14
+ say "Adding Tailmix to asset manifest", :green
15
+ append_to_file "app/assets/config/manifest.js", "\n//= link tailmix/index.js.js\n"
16
+ end
17
+ end
18
+ end
19
+ 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
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "contexts/action_builder"
4
+ require_relative "contexts/element_builder"
5
+
6
+ module Tailmix
7
+ module Definition
8
+ class ContextBuilder
9
+ attr_reader :elements, :actions
10
+
11
+ def initialize
12
+ @elements = {}
13
+ @actions = {}
14
+ end
15
+
16
+ def element(name, base_classes = "", &block)
17
+ builder = Contexts::ElementBuilder.new(name)
18
+ builder.attributes.classes(base_classes.split)
19
+
20
+ builder.instance_eval(&block) if block
21
+
22
+ @elements[name.to_sym] = builder
23
+ end
24
+
25
+ def action(name, method:, &block)
26
+ builder = Contexts::ActionBuilder.new(method)
27
+ builder.instance_eval(&block) if block
28
+ @actions[name.to_sym] = builder
29
+ end
30
+
31
+ def build_definition
32
+ Definition::Result::Context.new(
33
+ elements: @elements.transform_values(&:build_definition).freeze,
34
+ actions: @actions.transform_values(&:build_definition).freeze
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "actions/element_builder"
4
+
5
+ module Tailmix
6
+ module Definition
7
+ module Contexts
8
+ class ActionBuilder
9
+ def initialize(method)
10
+ @method = method
11
+ @mutations = {}
12
+ end
13
+
14
+ def element(name, &block)
15
+ builder = Actions::ElementBuilder.new(@method)
16
+ builder.instance_eval(&block)
17
+
18
+ commands = builder.build_commands
19
+ @mutations[name.to_sym] = commands unless commands.empty?
20
+ end
21
+
22
+ def build_definition
23
+ Definition::Result::Action.new(
24
+ action: @method,
25
+ mutations: @mutations.freeze
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Contexts
6
+ module Actions
7
+ class ElementBuilder
8
+ def initialize(default_method)
9
+ @default_method = default_method
10
+ @commands = []
11
+ end
12
+
13
+ def classes(classes_string, options = {})
14
+ method = options.fetch(:method, @default_method)
15
+ @commands << { field: :classes, method: method, payload: classes_string }
16
+ end
17
+
18
+ def data(data_hash)
19
+ operation = data_hash.delete(:method) || @default_method
20
+ @commands << { field: :data, method: operation, payload: data_hash }
21
+ end
22
+
23
+ def build_commands
24
+ @commands
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Contexts
6
+ class AttributeBuilder
7
+ def initialize
8
+ @classes = []
9
+ end
10
+
11
+ def classes(*list)
12
+ @classes.concat(list.flatten.map(&:to_s))
13
+ end
14
+
15
+ def build_definition
16
+ Definition::Result::Attributes.new(classes: @classes.freeze)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "variant_builder"
4
+
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
24
+
25
+ def build_dimension
26
+ {
27
+ default: @default,
28
+ variants: @variants.freeze
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute_builder"
4
+ require_relative "stimulus_builder"
5
+ require_relative "dimension_builder"
6
+
7
+ module Tailmix
8
+ module Definition
9
+ module Contexts
10
+ class ElementBuilder
11
+ def initialize(name)
12
+ @name = name
13
+ @dimensions = {}
14
+ end
15
+
16
+ def attributes
17
+ @attributes_builder ||= AttributeBuilder.new
18
+ end
19
+
20
+ def stimulus
21
+ @stimulus_builder ||= StimulusBuilder.new
22
+ end
23
+
24
+ def dimension(name, default: nil, &block)
25
+ builder = Contexts::DimensionBuilder.new(default: default)
26
+ builder.instance_eval(&block)
27
+ @dimensions[name.to_sym] = builder.build_dimension
28
+ end
29
+
30
+ def build_definition
31
+ Definition::Result::Element.new(
32
+ name: @name,
33
+ attributes: attributes.build_definition,
34
+ stimulus: stimulus.build_definition,
35
+ dimensions: @dimensions.freeze
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Contexts
6
+ class StimulusBuilder
7
+ attr_reader :definitions
8
+
9
+ def initialize
10
+ @definitions = []
11
+ @current_context = nil
12
+ end
13
+
14
+ def controller(name)
15
+ @definitions << { type: :controller, name: name }
16
+ context(name)
17
+ end
18
+ alias_method :ctr, :controller
19
+
20
+ def context(name)
21
+ @current_context = name.to_s
22
+ self
23
+ end
24
+ alias_method :ctx, :context
25
+
26
+ def target(target_name)
27
+ raise "A controller context must be set..." unless @current_context
28
+ @definitions << { type: :target, controller: @current_context, name: target_name }
29
+ self
30
+ end
31
+
32
+ def action(*args)
33
+ ensure_context!
34
+
35
+ action_data = case args.first
36
+ when String
37
+ { type: :raw, content: args.first }
38
+ when Hash
39
+ { type: :hash, content: args.first }
40
+ else
41
+ { type: :tuple, content: args }
42
+ end
43
+
44
+ @definitions << { type: :action, controller: @current_context, data: action_data }
45
+ self
46
+ end
47
+
48
+ def value(value_name, value: nil, call: nil, method: nil)
49
+ ensure_context!
50
+
51
+ source = if !value.nil?
52
+ { type: :literal, content: value }
53
+ elsif call.is_a?(Proc)
54
+ { type: :proc, content: call }
55
+ elsif method.is_a?(Symbol) || method.is_a?(String)
56
+ { type: :method, content: method.to_sym }
57
+ else
58
+ raise ArgumentError, "You must provide one of value:, call:, or method: keyword arguments."
59
+ end
60
+
61
+ @definitions << {
62
+ type: :value,
63
+ controller: @current_context,
64
+ name: value_name,
65
+ source: source
66
+ }
67
+ self
68
+ end
69
+
70
+ def action_payload(action_name, as: nil)
71
+ ensure_context!
72
+ value_name = as || "#{action_name}_action"
73
+
74
+ @definitions << {
75
+ type: :action_payload,
76
+ controller: @current_context,
77
+ action_name: action_name.to_sym,
78
+ value_name: value_name.to_sym
79
+ }
80
+ self
81
+ end
82
+
83
+ def param(params_hash)
84
+ ensure_context!
85
+ @definitions << { type: :param, controller: @current_context, params: params_hash }
86
+ self
87
+ end
88
+
89
+ def build_definition
90
+ Definition::Result::Stimulus.new(definitions: @definitions.freeze)
91
+ end
92
+
93
+ private
94
+
95
+ def ensure_context!
96
+ raise "A controller context must be set via .controller() or .context() before this call." unless @current_context
97
+ end
98
+ end
99
+ end
100
+ end
101
+ 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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ # A service object responsible for deep-merging two tailmix definitions.
6
+ class Merger
7
+ def self.call(parent_def, child_def)
8
+ new(parent_def, child_def).merge
9
+ end
10
+
11
+ def initialize(parent_def, child_def)
12
+ @parent_def = parent_def
13
+ @child_def = child_def
14
+ end
15
+
16
+ def merge
17
+ Result::Context.new(
18
+ elements: merged_elements,
19
+ actions: merged_actions
20
+ )
21
+ end
22
+
23
+ private
24
+
25
+ def merged_actions
26
+ # For actions, child definitions completely override parent definitions.
27
+ @parent_def.actions.merge(@child_def.actions)
28
+ end
29
+
30
+ def merged_elements
31
+ all_element_keys = (@parent_def.elements.keys | @child_def.elements.keys)
32
+
33
+ all_element_keys.each_with_object({}) do |key, h|
34
+ parent_element = @parent_def.elements[key]
35
+ child_element = @child_def.elements[key]
36
+
37
+ h[key] = if parent_element && child_element
38
+ merge_element(parent_element, child_element)
39
+ else
40
+ child_element || parent_element
41
+ end
42
+ end
43
+ end
44
+
45
+ def merge_element(parent_el, child_el)
46
+ Result::Element.new(
47
+ name: parent_el.name,
48
+ attributes: merge_attributes(parent_el.attributes, child_el.attributes),
49
+ dimensions: merge_dimensions(parent_el.dimensions, child_el.dimensions),
50
+ stimulus: merge_stimulus(parent_el.stimulus, child_el.stimulus)
51
+ )
52
+ end
53
+
54
+ def merge_attributes(parent_attrs, child_attrs)
55
+ # Combine base classes, ensuring no duplicates.
56
+ combined_classes = (parent_attrs.classes + child_attrs.classes).uniq
57
+ Result::Attributes.new(classes: combined_classes)
58
+ end
59
+
60
+ def merge_stimulus(parent_stimulus, child_stimulus)
61
+ # Combine stimulus definitions.
62
+ combined_definitions = parent_stimulus.definitions + child_stimulus.definitions
63
+ Result::Stimulus.new(definitions: combined_definitions)
64
+ end
65
+
66
+ def merge_dimensions(parent_dims, child_dims)
67
+ all_keys = parent_dims.keys | child_dims.keys
68
+
69
+ all_keys.each_with_object({}) do |key, merged|
70
+ parent_val = parent_dims[key]
71
+ child_val = child_dims[key]
72
+
73
+ if parent_val && child_val
74
+ merged_variants = parent_val.fetch(:variants, {}).merge(child_val.fetch(:variants, {}))
75
+
76
+ default = child_val.key?(:default) ? child_val[:default] : parent_val[:default]
77
+
78
+ merged[key] = { default: default, variants: merged_variants }
79
+ else
80
+ merged[key] = parent_val || child_val
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Result
6
+ Context = Struct.new(:elements, :actions, keyword_init: true) do
7
+ def to_h
8
+ {
9
+ elements: elements.transform_values(&:to_h),
10
+ actions: actions.transform_values(&:to_h)
11
+ }
12
+ end
13
+ end
14
+
15
+ Element = Struct.new(:name, :attributes, :dimensions, :stimulus, keyword_init: true) do
16
+ def to_h
17
+ {
18
+ name: name,
19
+ attributes: attributes.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
+ }
34
+ end
35
+ end
36
+
37
+ Variant = Struct.new(:class_groups, :data, :aria, keyword_init: true) do
38
+ def classes
39
+ class_groups.flat_map { |group| group[:classes] }
40
+ end
41
+
42
+ def to_h
43
+ {
44
+ classes: classes,
45
+ class_groups: class_groups,
46
+ data: data,
47
+ aria: aria
48
+ }
49
+ end
50
+ end
51
+
52
+ Attributes = Struct.new(:classes, keyword_init: true) do
53
+ def to_h
54
+ {
55
+ classes: classes
56
+ }
57
+ end
58
+ end
59
+
60
+ Stimulus = Struct.new(:definitions, keyword_init: true) do
61
+ def to_h
62
+ {
63
+ definitions: definitions
64
+ }
65
+ end
66
+ end
67
+
68
+ Action = Struct.new(:action, :mutations, keyword_init: true) do
69
+ def to_h
70
+ {
71
+ action: action,
72
+ mutations: mutations
73
+ }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "definition/result"
4
+ require_relative "definition/merger"
5
+ require_relative "definition/context_builder"
6
+
7
+ module Tailmix
8
+ module Definition
9
+
10
+ end
11
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Dev
5
+ class Docs
6
+
7
+ def initialize(tools)
8
+ @tools = tools
9
+ @definition = tools.definition
10
+ @component_class_name = tools.component_class
11
+ end
12
+
13
+ def generate
14
+ output = ["== Tailmix Docs for #{@component_class_name} =="]
15
+
16
+ signature = generate_signature
17
+ output << "Signature: `initialize(#{signature})`" unless signature.empty?
18
+ output << ""
19
+
20
+ output << generate_dimensions_docs
21
+ output << ""
22
+ output << generate_actions_docs
23
+ output << ""
24
+ output << generate_stimulus_docs
25
+
26
+ output.join("\n")
27
+ end
28
+
29
+ private
30
+
31
+ def generate_signature
32
+ all_dimensions
33
+ .map { |name, config| "#{name}: #{config[:default].inspect}" if config.key?(:default) }
34
+ .compact
35
+ .join(", ")
36
+ end
37
+
38
+ def generate_dimensions_docs
39
+ output = []
40
+
41
+ if all_dimensions.any?
42
+ output << "Dimensions:"
43
+ all_dimensions.each do |dim_name, config|
44
+ default_info = config[:default] ? "(default: #{config[:default].inspect})" : ""
45
+ output << " - #{dim_name} #{default_info}"
46
+ config[:variants].each do |variant_name, variant_def|
47
+ output << " - #{variant_name.inspect}:"
48
+ variant_def.class_groups.each do |group|
49
+ label = group[:options][:group] ? "(group: :#{group[:options][:group]})" : ""
50
+ output << " - classes #{label}: \"#{group[:classes].join(' ')}\""
51
+ end
52
+ output << " - data: #{variant_def.data.inspect}" if variant_def.data.any?
53
+ output << " - aria: #{variant_def.aria.inspect}" if variant_def.aria.any?
54
+ end
55
+ end
56
+ else
57
+ output << "No dimensions defined."
58
+ end
59
+
60
+ output.join("\n")
61
+ end
62
+
63
+ def generate_actions_docs
64
+ output = []
65
+ actions = @definition.actions
66
+
67
+ if actions.any?
68
+ output << "Actions:"
69
+ actions.keys.each do |action_name|
70
+ output << " - :#{action_name}"
71
+ end
72
+ else
73
+ output << "No actions defined."
74
+ end
75
+
76
+ output.join("\n")
77
+ end
78
+
79
+ def generate_stimulus_docs
80
+ @tools.stimulus.scaffold(show_docs: true)
81
+ end
82
+
83
+ def all_dimensions
84
+ @_all_dimensions ||= @definition.elements.values.flat_map(&:dimensions).reduce({}, :merge)
85
+ end
86
+ end
87
+ end
88
+ end