tailmix 0.4.7 → 0.4.8

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +46 -205
  4. data/app/javascript/tailmix/runtime/action_dispatcher.js +132 -0
  5. data/app/javascript/tailmix/runtime/component.js +130 -0
  6. data/app/javascript/tailmix/runtime/index.js +130 -0
  7. data/app/javascript/tailmix/runtime/plugins.js +35 -0
  8. data/app/javascript/tailmix/runtime/updater.js +140 -0
  9. data/docs/01_getting_started.md +0 -81
  10. data/examples/_modal_component.arb +17 -25
  11. data/examples/modal_component.rb +31 -155
  12. data/lib/tailmix/definition/builders/action_builder.rb +39 -0
  13. data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
  14. data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
  15. data/lib/tailmix/definition/builders/component_builder.rb +81 -0
  16. data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
  17. data/lib/tailmix/definition/builders/element_builder.rb +83 -0
  18. data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
  19. data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
  20. data/lib/tailmix/definition/builders/state_builder.rb +21 -0
  21. data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
  22. data/lib/tailmix/definition/context_builder.rb +17 -12
  23. data/lib/tailmix/definition/payload_proxy.rb +16 -0
  24. data/lib/tailmix/definition/result.rb +17 -19
  25. data/lib/tailmix/dev/docs.rb +3 -9
  26. data/lib/tailmix/dev/tools.rb +0 -5
  27. data/lib/tailmix/dsl.rb +3 -5
  28. data/lib/tailmix/engine.rb +11 -1
  29. data/lib/tailmix/html/attributes.rb +22 -12
  30. data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
  31. data/lib/tailmix/registry.rb +37 -0
  32. data/lib/tailmix/runtime/action_proxy.rb +29 -0
  33. data/lib/tailmix/runtime/attribute_builder.rb +102 -0
  34. data/lib/tailmix/runtime/attribute_cache.rb +23 -0
  35. data/lib/tailmix/runtime/context.rb +51 -62
  36. data/lib/tailmix/runtime/facade_builder.rb +8 -5
  37. data/lib/tailmix/runtime/state.rb +36 -0
  38. data/lib/tailmix/runtime/state_proxy.rb +34 -0
  39. data/lib/tailmix/runtime.rb +0 -1
  40. data/lib/tailmix/version.rb +1 -1
  41. data/lib/tailmix/view_helpers.rb +49 -0
  42. data/lib/tailmix.rb +4 -2
  43. metadata +26 -20
  44. data/app/javascript/tailmix/finder.js +0 -15
  45. data/app/javascript/tailmix/index.js +0 -7
  46. data/app/javascript/tailmix/mutator.js +0 -28
  47. data/app/javascript/tailmix/runner.js +0 -7
  48. data/app/javascript/tailmix/stimulus_adapter.js +0 -37
  49. data/docs/02_dsl_reference.md +0 -266
  50. data/docs/03_advanced_usage.md +0 -88
  51. data/docs/04_client_side_bridge.md +0 -119
  52. data/docs/05_cookbook.md +0 -249
  53. data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
  54. data/lib/tailmix/definition/contexts/element_builder.rb +0 -55
  55. data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
  56. data/lib/tailmix/dev/stimulus_generator.rb +0 -124
  57. data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
