poetry-core 0.0.2

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 (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/DESIGN.md +145 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +30 -0
  6. data/THIRD_PARTY_NOTICES.md +507 -0
  7. data/app/components/poetry/core/box/component.rb +82 -0
  8. data/app/components/poetry/core/component.rb +390 -0
  9. data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
  10. data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
  11. data/app/components/poetry/core/concerns/introspection.rb +533 -0
  12. data/app/components/poetry/core/concerns/options.rb +262 -0
  13. data/app/components/poetry/core/concerns/parts.rb +148 -0
  14. data/app/components/poetry/core/concerns/stimulus.rb +363 -0
  15. data/app/components/poetry/core/concerns/styles.rb +323 -0
  16. data/app/components/poetry/core/style.rb +150 -0
  17. data/app/components/poetry/core/wrapper/component.rb +90 -0
  18. data/app/javascript/poetry/core/accordion_controller.js +165 -0
  19. data/app/javascript/poetry/core/action_bar_controller.js +109 -0
  20. data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
  21. data/app/javascript/poetry/core/calendar_controller.js +406 -0
  22. data/app/javascript/poetry/core/carousel_controller.js +167 -0
  23. data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
  24. data/app/javascript/poetry/core/checked_controller.js +208 -0
  25. data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
  26. data/app/javascript/poetry/core/combobox_controller.js +1095 -0
  27. data/app/javascript/poetry/core/command_controller.js +510 -0
  28. data/app/javascript/poetry/core/context_menu_controller.js +204 -0
  29. data/app/javascript/poetry/core/date_field_controller.js +584 -0
  30. data/app/javascript/poetry/core/date_picker_controller.js +111 -0
  31. data/app/javascript/poetry/core/deferred_controller.js +111 -0
  32. data/app/javascript/poetry/core/dialog_controller.js +206 -0
  33. data/app/javascript/poetry/core/dismissable_controller.js +182 -0
  34. data/app/javascript/poetry/core/drawer_controller.js +365 -0
  35. data/app/javascript/poetry/core/file_input_controller.js +191 -0
  36. data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
  37. data/app/javascript/poetry/core/helpers/announce.js +228 -0
  38. data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
  39. data/app/javascript/poetry/core/helpers/collection.js +18 -0
  40. data/app/javascript/poetry/core/helpers/direction.js +15 -0
  41. data/app/javascript/poetry/core/helpers/escape.js +38 -0
  42. data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
  43. data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
  44. data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
  45. data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
  46. data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
  47. data/app/javascript/poetry/core/helpers/mask.js +283 -0
  48. data/app/javascript/poetry/core/helpers/portal.js +217 -0
  49. data/app/javascript/poetry/core/helpers/presence.js +210 -0
  50. data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
  51. data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
  52. data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
  53. data/app/javascript/poetry/core/helpers/state.js +80 -0
  54. data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
  55. data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
  56. data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
  57. data/app/javascript/poetry/core/hotkey_controller.js +55 -0
  58. data/app/javascript/poetry/core/hover_card_controller.js +431 -0
  59. data/app/javascript/poetry/core/index.js +218 -0
  60. data/app/javascript/poetry/core/mask_controller.js +500 -0
  61. data/app/javascript/poetry/core/menu_controller.js +1050 -0
  62. data/app/javascript/poetry/core/menubar_controller.js +329 -0
  63. data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
  64. data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
  65. data/app/javascript/poetry/core/number_field_controller.js +404 -0
  66. data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
  67. data/app/javascript/poetry/core/otp_controller.js +219 -0
  68. data/app/javascript/poetry/core/popover_controller.js +300 -0
  69. data/app/javascript/poetry/core/popper_controller.js +439 -0
  70. data/app/javascript/poetry/core/pressed_controller.js +74 -0
  71. data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
  72. data/app/javascript/poetry/core/radio_group_controller.js +197 -0
  73. data/app/javascript/poetry/core/resizable_controller.js +202 -0
  74. data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
  75. data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
  76. data/app/javascript/poetry/core/search_field_controller.js +95 -0
  77. data/app/javascript/poetry/core/select_controller.js +924 -0
  78. data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
  79. data/app/javascript/poetry/core/sheet_controller.js +24 -0
  80. data/app/javascript/poetry/core/sidebar_controller.js +272 -0
  81. data/app/javascript/poetry/core/slider_controller.js +419 -0
  82. data/app/javascript/poetry/core/state_controller.js +82 -0
  83. data/app/javascript/poetry/core/table_selection_controller.js +153 -0
  84. data/app/javascript/poetry/core/tabs_controller.js +151 -0
  85. data/app/javascript/poetry/core/tag_group_controller.js +134 -0
  86. data/app/javascript/poetry/core/toast_controller.js +228 -0
  87. data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
  88. data/app/javascript/poetry/core/toaster_controller.js +197 -0
  89. data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
  90. data/app/javascript/poetry/core/tooltip_controller.js +541 -0
  91. data/app/javascript/poetry/core/tree_controller.js +265 -0
  92. data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
  93. data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
  94. data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
  95. data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
  96. data/app/views/poetry/core/preview.html.erb +13 -0
  97. data/config/component_registry.yml +20 -0
  98. data/config/controllers_manifest.json +1528 -0
  99. data/config/importmap.rb +11 -0
  100. data/config/state_vocabulary.json +31 -0
  101. data/lib/active_model/type/list.rb +33 -0
  102. data/lib/active_model/type/symbol.rb +36 -0
  103. data/lib/poetry/core/check/stable_identity.rb +104 -0
  104. data/lib/poetry/core/check.rb +1445 -0
  105. data/lib/poetry/core/config.rb +286 -0
  106. data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
  107. data/lib/poetry/core/css/bem_merger.rb +45 -0
  108. data/lib/poetry/core/css/bem_reference.rb +87 -0
  109. data/lib/poetry/core/css/override_scan.rb +110 -0
  110. data/lib/poetry/core/css/resolver.rb +161 -0
  111. data/lib/poetry/core/css/safelist.rb +41 -0
  112. data/lib/poetry/core/css/tailwind_merger.rb +102 -0
  113. data/lib/poetry/core/css/template_classes.rb +102 -0
  114. data/lib/poetry/core/css/theme_coverage.rb +72 -0
  115. data/lib/poetry/core/css/var_coverage.rb +75 -0
  116. data/lib/poetry/core/css/verifier.rb +106 -0
  117. data/lib/poetry/core/design_lint.rb +720 -0
  118. data/lib/poetry/core/design_md/import.rb +250 -0
  119. data/lib/poetry/core/design_md.rb +461 -0
  120. data/lib/poetry/core/engine.rb +71 -0
  121. data/lib/poetry/core/errors.rb +53 -0
  122. data/lib/poetry/core/html/attributes.rb +667 -0
  123. data/lib/poetry/core/icons.rb +157 -0
  124. data/lib/poetry/core/llms_text.rb +354 -0
  125. data/lib/poetry/core/page_architectures.rb +304 -0
  126. data/lib/poetry/core/part_contract.rb +285 -0
  127. data/lib/poetry/core/preview/abstract.rb +121 -0
  128. data/lib/poetry/core/preview/base.rb +227 -0
  129. data/lib/poetry/core/preview/sidecarable.rb +136 -0
  130. data/lib/poetry/core/preview/template.rb +186 -0
  131. data/lib/poetry/core/recipe_items.rb +82 -0
  132. data/lib/poetry/core/registry.rb +363 -0
  133. data/lib/poetry/core/registry_address.rb +115 -0
  134. data/lib/poetry/core/registry_client.rb +217 -0
  135. data/lib/poetry/core/registry_installer.rb +234 -0
  136. data/lib/poetry/core/registry_items.rb +184 -0
  137. data/lib/poetry/core/skill_text.rb +288 -0
  138. data/lib/poetry/core/stable_id.rb +110 -0
  139. data/lib/poetry/core/stimulus/builder.rb +395 -0
  140. data/lib/poetry/core/stimulus/declarations.rb +444 -0
  141. data/lib/poetry/core/stimulus/manifest.rb +68 -0
  142. data/lib/poetry/core/stimulus/merger.rb +131 -0
  143. data/lib/poetry/core/stimulus_contract.rb +263 -0
  144. data/lib/poetry/core/tag_helper.rb +10 -0
  145. data/lib/poetry/core/template_compile.rb +74 -0
  146. data/lib/poetry/core/token_import.rb +317 -0
  147. data/lib/poetry/core/tokens/color.rb +215 -0
  148. data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
  149. data/lib/poetry/core/tokens/generator.rb +211 -0
  150. data/lib/poetry/core/tokens.rb +117 -0
  151. data/lib/poetry/core/version.rb +9 -0
  152. data/lib/poetry/core.rb +52 -0
  153. data/lib/poetry-core.rb +5 -0
  154. data/tokens/aliases.css +145 -0
  155. data/tokens/tailwind-theme.css +47 -0
  156. data/tokens/tokens.css +78 -0
  157. data/tokens/tokens.dtcg.json +779 -0
  158. data/vendor/floating-ui/LICENSE +20 -0
  159. data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
  160. data/vendor/shadcn-tailwind/LICENSE +21 -0
  161. data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
  162. data/vendor/shadcn-tailwind/tailwind.css +629 -0
  163. data/vendor/tw-animate-css/LICENSE +21 -0
  164. data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
  165. data/vendor/tw-animate-css/tw-animate.css +1 -0
  166. metadata +267 -0
@@ -0,0 +1,395 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Core
5
+ module Stimulus
6
+ # Builder class for constructing Stimulus controller HTML attributes in a Ruby-friendly way.
7
+ #
8
+ # This class provides a clean API for adding Stimulus data attributes to HTML elements
9
+ # without manually constructing attribute strings. It handles:
10
+ # - Controller registration
11
+ # - Values (data passed to controllers)
12
+ # - CSS class references (for Stimulus classes API)
13
+ # - Outlets (connections to other controllers)
14
+ # - Actions (event listeners)
15
+ # - Custom parameters
16
+ #
17
+ # @example Basic usage
18
+ # builder = Poetry::Core::Stimulus::Builder.new("dropdown", html_attributes)
19
+ # builder.register_controller
20
+ # builder.with_value(:open, false)
21
+ # builder.with_action(:toggle, on: :click)
22
+ # # Produces: data-controller="dropdown"
23
+ # # data-dropdown-open-value="false"
24
+ # # data-action="click->dropdown#toggle"
25
+ #
26
+ # @example With initialization options
27
+ # builder = Poetry::Core::Stimulus::Builder.new("menu", html_attributes,
28
+ # values: { visible: true },
29
+ # actions: { show: :mouseenter, hide: :mouseleave },
30
+ # classes: { active: "bg-blue-500" }
31
+ # )
32
+ #
33
+ class Builder
34
+ # Event aliases for common event combinations
35
+ # @example Using hover_in alias
36
+ # with_action(:show, on: :hover_in)
37
+ # # Expands to both mouseenter and focus events
38
+ EVENT_ALIASES = {
39
+ hover_in: %w[mouseenter focus].freeze,
40
+ hover_out: %w[mouseleave blur].freeze
41
+ }.freeze
42
+
43
+ # @return [String] The formatted Stimulus controller identifier
44
+ attr_reader :identifier
45
+
46
+ # @return [Object] The HTML attributes object that will be modified
47
+ attr_reader :html_attributes
48
+
49
+ # Creates a new Stimulus builder instance
50
+ #
51
+ # @param identifier [String, Symbol, Array] The Stimulus controller identifier(s)
52
+ # @param html_attributes [Object] An object that responds to merge_stimulus! methods
53
+ # @param options [Hash] Optional configurations
54
+ # @option options [Hash] :values Key-value pairs to register as Stimulus values
55
+ # @option options [Hash] :classes Key-value pairs to register as Stimulus classes
56
+ # @option options [Hash] :outlets Key-value pairs to register as Stimulus outlets
57
+ # @option options [Hash] :params Key-value pairs to register as custom params
58
+ # @option options [Hash] :actions Method-event pairs to register as Stimulus actions
59
+ #
60
+ # @example Single controller
61
+ # Builder.new("dropdown", html_attributes)
62
+ #
63
+ # @example Multiple controllers (namespaced)
64
+ # Builder.new([:admin, :dropdown], html_attributes)
65
+ #
66
+ # @example With options
67
+ # Builder.new("modal", html_attributes,
68
+ # values: { open: false, size: "large" },
69
+ # actions: { toggle: :click, close: { on: :keydown, at: :window } }
70
+ # )
71
+ def initialize(identifier, html_attributes, options = {})
72
+ @identifier = format_identifier identifier
73
+ # nil for host-app controllers; poetry-namespaced identifiers are
74
+ # validated against the committed controllers manifest.
75
+ @definition = Manifest.definition(@identifier)
76
+ @html_attributes = html_attributes
77
+ register_values options[:values]
78
+ register_classes options[:classes]
79
+ register_outlets options[:outlets]
80
+ register_params options[:params]
81
+ register_actions options[:actions]
82
+ end
83
+
84
+ # Registers this controller in the data-controller attribute
85
+ #
86
+ # @return [void]
87
+ #
88
+ # @example
89
+ # builder.register_controller
90
+ # # Adds: data-controller="dropdown"
91
+ def register_controller
92
+ html_attributes.merge_stimulus_controllers!(identifier)
93
+ end
94
+
95
+ # Returns the attribute name for a Stimulus target
96
+ #
97
+ # @return [Symbol] The target attribute name (e.g., :dropdown_target)
98
+ #
99
+ # @example
100
+ # builder.target_attribute_name
101
+ # # => :dropdown_target
102
+ def target_attribute_name
103
+ key :target
104
+ end
105
+
106
+ # Adds a Stimulus value to the HTML attributes
107
+ #
108
+ # Values are used to pass data from HTML to Stimulus controllers.
109
+ #
110
+ # @param name [String, Symbol] The name of the value
111
+ # @param value [Object] The value to set (will be JSON-encoded if necessary)
112
+ # @return [void]
113
+ #
114
+ # @example
115
+ # builder.with_value(:open, false)
116
+ # # Adds: data-dropdown-open-value="false"
117
+ def with_value(name, value)
118
+ validate! camelize(name), @definition&.fetch("values", {})&.keys, "value"
119
+ html_attributes.merge_stimulus! key(name, :value) => value
120
+ end
121
+
122
+ # Adds a Stimulus target attribute to the HTML attributes
123
+ #
124
+ # @param name [String, Symbol] The target name (snake_case camelizes)
125
+ # @return [void]
126
+ #
127
+ # @example
128
+ # builder.with_target(:dialog)
129
+ # # Adds: data-dropdown-target="dialog"
130
+ def with_target(name)
131
+ html_attributes.merge_stimulus! target_attribute_name => target(name)
132
+ end
133
+
134
+ # Validates and returns the JS target name
135
+ #
136
+ # @param name [String, Symbol] The target name
137
+ # @return [String] The camelCase target name
138
+ def target(name)
139
+ camelize(name).tap { |js_name| validate! js_name, @definition&.fetch("targets", []), "target" }
140
+ end
141
+
142
+ # Adds a Stimulus class reference to the HTML attributes
143
+ #
144
+ # Classes are used to reference CSS classes that the controller can toggle.
145
+ #
146
+ # @param name [String, Symbol] The name of the class reference
147
+ # @param value [String] The CSS class name(s)
148
+ # @return [void]
149
+ #
150
+ # @example
151
+ # builder.with_class(:active, "bg-blue-500 text-white")
152
+ # # Adds: data-dropdown-active-class="bg-blue-500 text-white"
153
+ def with_class(name, value)
154
+ html_attributes.merge_stimulus! key(name, :class) => value
155
+ end
156
+
157
+ # Adds a Stimulus outlet reference to the HTML attributes
158
+ #
159
+ # Outlets allow one controller to reference and interact with other controllers.
160
+ #
161
+ # @param name [String, Symbol] The name of the outlet
162
+ # @param value [String] The outlet selector
163
+ # @return [void]
164
+ #
165
+ # @example
166
+ # builder.with_outlet(:modal, ".modal-controller")
167
+ # # Adds: data-dropdown-modal-outlet=".modal-controller"
168
+ def with_outlet(name, value)
169
+ html_attributes.merge_stimulus! key(format_identifier(name), :outlet) => value
170
+ end
171
+
172
+ # Adds an action parameter attribute (`data-<identifier>-<name>-param`),
173
+ # which the controller reads from `event.params`.
174
+ #
175
+ # @param name [String, Symbol] The parameter name
176
+ # @param value [Object] The parameter value
177
+ # @return [void]
178
+ #
179
+ # @example
180
+ # builder.with_param(:size, "large")
181
+ # # Adds: data-dropdown-size-param="large"
182
+ def with_param(name, value)
183
+ html_attributes.merge_stimulus! param_attribute_name(name) => value
184
+ end
185
+
186
+ # Returns the attribute name for a parameter
187
+ #
188
+ # @param name [String, Symbol] The parameter name
189
+ # @return [Symbol] The parameter attribute name
190
+ #
191
+ # @example
192
+ # builder.param_attribute_name(:size)
193
+ # # => :dropdown_size_param
194
+ def param_attribute_name(name)
195
+ key name, :param
196
+ end
197
+
198
+ # Adds a Stimulus action (event listener) to the HTML attributes
199
+ #
200
+ # @param method [String, Symbol] The controller method to call
201
+ # @param on [Symbol, String, Array<Symbol>, nil] The event(s) to listen for
202
+ # @param at [Symbol, String, nil] The event target (e.g., :window, :document)
203
+ # @return [void]
204
+ #
205
+ # @example Basic action
206
+ # builder.with_action(:toggle, on: :click)
207
+ # # Adds: data-action="click->dropdown#toggle"
208
+ #
209
+ # @example Multiple events using alias
210
+ # builder.with_action(:show, on: :hover_in)
211
+ # # Adds: data-action="mouseenter->dropdown#show focus->dropdown#show"
212
+ #
213
+ # @example Global event
214
+ # builder.with_action(:close, on: :keydown, at: :window)
215
+ # # Adds: data-action="keydown@window->dropdown#close"
216
+ def with_action(method, on: nil, at: nil)
217
+ html_attributes.merge_stimulus_actions! action(method, on: on, at: at)
218
+ end
219
+
220
+ # Builds a Stimulus action descriptor string without adding it to the
221
+ # attributes ({#with_action} adds it).
222
+ #
223
+ # @param method [String, Symbol] The controller method to call
224
+ # @param on [Symbol, String, Array<Symbol>, nil] The event(s) to listen for
225
+ # @param at [Symbol, String, nil] The event target (e.g., :window, :document)
226
+ # @return [String] The formatted action string
227
+ #
228
+ # @example Simple action
229
+ # builder.action(:toggle)
230
+ # # => "dropdown#toggle"
231
+ #
232
+ # @example With event
233
+ # builder.action(:toggle, on: :click)
234
+ # # => "click->dropdown#toggle"
235
+ #
236
+ # @example With event target
237
+ # builder.action(:close, on: :keydown, at: :window)
238
+ # # => "keydown@window->dropdown#close"
239
+ #
240
+ # @example Multiple events
241
+ # builder.action(:show, on: [:mouseenter, :focus])
242
+ # # => "mouseenter->dropdown#show focus->dropdown#show"
243
+ def action(method, on: nil, at: nil)
244
+ js_method = camelize(method)
245
+ validate! js_method, @definition&.fetch("methods", []), "action method"
246
+ controller_method = "#{identifier}##{js_method}"
247
+
248
+ if on.nil?
249
+ controller_method
250
+ else
251
+ events = Array(on).map { |event| EVENT_ALIASES.fetch(event, event) }.flatten
252
+
253
+ controller_actions = events.each_with_object([]) do |event, actions|
254
+ if event.nil?
255
+ actions << controller_method
256
+ else
257
+ event = "#{event}@#{at}" if at.present?
258
+ actions << "#{event}->#{controller_method}"
259
+ end
260
+ end
261
+
262
+ controller_actions.join " "
263
+ end
264
+ end
265
+
266
+ # Builds a custom Stimulus event name
267
+ #
268
+ # @param event [String, Symbol] The event name
269
+ # @return [String] The namespaced event name
270
+ #
271
+ # @example
272
+ # builder.event(:opened)
273
+ # # => "dropdown:opened"
274
+ #
275
+ # @example Usage in JavaScript
276
+ # // Dispatch custom event: this.dispatch("opened")
277
+ # // Listen in HTML: data-action="dropdown:opened->other#handleOpened"
278
+ def event(event)
279
+ "#{identifier}:#{event}"
280
+ end
281
+
282
+ private
283
+
284
+ delegate :format_identifier, to: :class
285
+
286
+ # Ruby snake_case -> Stimulus camelCase (the JS-side name).
287
+ def camelize(name)
288
+ name.to_s.gsub(/_([a-z\d])/) { Regexp.last_match(1).upcase }
289
+ end
290
+
291
+ # No-op for host-app controllers (no definition); raises with the
292
+ # known list for poetry controllers - the message IS the fix
293
+ # (agent-teachable failures).
294
+ def validate!(js_name, known, kind)
295
+ return if known.nil? || known.include?(js_name)
296
+
297
+ raise Manifest::UnknownName,
298
+ "unknown #{kind} #{js_name.inspect} for #{identifier} - known #{kind}s: #{known.sort.join(", ")}"
299
+ end
300
+
301
+ # Builds an attribute key from parts
302
+ #
303
+ # @param parts [Array] The parts to join
304
+ # @return [Symbol] The formatted key
305
+ #
306
+ # @example
307
+ # key(:open, :value)
308
+ # # => :dropdown_open_value
309
+ def key(*parts)
310
+ key = parts.map(&:to_s)
311
+ key.unshift identifier
312
+ key.join("_").tr("-", "_").to_sym
313
+ end
314
+
315
+ # Registers multiple Stimulus values from a hash
316
+ #
317
+ # @param values [Hash, nil] Hash of name-value pairs
318
+ # @return [void]
319
+ def register_values(values)
320
+ return if values.blank?
321
+
322
+ values.each { |name, value| with_value name, value }
323
+ end
324
+
325
+ # Registers multiple Stimulus classes from a hash
326
+ #
327
+ # @param classes [Hash, nil] Hash of name-class pairs
328
+ # @return [void]
329
+ def register_classes(classes)
330
+ return if classes.blank?
331
+
332
+ classes.each { |name, value| with_class name, value }
333
+ end
334
+
335
+ # Registers multiple Stimulus outlets from a hash
336
+ #
337
+ # @param outlets [Hash, nil] Hash of name-outlet pairs
338
+ # @return [void]
339
+ def register_outlets(outlets)
340
+ return if outlets.blank?
341
+
342
+ outlets.each { |name, value| with_outlet name, value }
343
+ end
344
+
345
+ # Registers multiple custom params from a hash
346
+ #
347
+ # @param params [Hash, nil] Hash of name-param pairs
348
+ # @return [void]
349
+ def register_params(params)
350
+ return if params.blank?
351
+
352
+ params.each { |name, value| with_param name, value }
353
+ end
354
+
355
+ # Registers multiple Stimulus actions from a hash
356
+ #
357
+ # @param actions [Hash, nil] Hash of method-event pairs
358
+ # Values can be symbols, arrays, or hashes with :on and :at keys
359
+ # @return [void]
360
+ #
361
+ # @example
362
+ # register_actions(toggle: :click, close: { on: :keydown, at: :window })
363
+ def register_actions(actions)
364
+ return if actions.blank?
365
+
366
+ actions.each do |method, value|
367
+ if value.is_a?(Hash)
368
+ with_action method, **value
369
+ else
370
+ with_action method, on: value
371
+ end
372
+ end
373
+ end
374
+
375
+ class << self
376
+ # Formats an identifier by converting underscores to dashes
377
+ #
378
+ # @param identifier [String, Symbol, Array] The identifier(s) to format
379
+ # @return [String] The formatted identifier
380
+ #
381
+ # @example Single identifier
382
+ # format_identifier(:my_controller)
383
+ # # => "my-controller"
384
+ #
385
+ # @example Multiple identifiers (namespaced)
386
+ # format_identifier([:admin, :dropdown])
387
+ # # => "admin--dropdown"
388
+ def format_identifier(identifier)
389
+ Array(identifier).map { |i| i.to_s.tr("_", "-") }.join("--")
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end