vident 1.0.2 → 2.0.1

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +45 -17
  4. data/lib/vident/caching.rb +4 -110
  5. data/lib/vident/capabilities/caching.rb +98 -0
  6. data/lib/vident/capabilities/child_element_rendering.rb +92 -0
  7. data/lib/vident/capabilities/class_list_building.rb +23 -0
  8. data/lib/vident/capabilities/declarable.rb +39 -0
  9. data/lib/vident/capabilities/identifiable.rb +54 -0
  10. data/lib/vident/capabilities/inspectable.rb +17 -0
  11. data/lib/vident/capabilities/root_element_rendering.rb +31 -0
  12. data/lib/vident/capabilities/stimulus_data_emitting.rb +98 -0
  13. data/lib/vident/capabilities/stimulus_declaring.rb +79 -0
  14. data/lib/vident/capabilities/stimulus_draft.rb +51 -0
  15. data/lib/vident/capabilities/stimulus_mutation.rb +60 -0
  16. data/lib/vident/capabilities/stimulus_parsing.rb +144 -0
  17. data/lib/vident/capabilities/tailwind.rb +18 -0
  18. data/lib/vident/component.rb +14 -76
  19. data/lib/vident/engine.rb +6 -5
  20. data/lib/vident/error.rb +16 -0
  21. data/lib/{vident2 → vident}/internals/action_builder.rb +18 -22
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/{vident2 → vident}/internals/class_list_builder.rb +5 -22
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/{vident2 → vident}/internals/declarations.rb +6 -18
  26. data/lib/{vident2 → vident}/internals/draft.rb +3 -16
  27. data/lib/{vident2 → vident}/internals/dsl.rb +6 -32
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/{vident2 → vident}/internals/resolver.rb +101 -91
  31. data/lib/{vident2 → vident}/internals/target_builder.rb +1 -7
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/{vident2 → vident}/stimulus/action.rb +11 -24
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/{vident2 → vident}/stimulus/class_map.rb +6 -18
  36. data/lib/{vident2 → vident}/stimulus/collection.rb +6 -8
  37. data/lib/vident/stimulus/combinable.rb +30 -0
  38. data/lib/vident/stimulus/controller.rb +45 -0
  39. data/lib/vident/stimulus/naming.rb +9 -9
  40. data/lib/vident/stimulus/null.rb +7 -0
  41. data/lib/{vident2 → vident}/stimulus/outlet.rb +12 -32
  42. data/lib/{vident2 → vident}/stimulus/param.rb +5 -11
  43. data/lib/{vident2 → vident}/stimulus/target.rb +5 -14
  44. data/lib/vident/stimulus/value.rb +57 -0
  45. data/lib/vident/stimulus_null.rb +4 -8
  46. data/lib/vident/tailwind.rb +4 -17
  47. data/lib/vident/types.rb +28 -0
  48. data/lib/vident/version.rb +1 -6
  49. data/lib/vident.rb +46 -36
  50. data/skills/vident/SKILL.md +122 -19
  51. data/skills/vident/api-reference.md +259 -115
  52. data/skills/vident/examples.md +23 -10
  53. metadata +38 -60
  54. data/lib/vident/child_element_helper.rb +0 -64
  55. data/lib/vident/class_list_builder.rb +0 -112
  56. data/lib/vident/component_attribute_resolver.rb +0 -106
  57. data/lib/vident/component_class_lists.rb +0 -37
  58. data/lib/vident/stimulus/primitive.rb +0 -38
  59. data/lib/vident/stimulus.rb +0 -31
  60. data/lib/vident/stimulus_action.rb +0 -133
  61. data/lib/vident/stimulus_action_collection.rb +0 -11
  62. data/lib/vident/stimulus_attribute_base.rb +0 -67
  63. data/lib/vident/stimulus_attributes.rb +0 -129
  64. data/lib/vident/stimulus_builder.rb +0 -136
  65. data/lib/vident/stimulus_class.rb +0 -59
  66. data/lib/vident/stimulus_class_collection.rb +0 -11
  67. data/lib/vident/stimulus_collection_base.rb +0 -51
  68. data/lib/vident/stimulus_component.rb +0 -75
  69. data/lib/vident/stimulus_controller.rb +0 -41
  70. data/lib/vident/stimulus_controller_collection.rb +0 -14
  71. data/lib/vident/stimulus_data_attribute_builder.rb +0 -32
  72. data/lib/vident/stimulus_helper.rb +0 -66
  73. data/lib/vident/stimulus_outlet.rb +0 -90
  74. data/lib/vident/stimulus_outlet_collection.rb +0 -11
  75. data/lib/vident/stimulus_param.rb +0 -42
  76. data/lib/vident/stimulus_param_collection.rb +0 -11
  77. data/lib/vident/stimulus_target.rb +0 -47
  78. data/lib/vident/stimulus_target_collection.rb +0 -18
  79. data/lib/vident/stimulus_value.rb +0 -39
  80. data/lib/vident/stimulus_value_collection.rb +0 -11
  81. data/lib/vident2/caching.rb +0 -93
  82. data/lib/vident2/component.rb +0 -538
  83. data/lib/vident2/engine.rb +0 -18
  84. data/lib/vident2/error.rb +0 -30
  85. data/lib/vident2/internals/attribute_writer.rb +0 -22
  86. data/lib/vident2/internals/declaration.rb +0 -17
  87. data/lib/vident2/internals/plan.rb +0 -12
  88. data/lib/vident2/internals/registry.rb +0 -41
  89. data/lib/vident2/phlex/html.rb +0 -84
  90. data/lib/vident2/phlex.rb +0 -9
  91. data/lib/vident2/stimulus/controller.rb +0 -59
  92. data/lib/vident2/stimulus/naming.rb +0 -26
  93. data/lib/vident2/stimulus/null.rb +0 -16
  94. data/lib/vident2/stimulus/value.rb +0 -77
  95. data/lib/vident2/tailwind.rb +0 -19
  96. data/lib/vident2/version.rb +0 -5
  97. data/lib/vident2/view_component/base.rb +0 -124
  98. data/lib/vident2/view_component.rb +0 -9
  99. data/lib/vident2.rb +0 -50
