tailmix 0.4.6 → 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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +76 -145
  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 +39 -0
  10. data/examples/_modal_component.arb +17 -25
  11. data/examples/button.rb +81 -0
  12. data/examples/helpers.rb +25 -0
  13. data/examples/modal_component.rb +32 -164
  14. data/lib/tailmix/definition/builders/action_builder.rb +39 -0
  15. data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
  16. data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
  17. data/lib/tailmix/definition/builders/component_builder.rb +81 -0
  18. data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
  19. data/lib/tailmix/definition/builders/element_builder.rb +83 -0
  20. data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
  21. data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
  22. data/lib/tailmix/definition/builders/state_builder.rb +21 -0
  23. data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
  24. data/lib/tailmix/definition/context_builder.rb +19 -13
  25. data/lib/tailmix/definition/merger.rb +2 -1
  26. data/lib/tailmix/definition/payload_proxy.rb +16 -0
  27. data/lib/tailmix/definition/result.rb +17 -18
  28. data/lib/tailmix/dev/docs.rb +32 -7
  29. data/lib/tailmix/dev/tools.rb +0 -5
  30. data/lib/tailmix/dsl.rb +3 -5
  31. data/lib/tailmix/engine.rb +11 -1
  32. data/lib/tailmix/html/attributes.rb +22 -12
  33. data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
  34. data/lib/tailmix/registry.rb +37 -0
  35. data/lib/tailmix/runtime/action_proxy.rb +29 -0
  36. data/lib/tailmix/runtime/attribute_builder.rb +102 -0
  37. data/lib/tailmix/runtime/attribute_cache.rb +23 -0
  38. data/lib/tailmix/runtime/context.rb +51 -47
  39. data/lib/tailmix/runtime/facade_builder.rb +8 -5
  40. data/lib/tailmix/runtime/state.rb +36 -0
  41. data/lib/tailmix/runtime/state_proxy.rb +34 -0
  42. data/lib/tailmix/runtime.rb +0 -1
  43. data/lib/tailmix/version.rb +1 -1
  44. data/lib/tailmix/view_helpers.rb +49 -0
  45. data/lib/tailmix.rb +4 -2
  46. metadata +34 -21
  47. data/app/javascript/tailmix/finder.js +0 -15
  48. data/app/javascript/tailmix/index.js +0 -7
  49. data/app/javascript/tailmix/mutator.js +0 -28
  50. data/app/javascript/tailmix/runner.js +0 -7
  51. data/app/javascript/tailmix/stimulus_adapter.js +0 -37
  52. data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
  53. data/lib/tailmix/definition/contexts/element_builder.rb +0 -41
  54. data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
  55. data/lib/tailmix/dev/stimulus_generator.rb +0 -124
  56. data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
@@ -1,201 +1,69 @@
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
7
8
  attr_reader :ui
8
9
 
9
10
  tailmix do
10
- element :base, "fixed inset-0 z-50 flex items-center justify-center" do
11
- dimension :open, default: true do
12
- variant true, "visible opacity-100"
13
- variant false, "invisible opacity-0"
14
- end
15
- stimulus.controller("modal").action_payload(:toggle, as: :toggle_data)
16
- end
11
+ plugin :auto_focus, on: :open_button, delay: 100
12
+ state :open, default: false, toggle: true
17
13
 
18
- element :overlay, "fixed inset-0 bg-black/50 transition-opacity" do
19
- stimulus.context("modal").action(:click, :close)
20
- end
21
-
22
- element :panel, "relative bg-white rounded-lg shadow-xl transition-transform transform" do
23
- dimension :size, default: :md do
24
- variant :sm, "w-full max-w-sm p-4" do
25
- classes "dark:text-slate-400", group: :dark_mode
26
- classes "one two"
27
- end
28
- variant :md, "w-full max-w-md p-6"
29
- variant :lg, "w-full max-w-lg p-8"
30
- end
31
- stimulus.context("modal").target("panel")
14
+ element :container do
32
15
  end
33
16
 
34
- element :title, "text-lg font-semibold text-gray-900"
35
- element :body, "mt-2 text-sm text-gray-600"
36
- element :close_button, "absolute top-2 right-2 p-1 text-gray-400 rounded-full hover:bg-gray-100 hover:text-gray-600" do
37
- stimulus.context("modal").action(:click, :close)
17
+ element :open_button do
18
+ # We attach the `click` event to our auto-generated action.
19
+ on :click, :toggle_open
38
20
  end
39
21
 