data/docs/05_cookbook.md DELETED
@@ -1,249 +0,0 @@
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
- ```
@@ -1,31 +0,0 @@
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
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "attribute_builder"
4
- require_relative "stimulus_builder"
5
- require_relative "dimension_builder"
6
- require_relative "variant_builder"
7
-
8
- module Tailmix
9
- module Definition
10
- module Contexts
11
- class ElementBuilder
12
- def initialize(name)
13
- @name = name
14
- @dimensions = {}
15
- @compound_variants = []
16
- end
17
-
18
- def attributes
19
- @attributes_builder ||= AttributeBuilder.new
20
- end
21
-
22
- def stimulus
23
- @stimulus_builder ||= StimulusBuilder.new
24
- end
25
-
26
- def dimension(name, default: nil, &block)
27
- builder = Contexts::DimensionBuilder.new(default: default)
28
- builder.instance_eval(&block)
29
- @dimensions[name.to_sym] = builder.build_dimension
30
- end
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
-
43
- def build_definition
44
- Definition::Result::Element.new(
45
- name: @name,
46
- attributes: attributes.build_definition,
47
- stimulus: stimulus.build_definition,
48
- dimensions: @dimensions.freeze,
49
- compound_variants: @compound_variants.freeze
50
- )
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,101 +0,0 @@
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
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tailmix
4
- module Dev
5
- class StimulusGenerator
6
- def initialize(definition, component_name)
7
- @definition = definition
8
- @component_name = component_name
9
- @stimulus_defs = definition.elements.values.flat_map(&:stimulus).flat_map(&:definitions)
10
- end
11
-
12
- def scaffold(controller_name = nil, show_docs: false)
13
- controllers_to_generate = controller_name ? [ controller_name.to_s ] : all_controllers
14
-
15
- output = controllers_to_generate.map do |name|
16
- defs = @stimulus_defs.select { |d| d[:controller] == name }
17
-
18
- show_docs ? generate_docs_for(name, defs) + "\n" : generate_js_for(name, defs) + ("-" * 60) + "\n"
19
- end
20
- output.join("\n")
21
- end
22
-
23
- private
24
-
25
- def generate_docs_for(controller_name, defs)
26
- output = [ "Stimulus:" ]
27
- output << " - on `#{controller_name}` controller:"
28
- targets = defs.select { |d| d[:type] == :target }.map { |d| d[:name] }
29
- output << " - Targets: #{targets.join(', ')}" if targets.any?
30
-
31
- actions = action_methods(defs)
32
- output << " - Actions: #{actions.join(', ')}" if actions.any?
33
-
34
- values = defs.select { |d| d[:type] == :value }.map { |d| d[:name] }
35
- output << " - Values: #{values.join(', ')}" if values.any?
36
-
37
- output.join("\n")
38
- end
39
-
40
- def generate_js_for(controller_name, defs)
41
- targets = defs.select { |d| d[:type] == :target }.map { |d| "'#{d[:name]}'" }.uniq.join(", ")
42
-
43
- payload_actions = defs.select { |d| d[:type] == :action_payload }
44
- simple_values = defs.select { |d| d[:type] == :value }
45
-
46
- value_names = (payload_actions.map { |d| d[:value_name] } + simple_values.map { |d| d[:name] })
47
- .uniq.map { |name| "#{snake_to_camel(name.to_s)}: Object" }.join(", ")
48
-
49
- isomorphic_methods = payload_actions.map do |payload_def|
50
- action_name_camel = snake_to_camel(payload_def[:action_name].to_s)
51
- value_name_camel = snake_to_camel(payload_def[:value_name].to_s)
52
-
53
- " #{action_name_camel}(event) {\n if (event) event.preventDefault();\n Tailmix.run({ config: this.#{value_name_camel}Value, controller: this });\n }"
54
- end.join.strip
55
-
56
- standard_action_names = defs.select { |d| d[:type] == :action }
57
- .flat_map { |d| extract_action_methods(d[:data]) }
58
- .uniq
59
-
60
- implemented_action_names = payload_actions.map { |d| d[:action_name].to_s }
61
- stub_methods = (standard_action_names - implemented_action_names).map do |method_name|
62
- method_name_camel = snake_to_camel(method_name)
63
- " #{method_name_camel}() {\n console.log('#{controller_name}##{method_name_camel} fired');\n }"
64
- end.join("\n\n")
65
-
66
- js_methods = [isomorphic_methods, stub_methods].reject(&:empty?).join("\n\n")
67
-
68
- <<~JAVASCRIPT
69
- // Generated by Tailmix for the "#{controller_name}" controller
70
- // Path: app/javascript/controllers/#{controller_name.tr('_', '-')}_controller.js
71
- import { Controller } from "@hotwired/stimulus"
72
- import Tailmix from "tailmix"
73
-
74
- export default class extends Controller {
75
- static targets = [#{targets}]
76
- static values = { #{value_names} }
77
-
78
- connect() {
79
- console.log("#{controller_name} controller connected to", this.element);
80
- }
81
- #{js_methods}
82
- }
83
- JAVASCRIPT
84
- end
85
-
86
- def extract_action_methods(action_data)
87
- case action_data[:type]
88
- when :raw
89
- action_data[:content].to_s.scan(/#(\w+)/).flatten
90
- when :hash
91
- action_data[:content].values.map(&:to_s)
92
- when :tuple
93
- [action_data[:content][1].to_s]
94
- else
95
- []
96
- end
97
- end
98
-
99
- def all_controllers
100
- @stimulus_defs.map { |d| d[:controller] }.compact.uniq
101
- end
102
-
103
- def action_methods(defs)
104
- defs.select { |d| d[:type] == :action }.flat_map do |action_definition|
105
- data = action_definition[:data]
106
- case data[:type]
107
- when :raw
108
- data[:content].to_s.scan(/#(\w+)/).flatten
109
- when :hash
110
- data[:content].values.map(&:to_s)
111
- when :tuple
112
- [ data[:content][1].to_s ]
113
- else
114
- []
115
- end
116
- end.uniq
117
- end
118
-
119
- def snake_to_camel(str)
120
- str.split("_").map.with_index { |word, i| i.zero? ? word : word.capitalize }.join
121
- end
122
- end
123
- end
124
- end
@@ -1,59 +0,0 @@
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