@@ -1,538 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
-
5
- require_relative "error"
6
- require_relative "internals/declarations"
7
- require_relative "internals/dsl"
8
- require_relative "internals/registry"
9
- require_relative "internals/draft"
10
- require_relative "internals/plan"
11
- require_relative "internals/resolver"
12
- require_relative "internals/attribute_writer"
13
- require_relative "internals/class_list_builder"
14
- require_relative "stimulus/collection"
15
- require_relative "tailwind"
16
-
17
- module Vident2
18
- # Composition root for Vident 2.0 components: props, DSL receiver,
19
- # singular/plural parsers, add_stimulus_* mutators, and the render
20
- # pipeline glue.
21
- module Component
22
- extend ActiveSupport::Concern
23
-
24
- include ::Vident2::Tailwind
25
-
26
- included do
27
- extend Literal::Properties
28
-
29
- prop :element_tag, Symbol, default: :div
30
- prop :id, _Nilable(String)
31
- prop :classes, _Union(String, _Array(String)), default: -> { [] }
32
- prop :html_options, Hash, default: -> { {} }
33
-
34
- # Stimulus input props. Resolver folds these into the Draft at init.
35
- # `stimulus_controllers:` APPENDS to the implied controller (which
36
- # seeds first unless `no_stimulus_controller`).
37
- prop :stimulus_controllers,
38
- _Array(_Union(String, Symbol, ::Vident2::Stimulus::Controller)),
39
- default: -> { [] }
40
- prop :stimulus_actions,
41
- _Array(_Union(String, Symbol, Array, Hash, ::Vident2::Stimulus::Action)),
42
- default: -> { [] }
43
- prop :stimulus_targets,
44
- _Array(_Union(String, Symbol, Array, ::Vident2::Stimulus::Target)),
45
- default: -> { [] }
46
- prop :stimulus_outlets,
47
- _Array(_Union(String, Symbol, Array, ::Vident2::Stimulus::Outlet)),
48
- default: -> { [] }
49
- prop :stimulus_outlet_host, _Nilable(::Vident2::Component)
50
- prop :stimulus_values,
51
- _Union(_Hash(Symbol, _Any), Array, ::Vident2::Stimulus::Value),
52
- default: -> { {} }
53
- prop :stimulus_params,
54
- _Union(_Hash(Symbol, _Any), Array, ::Vident2::Stimulus::Param),
55
- default: -> { {} }
56
- prop :stimulus_classes,
57
- _Union(_Hash(Symbol, _Any), Array, ::Vident2::Stimulus::ClassMap),
58
- default: -> { {} }
59
-
60
- # Eager inheritance: subclasses copy parent's frozen Declarations
61
- # (see `inherited` below).
62
- @__vident2_declarations = ::Vident2::Internals::Declarations.empty
63
- @__vident2_no_stimulus_controller = false
64
- end
65
-
66
- class_methods do
67
- def prop_names
68
- literal_properties.properties_index.keys.map(&:to_sym)
69
- end
70
-
71
- # `Admin::UserCardComponent` → `admin/user_card_component`.
72
- # Anonymous classes return a stable placeholder.
73
- def stimulus_identifier_path
74
- name&.underscore || "anonymous_component"
75
- end
76
-
77
- # The `data-controller` / root-class form, e.g. `admin--user-card-component`.
78
- def stimulus_identifier
79
- stimulus_identifier_path.split("/").map(&:dasherize).join("--")
80
- end
81
-
82
- def component_name
83
- @component_name ||= stimulus_identifier
84
- end
85
-
86
- # Frozen DSL aggregate (own + inherited). Always non-nil.
87
- def declarations
88
- @__vident2_declarations ||= ::Vident2::Internals::Declarations.empty
89
- end
90
-
91
- # Suppresses the implied controller. A `stimulus do` block with
92
- # entries after this call raises `DeclarationError`.
93
- def no_stimulus_controller
94
- if declarations.any?
95
- raise ::Vident2::DeclarationError,
96
- "#{name || "anonymous component"} called `no_stimulus_controller` after " \
97
- "`stimulus do` already recorded DSL entries. Declare `no_stimulus_controller` " \
98
- "before any `stimulus do` block."
99
- end
100
- @__vident2_no_stimulus_controller = true
101
- end
102
-
103
- def stimulus_controller?
104
- !@__vident2_no_stimulus_controller
105
- end
106
-
107
- # `stimulus do ... end` block receiver. Second+ calls append
108
- # (positional) or last-write-wins (keyed).
109
- def stimulus(&block)
110
- call_site = caller_locations(1, 1)&.first
111
- dsl = ::Vident2::Internals::DSL.new(caller_location: call_site)
112
- dsl.instance_eval(&block) if block
113
- fresh = dsl.to_declarations
114
-
115
- if !stimulus_controller? && fresh.any?
116
- location = call_site ? " at #{call_site.path}:#{call_site.lineno}" : ""
117
- raise ::Vident2::DeclarationError,
118
- "#{name || "anonymous component"} declared `no_stimulus_controller` but `stimulus do` emitted DSL entries#{location}. " \
119
- "A class with no implied controller cannot route DSL entries; drop the `stimulus do` block or remove `no_stimulus_controller`."
120
- end
121
-
122
- @__vident2_declarations = declarations.merge(fresh).freeze
123
- end
124
-
125
- # Component-scoped Stimulus event (Symbol, usable directly in `action` DSL).
126
- def stimulus_scoped_event(event)
127
- :"#{component_name}:#{event.to_s.camelize(:lower)}"
128
- end
129
-
130
- def stimulus_scoped_event_on_window(event)
131
- :"#{component_name}:#{event.to_s.camelize(:lower)}@window"
132
- end
133
-
134
- # @api private — called by the Ruby VM on subclass definition.
135
- def inherited(subclass)
136
- super
137
- subclass.instance_variable_set(:@__vident2_declarations, declarations)
138
- subclass.instance_variable_set(
139
- :@__vident2_no_stimulus_controller,
140
- instance_variable_get(:@__vident2_no_stimulus_controller) || false
141
- )
142
- end
143
- end
144
-
145
- def prop_names = self.class.prop_names
146
- def component_name = self.class.component_name
147
- def stimulus_identifier = self.class.stimulus_identifier
148
-
149
- private def default_controller_path = self.class.stimulus_identifier_path
150
-
151
- def stimulus_scoped_event(event) = self.class.stimulus_scoped_event(event)
152
- def stimulus_scoped_event_on_window(event) = self.class.stimulus_scoped_event_on_window(event)
153
-
154
- # Auto-id: `<component-name>-<stable-id>`. `.presence` is intentional
155
- # — blank string falls through to auto-generation.
156
- def id
157
- @id.presence || random_id
158
- end
159
-
160
- # Auto-generated id, independent of the `id:` prop. Useful for ARIA
161
- # references that need to be stable per-instance.
162
- def random_id
163
- @__vident2_auto_id ||= "#{component_name}-#{::Vident2::StableId.next_id_in_sequence}"
164
- end
165
-
166
- # Stable `[identifier, "#<id>"]` pair for connecting an outlet to
167
- # this instance.
168
- def outlet_id
169
- @outlet_id ||= [stimulus_identifier, "##{id}"]
170
- end
171
-
172
- # Fresh instance with current props merged with overrides.
173
- def clone(overrides = {})
174
- self.class.new(**to_h.merge(overrides))
175
- end
176
-
177
- # Custom format kept for tooling / specs that regex-match output.
178
- def inspect(klass_name = "Component")
179
- attr_text = to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(", ")
180
- "#<#{self.class.name}<Vident::#{klass_name}> #{attr_text}>"
181
- end
182
-
183
- # Memoised `root_element_attributes`: the user's override runs exactly
184
- # once (across Resolver + renderer reads).
185
- private def resolved_root_element_attributes
186
- return @__vident2_rea if defined?(@__vident2_rea)
187
- value = root_element_attributes
188
- @__vident2_rea = value.is_a?(Hash) ? value : {}
189
- end
190
-
191
- # User override: extra attrs for the root. `stimulus_*:` keys APPEND
192
- # into the Draft (same as props).
193
- def root_element_attributes = {}
194
-
195
- # User override: instance-level extra classes for the root (one tier
196
- # of ClassListBuilder's cascade). Return nil for no contribution.
197
- def root_element_classes
198
- nil
199
- end
200
-
201
- # User hook: runs after the Draft is built but before seal.
202
- def after_component_initialize
203
- end
204
-
205
- # Singular parsers return a pre-built value object; pre-built input
206
- # passes through unchanged.
207
- SINGULAR_PARSERS = {
208
- controllers: :stimulus_controller,
209
- actions: :stimulus_action,
210
- targets: :stimulus_target,
211
- outlets: :stimulus_outlet,
212
- values: :stimulus_value,
213
- params: :stimulus_param,
214
- class_maps: :stimulus_class
215
- }.freeze
216
-
217
- SINGULAR_PARSERS.each do |kind_name, method_name|
218
- kind = ::Vident2::Internals::Registry.fetch(kind_name)
219
- define_method(method_name) do |*args|
220
- return args.first if args.length == 1 && args.first.is_a?(kind.value_class)
221
- kind.value_class.parse(*args, implied: implied_controller, component_id: id)
222
- end
223
- end
224
-
225
- # Plural parsers return a Collection (exposes `#to_h` for `data:`
226
- # splatting). Inputs: Symbol / String / Array (= singular parser's
227
- # arg tuple) / Hash (keyed: one pair each) / pre-built Value or
228
- # Collection (pass-through).
229
- PLURAL_PARSERS = {
230
- controllers: :stimulus_controllers,
231
- actions: :stimulus_actions,
232
- targets: :stimulus_targets,
233
- outlets: :stimulus_outlets,
234
- values: :stimulus_values,
235
- params: :stimulus_params,
236
- class_maps: :stimulus_classes
237
- }.freeze
238
-
239
- PLURAL_PARSERS.each do |kind_name, method_name|
240
- kind = ::Vident2::Internals::Registry.fetch(kind_name)
241
- define_method(method_name) do |*args|
242
- return ::Vident2::Stimulus::Collection.new(kind: kind, items: []) if args.empty? || args.all?(&:nil?)
243
- return args.first if args.length == 1 && args.first.is_a?(::Vident2::Stimulus::Collection)
244
-
245
- items = []
246
- args.each do |arg|
247
- case arg
248
- when kind.value_class
249
- items << arg
250
- when ::Vident2::Stimulus::Collection
251
- items.concat(arg.items)
252
- when Hash
253
- if kind.keyed
254
- arg.each { |name, val| items << kind.value_class.parse(name, val, implied: implied_controller, component_id: id) }
255
- else
256
- items << kind.value_class.parse(arg, implied: implied_controller, component_id: id)
257
- end
258
- when Array
259
- items << kind.value_class.parse(*arg, implied: implied_controller, component_id: id)
260
- else
261
- items << kind.value_class.parse(arg, implied: implied_controller, component_id: id)
262
- end
263
- end
264
- ::Vident2::Stimulus::Collection.new(kind: kind, items: items)
265
- end
266
- end
267
-
268
- # Mutators. One call = one entry: Array input is the singular
269
- # parser's arg tuple, NOT splatted across multiple mutator calls.
270
- MUTATOR_METHODS = {
271
- controllers: :add_stimulus_controllers,
272
- actions: :add_stimulus_actions,
273
- targets: :add_stimulus_targets,
274
- outlets: :add_stimulus_outlets,
275
- values: :add_stimulus_values,
276
- params: :add_stimulus_params,
277
- class_maps: :add_stimulus_classes
278
- }.freeze
279
-
280
- # Kind name → `stimulus_<singular>` suffix (used by child_element).
281
- SINGULAR_NAMES = {
282
- controllers: :controller,
283
- actions: :action,
284
- targets: :target,
285
- outlets: :outlet,
286
- values: :value,
287
- params: :param,
288
- class_maps: :class
289
- }.freeze
290
-
291
- MUTATOR_METHODS.each do |kind_name, method_name|
292
- kind = ::Vident2::Internals::Registry.fetch(kind_name)
293
- define_method(method_name) do |input|
294
- raise_if_sealed!
295
- values = unwrap_mutator_input(kind, input)
296
- values.each { |v| @__vident2_draft.public_send(:"add_#{kind.name}", v) if v }
297
- self
298
- end
299
- end
300
-
301
- # SSR helper: resolved ClassMap entries matching `names` as a
302
- # space-joined String. Tailwind-merged if available. `""` on miss.
303
- def class_list_for_stimulus_classes(*names)
304
- resolve_stimulus_attributes_at_render_time
305
- plan = seal_draft
306
- maps = plan.class_maps
307
- return "" if maps.empty? || names.empty?
308
-
309
- result = ::Vident2::Internals::ClassListBuilder.call(
310
- stimulus_classes: maps,
311
- stimulus_class_names: names,
312
- tailwind_merger: tailwind_merger
313
- )
314
- result || ""
315
- end
316
-
317
- # Emit a child element with stimulus_* kwargs folded into data-*
318
- # attrs. Plural kwargs must be Enumerable. Adapter provides the tag
319
- # emission (`generate_child_element`).
320
- def child_element(
321
- tag_name,
322
- stimulus_controllers: nil,
323
- stimulus_targets: nil,
324
- stimulus_actions: nil,
325
- stimulus_outlets: nil,
326
- stimulus_values: nil,
327
- stimulus_params: nil,
328
- stimulus_classes: nil,
329
- stimulus_controller: nil,
330
- stimulus_target: nil,
331
- stimulus_action: nil,
332
- stimulus_outlet: nil,
333
- stimulus_value: nil,
334
- stimulus_param: nil,
335
- stimulus_class: nil,
336
- **options,
337
- &block
338
- )
339
- inputs = {
340
- controllers: [stimulus_controllers, stimulus_controller],
341
- actions: [stimulus_actions, stimulus_action],
342
- targets: [stimulus_targets, stimulus_target],
343
- outlets: [stimulus_outlets, stimulus_outlet],
344
- values: [stimulus_values, stimulus_value],
345
- params: [stimulus_params, stimulus_param],
346
- class_maps: [stimulus_classes, stimulus_class]
347
- }
348
-
349
- data_attrs = {}
350
- ::Vident2::Internals::Registry.each do |kind|
351
- plural, singular = inputs.fetch(kind.name)
352
- child_element_check_plural!(plural, singular, kind)
353
- coll = child_element_build_collection(kind, plural, singular)
354
- data_attrs.merge!(coll.to_h) unless coll.empty?
355
- end
356
-
357
- generate_child_element(tag_name, data_attrs, options, &block)
358
- end
359
-
360
- private
361
-
362
- # Literal callback after props are assigned. Builds the Draft with
363
- # STATIC entries; DSL procs defer to render (adapter's
364
- # `before_template` / `before_render` — `view_context` isn't wired yet).
365
- def after_initialize
366
- @__vident2_draft = ::Vident2::Internals::Resolver.call(
367
- self.class.declarations, self, phase: :static
368
- )
369
- @stimulus_outlet_host&.add_stimulus_outlets(self)
370
- after_component_initialize
371
- end
372
-
373
- public
374
-
375
- # Resolve DSL proc entries deferred at `after_initialize`. Called by
376
- # the adapter's `before_template` / `before_render`; `seal_draft` and
377
- # `class_list_for_stimulus_classes` call it as safety nets.
378
- #
379
- # Flag set before the guards so a sealed Draft can't trap us in a
380
- # loop where every subsequent call re-takes the sealed branch.
381
- def resolve_stimulus_attributes_at_render_time
382
- return if @__vident2_procs_resolved
383
- @__vident2_procs_resolved = true
384
- # Nil = test double. Sealed = someone consumed the Draft already.
385
- return if @__vident2_draft.nil? || @__vident2_draft.sealed?
386
- ::Vident2::Internals::Resolver.resolve_procs_into(
387
- @__vident2_draft, self.class.declarations, self
388
- )
389
- end
390
-
391
- private
392
-
393
- def implied_controller
394
- @__vident2_implied_controller ||= ::Vident2::Stimulus::Controller.new(
395
- path: self.class.stimulus_identifier_path,
396
- name: self.class.stimulus_identifier
397
- )
398
- end
399
-
400
- # Array input is ONE entry — V2 intentionally does not splat Arrays
401
- # across entries (mirrors the DSL's plural→singular forwarding).
402
- def unwrap_mutator_input(kind, input)
403
- return [] if input.nil?
404
- return [input] if input.is_a?(kind.value_class)
405
- return input.items if input.is_a?(::Vident2::Stimulus::Collection)
406
-
407
- if kind.keyed && input.is_a?(Hash)
408
- return input.map do |name, raw|
409
- kind.value_class.parse(name, raw, implied: implied_controller, component_id: id)
410
- end
411
- end
412
-
413
- args = input.is_a?(Array) ? input : [input]
414
- [kind.value_class.parse(*args, implied: implied_controller, component_id: id)]
415
- end
416
-
417
- def raise_if_sealed!
418
- return unless @__vident2_draft&.sealed?
419
- raise ::Vident2::StateError,
420
- "cannot modify stimulus attributes after rendering has begun"
421
- end
422
-
423
- # Seal Draft and memoise the Plan. Also a safety net for deferred
424
- # proc resolution when the adapter hook didn't fire.
425
- def seal_draft
426
- resolve_stimulus_attributes_at_render_time
427
- @__vident2_plan ||= @__vident2_draft.seal!
428
- end
429
-
430
- # Merged root-element attribute Hash for adapters. `overrides` come
431
- # from `root_element(**overrides)` and win on non-data keys.
432
- def build_root_element_attributes(overrides)
433
- plan = seal_draft
434
- data_attrs = ::Vident2::Internals::AttributeWriter.call(plan)
435
-
436
- extra = resolved_root_element_attributes
437
- extra_html_options = extra[:html_options] || {}
438
- extra_class = extra[:classes]
439
- extra_id = extra[:id]
440
- extra_data = extra_html_options[:data] || {}
441
-
442
- # data precedence (low→high): Plan fragments → attrs html_options[:data]
443
- # → instance html_options[:data] → overrides[:data].
444
- merged_data = data_attrs.dup
445
- merged_data.merge!(symbolize_keys(extra_data))
446
- merged_data.merge!(symbolize_keys(@html_options[:data] || {}))
447
- merged_data.merge!(symbolize_keys(overrides[:data] || {}))
448
-
449
- # 6-tier class-list cascade — see ClassListBuilder.
450
- class_list = ::Vident2::Internals::ClassListBuilder.call(
451
- component_name: component_name,
452
- root_element_classes: root_element_classes,
453
- root_element_attributes_classes: extra_class,
454
- root_element_html_class: overrides[:class],
455
- html_options_class: (@html_options[:class] || extra_html_options[:class]),
456
- classes_prop: @classes,
457
- tailwind_merger: tailwind_merger
458
- )
459
-
460
- merged = {}
461
- merged.merge!(extra_html_options.except(:data, :class))
462
- merged.merge!(@html_options.except(:data, :class))
463
- merged.merge!(overrides.except(:data, :class))
464
- merged[:class] = class_list if class_list
465
- merged[:data] = merged_data unless merged_data.empty?
466
- merged[:id] ||= extra_id || id
467
-
468
- merged
469
- end
470
-
471
- def symbolize_keys(hash)
472
- return {} unless hash.is_a?(Hash)
473
- hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
474
- end
475
-
476
- def root_element_tag_type
477
- tag = resolved_root_element_attributes[:element_tag] || @element_tag
478
- tag.presence&.to_sym || :div
479
- end
480
-
481
- # --- child_element helpers ------------------------------------------
482
-
483
- def child_element_check_plural!(plural, singular, kind)
484
- if plural && singular
485
- raise ArgumentError,
486
- "'stimulus_#{kind.plural_name}:' and 'stimulus_#{SINGULAR_NAMES.fetch(kind.name)}:' " \
487
- "are mutually exclusive — pass one or the other."
488
- end
489
- return if plural.nil?
490
- return if plural.is_a?(Enumerable) && !plural.is_a?(Hash)
491
- return if plural.is_a?(Hash) && kind.keyed
492
- raise ArgumentError,
493
- "'stimulus_#{kind.plural_name}:' must be an enumerable. " \
494
- "Did you mean 'stimulus_#{SINGULAR_NAMES.fetch(kind.name)}:'?"
495
- end
496
-
497
- # Exactly one of `plural` / `singular` is non-nil; guard above
498
- # rejects both-set.
499
- def child_element_build_collection(kind, plural, singular)
500
- plural_method = :"stimulus_#{kind.plural_name}"
501
- singular_method = :"stimulus_#{SINGULAR_NAMES.fetch(kind.name)}"
502
-
503
- if plural
504
- if kind.keyed && plural.is_a?(Hash)
505
- send(plural_method, plural)
506
- elsif plural.is_a?(Array)
507
- send(plural_method, *plural)
508
- else
509
- send(plural_method, *Array(plural))
510
- end
511
- elsif singular
512
- coll_items = [send(singular_method, *Array.wrap(singular))]
513
- ::Vident2::Stimulus::Collection.new(kind: kind, items: coll_items)
514
- else
515
- ::Vident2::Stimulus::Collection.new(kind: kind, items: [])
516
- end
517
- end
518
-
519
- public
520
-
521
- def root_element(**overrides, &block)
522
- raise NoMethodError, "subclass must implement root_element"
523
- end
524
-
525
- # Dispatches to the adapter-specific `root_element` on subclasses
526
- # (Phlex / ViewComponent). Keep as `def` not `alias_method` so Ruby's
527
- # dynamic dispatch finds the subclass override.
528
- def root(...)
529
- root_element(...)
530
- end
531
-
532
- # @api private — adapter override point. Phlex: Phlex tag DSL + invalid-
533
- # tag guard. VC: content_tag / tag.
534
- def generate_child_element(tag_name, stimulus_data_attributes, options, &block)
535
- raise NoMethodError, "adapter must implement generate_child_element"
536
- end
537
- end
538
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident2
4
- # Rails engine hook for Vident2. Mirrors `Vident::Engine`'s autoload
5
- # setup (both live under `lib/`) and registers the V2-specific
6
- # acronym inflections Zeitwerk needs to load `Vident2::Internals::DSL`
7
- # and `Vident2::Phlex::HTML` from their respective files.
8
- class Engine < ::Rails::Engine
9
- config.before_initialize do
10
- Rails.autoloaders.each do |autoloader|
11
- autoloader.inflector.inflect(
12
- "dsl" => "DSL",
13
- "html" => "HTML"
14
- )
15
- end
16
- end
17
- end
18
- end
data/lib/vident2/error.rb DELETED
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident2
4
- # Root of the Vident2 error hierarchy. Every gem-raised exception
5
- # inherits from this; consumers can rescue by category.
6
- class Error < StandardError; end
7
-
8
- # Raised at class-definition time when a `stimulus do` block is
9
- # structurally incompatible with the class (e.g. a `no_stimulus_controller`
10
- # class emitting DSL entries). Carries a caller location pointing at
11
- # the offending `stimulus do` call.
12
- class DeclarationError < Error; end
13
-
14
- # Raised when a value-class parser cannot make sense of its arguments.
15
- # Subclass of DeclarationError because most parse failures originate
16
- # from DSL input recorded at class load.
17
- class ParseError < DeclarationError; end
18
-
19
- # Raised when a proc evaluated during render resolution fails or
20
- # returns an unusable shape.
21
- class RenderError < Error; end
22
-
23
- # Raised when a mutator (e.g. `add_stimulus_actions`) is invoked on a
24
- # sealed Draft.
25
- class StateError < Error; end
26
-
27
- # Raised for misconfiguration at the gem or host level (e.g. unknown
28
- # StableId strategy).
29
- class ConfigurationError < Error; end
30
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "registry"
4
-
5
- module Vident2
6
- module Internals
7
- # @api private
8
- # Pure: `Plan -> Hash{Symbol => String}` of `data-*` fragments.
9
- # Delegates per-kind combining (space-join, grouped-by-controller,
10
- # one-per-key) to each value class's `.to_data_hash`.
11
- module AttributeWriter
12
- module_function
13
-
14
- def call(plan)
15
- Registry::KINDS.each_with_object({}) do |kind, acc|
16
- fragment = kind.value_class.to_data_hash(plan.public_send(kind.name))
17
- acc.merge!(fragment)
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident2
4
- module Internals
5
- # @api private
6
- # One unresolved DSL entry. `args` is the raw argument tuple passed
7
- # to the DSL primitive; the Resolver parses it into a Stimulus value
8
- # object at instance init. `when_proc` (optional) is a `-> { ... }`
9
- # filter evaluated in the component binding; `meta` is a free-form
10
- # Hash for options like `from_prop: true` the parser needs to see.
11
- Declaration = Data.define(:args, :when_proc, :meta) do
12
- def self.of(*args, when_proc: nil, **meta)
13
- new(args: args.freeze, when_proc: when_proc, meta: meta.freeze)
14
- end
15
- end
16
- end
17
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "registry"
4
-
5
- module Vident2
6
- module Internals
7
- # @api private
8
- # Frozen snapshot produced by `Draft#seal!`. One field per Registry
9
- # kind, each an Array<Stimulus::*>.
10
- Plan = Data.define(*Registry.names)
11
- end
12
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../stimulus/controller"
4
- require_relative "../stimulus/action"
5
- require_relative "../stimulus/target"
6
- require_relative "../stimulus/outlet"
7
- require_relative "../stimulus/value"
8
- require_relative "../stimulus/param"
9
- require_relative "../stimulus/class_map"
10
-
11
- module Vident2
12
- # @api private — consumed by the DSL, Resolver, Draft, Plan,
13
- # AttributeWriter, and Capabilities::StimulusMutation. Not a public
14
- # extension surface; extensions monkeypatch at their own risk.
15
- module Internals
16
- module Registry
17
- # Per-kind metadata. `name` is the canonical internal key;
18
- # `plural_name` / `singular_name` drive DSL method names (e.g.
19
- # `stimulus_classes` / `stimulus_class` for `class_maps`); `keyed`
20
- # distinguishes hash-shaped kinds (values, params, class_maps,
21
- # outlets) from positional ones (controllers, actions, targets).
22
- Kind = Data.define(:name, :plural_name, :singular_name, :value_class, :keyed)
23
-
24
- KINDS = [
25
- Kind.new(:controllers, :controllers, :controller, Vident2::Stimulus::Controller, false),
26
- Kind.new(:actions, :actions, :action, Vident2::Stimulus::Action, false),
27
- Kind.new(:targets, :targets, :target, Vident2::Stimulus::Target, false),
28
- Kind.new(:outlets, :outlets, :outlet, Vident2::Stimulus::Outlet, true),
29
- Kind.new(:values, :values, :value, Vident2::Stimulus::Value, true),
30
- Kind.new(:params, :params, :param, Vident2::Stimulus::Param, true),
31
- Kind.new(:class_maps, :classes, :class, Vident2::Stimulus::ClassMap, true)
32
- ].freeze
33
-
34
- BY_NAME = KINDS.to_h { |k| [k.name, k] }.freeze
35
-
36
- def self.fetch(name) = BY_NAME.fetch(name)
37
- def self.each(&block) = KINDS.each(&block)
38
- def self.names = BY_NAME.keys
39
- end
40
- end
41
- end