40
- element :footer, "mt-4 pt-4 border-t flex justify-end"
41
- element :confirm_button, "relative inline-flex items-center px-4 py-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700" do
42
- stimulus.controller("form-submission")
43
- .action(:click, :submit)
44
- .action_payload(:enter_pending_state, as: :pending_data)
22
+ element :base do
23
+ dimension :open do
24
+ variant true, "fixed inset-0 z-50 flex items-center justify-center visible opacity-100 transition-opacity"
25
+ variant false, "invisible opacity-0"
26
+ end
45
27
  end
46
28
 
47
- element :spinner, "absolute inset-0 flex items-center justify-center hidden"
48
-
49
- action :toggle, method: :toggle do
50
- element :overlay do
51
- classes "hidden"
52
- end
53
- element :panel do
54
- classes "hidden"
29
+ element :overlay do
30
+ dimension :open do
31
+ variant true, "fixed inset-0 bg-black/50"
32
+ variant false, "hidden"
55
33
  end
34
+ on :click, :toggle_open
56
35
  end
57
36
 
58
- action :lock, method: :add do
59
- element :close_button do
60
- classes "hidden"
61
- end
62
- element :panel do
63
- data locked: true, reason: "processing"
37
+ element :panel, "relative bg-white rounded-lg shadow-xl" do
38
+ dimension :open do
39
+ variant true, "block"
40
+ variant false, "hidden"
64
41
  end
65
42
  end
66
43
 
67
- action :enter_pending_state, method: :add do
68
- element :confirm_button do
69
- classes "opacity-75 cursor-not-allowed"
70
- end
71
- element :spinner do
72
- classes "flex"
73
- end
44
+ element :close_button, "absolute top-2 right-2 p-1 text-gray-500 rounded-full cursor-pointer" do
45
+ on :click, :toggle_open
74
46
  end
75
- end
76
47
 
77
- def initialize(size: :md, open: false)
78
- @ui = tailmix(size: size, open: open)
48
+ element :title, "text-lg font-semibold text-gray-900 p-4 border-b"
49
+ element :body, "p-4 text-gray-900"
79
50
  end
80
51
 
81
- def lock!
82
- @ui.action(:lock).apply!
52
+ def initialize(open: false, id: nil)
53
+ @ui = tailmix(open: open, id: id)
83
54
  end
84
55
  end
85
56
 
86
- puts "-" * 100
87
- # puts ModalComponent.dev.docs
88
- # puts ""
89
- # puts "Scaffolds:"
90
- # puts ""
91
- # puts ModalComponent.dev.stimulus.scaffold
92
- # puts ""
93
-
94
- # >>>
95
- #
96
- # == Tailmix Docs for ModalComponent ==
97
- # Signature: `initialize(open: true, size: :md)`
98
- #
99
- # Dimensions:
100
- # - open (default: true)
101
- # - true:
102
- # - classes : "visible opacity-100"
103
- # - false:
104
- # - classes : "invisible opacity-0"
105
- # - size (default: :md)
106
- # - :sm:
107
- # - classes : "w-full max-w-sm p-4"
108
- # - classes (group: :dark_mode): "dark:text-slate-400"
109
- # - classes : "one two"
110
- # - :md:
111
- # - classes : "w-full max-w-md p-6"
112
- # - :lg:
113
- # - classes : "w-full max-w-lg p-8"
114
- #
115
- # Actions:
116
- # - :toggle
117
- # - :lock
118
- # - :enter_pending_state
119
- #
120
- # Stimulus:
121
- # - on `modal` controller:
122
- # - Targets: panel
123
- # - Actions: close
124
- #
125
- # Stimulus:
126
- # - on `form-submission` controller:
127
- # - Actions: submit
128
- #
129
- # Scaffolds:
130
- #
131
- # // Generated by Tailmix for the "modal" controller
132
- # // Path: app/javascript/controllers/modal_controller.js
133
- # import { Controller } from "@hotwired/stimulus"
134
- # import Tailmix from "tailmix"
135
- #
136
- # export default class extends Controller {
137
- # static targets = ['panel']
138
- # static values = { toggleData: Object }
139
- #
140
- # connect() {
141
- # console.log("modal controller connected to", this.element);
142
- # }
143
- # toggle(event) {
144
- # if (event) event.preventDefault();
145
- # Tailmix.run({ config: this.toggleDataValue, controller: this });
146
- # }
147
- #
148
- # close() {
149
- # console.log('modal#close fired');
150
- # }
151
- # }
152
- # ------------------------------------------------------------
153
- #
154
- # // Generated by Tailmix for the "form-submission" controller
155
- # // Path: app/javascript/controllers/form-submission_controller.js
156
- # import { Controller } from "@hotwired/stimulus"
157
- # import Tailmix from "tailmix"
158
- #
159
- # export default class extends Controller {
160
- # static targets = []
161
- # static values = { pendingData: Object }
162
- #
163
- # connect() {
164
- # console.log("form-submission controller connected to", this.element);
165
- # }
166
- # enterPendingState(event) {
167
- # if (event) event.preventDefault();
168
- # Tailmix.run({ config: this.pendingDataValue, controller: this });
169
- # }
170
- #
171
- # submit() {
172
- # console.log('form-submission#submit fired');
173
- # }
174
- # }
175
- # ------------------------------------------------------------
176
-
177
-
178
-
179
-
180
- modal = ModalComponent.new(size: :lg, open: true)
181
- # modal.lock!
182
- ui = modal.ui
183
57
 
58
+ modal = ModalComponent.new(open: false, id: :user_profile_modal)
59
+ ui = modal.ui
184
60
 
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
195
61
 
196
62
  # puts "Definition:"
197
63
  # puts JSON.pretty_generate(stringify_keys(ModalComponent.tailmix_definition.to_h))
198
- # ui.action(:lock).apply!
64
+ puts "-" * 100
65
+ puts ModalComponent.dev.docs
66
+ puts "-" * 100
199
67
 
200
68
  ModalComponent.dev.elements.each do |element_name|
201
69
  element = ui.send(element_name)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../payload_proxy"
3
+
4
+ module Tailmix
5
+ module Definition
6
+ module Builders
7
+ class ActionBuilder
8
+ def initialize
9
+ @transitions = []
10
+ end
11
+
12
+ def set(state_key, value)
13
+ processed_value = if value.is_a?(PayloadValue)
14
+ { __type: 'payload_value', key: value.key }
15
+ else
16
+ value
17
+ end
18
+ @transitions << { type: :set, payload: { key: state_key.to_sym, value: processed_value } }
19
+ end
20
+
21
+ def toggle(state_key)
22
+ @transitions << { type: :toggle, payload: { key: state_key.to_sym } }
23
+ end
24
+
25
+ def refresh(state_key, params: {})
26
+ @transitions << { type: :refresh, payload: { key: state_key.to_sym, params: params } }
27
+ end
28
+
29
+ def dispatch(event_name, detail: {})
30
+ @transitions << { type: :dispatch, payload: { name: event_name, detail: detail } }
31
+ end
32
+
33
+ def build_definition
34
+ Result::Action.new(transitions: @transitions.freeze)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tailmix
4
4
  module Definition
5
- module Contexts
5
+ module Builders
6
6
  module Actions
7
7
  class ElementBuilder
8
8
  def initialize(default_method)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tailmix
4
4
  module Definition
5
- module Contexts
5
+ module Builders
6
6
  class AttributeBuilder
7
7
  def initialize
8
8
  @classes = []
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "action_builder"
4
+ require_relative "element_builder"
5
+ require_relative "reactor_builder"
6
+ require_relative "../payload_proxy"
7
+
8
+ module Tailmix
9
+ module Definition
10
+ module Builders
11
+ class ComponentBuilder
12
+ attr_reader :component_name
13
+
14
+ def initialize(component_name:)
15
+ @states = {}
16
+ @actions = {}
17
+ @elements = {}
18
+ @component_name = component_name
19
+ @plugins = {}
20
+ @reactions = {}
21
+ end
22
+
23
+ def state(name, default: nil, endpoint: nil, toggle: false)
24
+ @states[name.to_sym] = { default: default, endpoint: endpoint }.compact
25
+ if toggle
26
+ action_name = :"toggle_#{name}"
27
+ action(action_name) { toggle name }
28
+ end
29
+ end
30
+
31
+ def action(name, &block)
32
+ builder = ActionBuilder.new
33
+ builder.instance_exec(PayloadProxy.new, &block)
34
+ @actions[name.to_sym] = builder
35
+ end
36
+
37
+ def element(name, classes = "", &block)
38
+ builder = ElementBuilder.new(name)
39
+ builder.attributes.classes(classes.split)
40
+ builder.instance_eval(&block) if block
41
+ @elements[name.to_sym] = builder
42
+ end
43
+
44
+ def plugin(name, options = {})
45
+ plugin_name = name.to_s.gsub(/_([a-z])/) { $1.upcase }
46
+ @plugins[plugin_name] = options
47
+ end
48
+
49
+ def react(on:, run: nil, **options, &block)
50
+ watched_states = Array(on)
51
+
52
+ # Processing the short form: `react on: :query, run: :search`
53
+ if run
54
+ builder = ReactorBuilder.new(watched_states.first)
55
+ builder.run(run, **options)
56
+ watched_states.each { |state| (@reactions[state] ||= []).concat(builder.build_rules) }
57
+ return
58
+ end
59
+
60
+ # Processing the full form with the block.
61
+ if block
62
+ builder = ReactorBuilder.new(watched_states.first)
63
+ builder.instance_eval(&block) # `instance_eval` чтобы не писать `r.`
64
+ watched_states.each { |state| (@reactions[state] ||= []).concat(builder.build_rules) }
65
+ end
66
+ end
67
+
68
+ def build_definition
69
+ Result::Context.new(
70
+ name: component_name,
71
+ states: @states.freeze,
72
+ actions: @actions.transform_values(&:build_definition).freeze,
73
+ elements: @elements.transform_values(&:build_definition).freeze,
74
+ reactions: @reactions.freeze,
75
+ plugins: @plugins.freeze
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -4,7 +4,7 @@ require_relative "variant_builder"
4
4
 
5
5
  module Tailmix
6
6
  module Definition
7
- module Contexts
7
+ module Builders
8
8
  class DimensionBuilder
9
9
  def initialize(default: nil)
10
10
  @variants = {}
@@ -13,12 +13,12 @@ module Tailmix
13
13
 
14
14
  def variant(name, classes = "", data: {}, aria: {}, &block)
15
15
  builder = VariantBuilder.new
16
+
16
17
  builder.classes(classes) if classes && !classes.empty?
17
18
  builder.data(data)
18
19
  builder.aria(aria)
19
20
 
20
21
  builder.instance_eval(&block) if block
21
-
22
22
  @variants[name] = builder.build_variant
23
23
  end
24
24
 
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "attribute_builder"
4
+ require_relative "dimension_builder"
5
+ require_relative "variant_builder"
6
+
7
+ module Tailmix
8
+ module Definition
9
+ module Builders
10
+ class ElementBuilder
11
+ def initialize(name)
12
+ @name = name
13
+ @default_attributes = {}
14
+ @dimensions = {}
15
+ @compound_variants = []
16
+ @event_bindings = []
17
+ @attribute_bindings = {}
18
+ @model_bindings = {}
19
+ end
20
+
21
+ def attributes
22
+ @attributes_builder ||= AttributeBuilder.new
23
+ end
24
+
25
+ def method_missing(name, *args, &block)
26
+ attribute_name = name.to_s.chomp("=").to_sym
27
+ @default_attributes[attribute_name] = args.first
28
+ end
29
+
30
+ def respond_to_missing?(*_args)
31
+ true
32
+ end
33
+
34
+ def on(event_name, action_name, with: nil, **options)
35
+ # `with` mapping: `{ payload_key => state_key }`
36
+ @event_bindings << { event: event_name, action: action_name, with: with, options: options }
37
+ end
38
+
39
+ def bind(attribute_name, to:)
40
+ @attribute_bindings[attribute_name.to_sym] = to.to_sym
41
+ end
42
+
43
+ def model(attribute_name, to:, on: :input, action: nil, debounce: nil)
44
+ @model_bindings[attribute_name.to_sym] = {
45
+ state: to.to_sym,
46
+ event: on,
47
+ action: action,
48
+ debounce: debounce
49
+ }.compact
50
+ end
51
+
52
+ def dimension(name, &block)
53
+ builder = DimensionBuilder.new
54
+ builder.instance_eval(&block)
55
+ @dimensions[name.to_sym] = builder.build_dimension
56
+ end
57
+
58
+ def compound_variant(on:, &block)
59
+ builder = VariantBuilder.new
60
+ builder.instance_eval(&block)
61
+
62
+ @compound_variants << {
63
+ on: on,
64
+ modifications: builder.build_variant
65
+ }
66
+ end
67
+
68
+ def build_definition
69
+ Result::Element.new(
70
+ name: @name,
71
+ attributes: attributes.build_definition,
72
+ default_attributes: @default_attributes.freeze,
73
+ dimensions: @dimensions.freeze,
74
+ compound_variants: @compound_variants.freeze,
75
+ event_bindings: @event_bindings.freeze,
76
+ attribute_bindings: @attribute_bindings.freeze,
77
+ model_bindings: @model_bindings.freeze
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require_relative "rule_builder"
3
+
4
+ module Tailmix
5
+ module Definition
6
+ module Builders
7
+ class ReactorBuilder
8
+ def initialize(watched_state)
9
+ @watched_state = watched_state
10
+ @rules = []
11
+ end
12
+
13
+ # Start a method for the rule chain: r.value("commercial")
14
+ def value(expected_value)
15
+ rule_builder = RuleBuilder.new(@watched_state)
16
+ rule_builder.value(expected_value)
17
+ @rules << rule_builder
18
+ rule_builder
19
+ end
20
+
21
+ # Alternative startup method: r.state(:zip_code)
22
+ def state(state_key)
23
+ rule_builder = RuleBuilder.new(state_key)
24
+ @rules << rule_builder
25
+ rule_builder
26
+ end
27
+
28
+ # Unconditional effect (always triggers on change)
29
+ def run(action_name, with: nil)
30
+ # We create an "empty" rule with a condition that is always true.
31
+ rule_builder = RuleBuilder.new(nil)
32
+ rule_builder.instance_variable_set(:@rule, { condition: { type: :always_true } })
33
+ rule_builder.run(action_name, with: with)
34
+ @rules << rule_builder
35
+ end
36
+
37
+ def build_rules
38
+ @rules.map(&:build_rule)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Builders
6
+ class RuleBuilder
7
+ def initialize(source_state_key)
8
+ @rule = { condition: { type: :eql, source: { type: :state, key: source_state_key } } }
9
+ end
10
+
11
+ def value(expected_value)
12
+ @rule[:condition][:value] = expected_value
13
+ self
14
+ end
15
+ alias_method :is, :value
16
+
17
+ def is_not(expected_value)
18
+ @rule[:condition][:type] = :not_eql
19
+ @rule[:condition][:value] = expected_value
20
+ self
21
+ end
22
+
23
+ def is_truthy
24
+ @rule[:condition][:type] = :truthy
25
+ self
26
+ end
27
+
28
+ def set_state(payload)
29
+ add_effect(:set_state, payload: payload)
30
+ end
31
+
32
+ def run(action_name, with: nil)
33
+ add_effect(:run_action, name: action_name, with: with)
34
+ end
35
+
36
+ def dispatch(event_name, detail: {})
37
+ add_effect(:dispatch_event, name: event_name, detail: detail)
38
+ end
39
+
40
+ def call(element_name, method_name, *args)
41
+ add_effect(:call_method, element: element_name, method: method_name, args: args)
42
+ end
43
+
44
+ def build_rule
45
+ @rule
46
+ end
47
+
48
+ private
49
+
50
+ def add_effect(type, **payload)
51
+ @rule[:effects] ||= []
52
+ @rule[:effects] << { type: type, payload: payload }
53
+ self
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailmix
4
+ module Definition
5
+ module Builders
6
+ class StateBuilder
7
+ def initialize
8
+ @data_source = {}
9
+ end
10
+
11
+ def endpoint(method, url:)
12
+ @data_source = { method: method, url: url }
13
+ end
14
+
15
+ def build_data_source
16
+ @data_source.empty? ? nil : @data_source.freeze
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  module Tailmix
4
4
  module Definition
5
- module Contexts
5
+ module Builders
6
6
  class VariantBuilder
7
7
  def initialize
8
8
  @class_groups = []
9
9
  @data = {}
10
10
  @aria = {}
11
+ @attributes = {}
11
12
  end
12
13
 
13
14
  def classes(class_string, options = {})
@@ -22,11 +23,25 @@ module Tailmix
22
23
  @aria.merge!(hash)
23
24
  end
24
25
 
26
+ def method_missing(name, *args, &block)
27
+ # `disabled true` -> { disabled: true }
28
+ # `placeholder "text"` -> { placeholder: "text" }
29
+ # `type "password"` -> { type: "password" }
30
+ attribute_name = name.to_s.chomp("=").to_sym
31
+ value = args.first
32
+ @attributes[attribute_name] = value
33
+ end
34
+
35
+ def respond_to_missing?(*_args)
36
+ true
37
+ end
38
+
25
39
  def build_variant
26
40
  Definition::Result::Variant.new(
27
41
  class_groups: @class_groups.freeze,
28
42
  data: @data.freeze,
29
- aria: @aria.freeze
43
+ aria: @aria.freeze,
44
+ attributes: @attributes.freeze
30
45
  )
31
46
  end
32
47
  end