vident 1.0.2 → 2.0.0

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 +43 -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 +44 -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
@@ -72,7 +72,7 @@ Public instance methods:
72
72
  `:element_tag` (Symbol), `:html_options` (Hash), `:id` (String), `:classes`
73
73
  (String | Array), and any of the seven `stimulus_<plural>:` / `stimulus_<singular>:`
74
74
  keys documented in section 5.
75
- - `clone(overrides = {})` — returns a new instance, `self.class.new(**to_h.merge(**overrides))`.
75
+ - `with(overrides = {})` — returns a new instance, `self.class.new(**to_h.merge(overrides))`. `clone(overrides = {})` is a backward-compat alias.
76
76
  - `inspect(klass_name = "Component")` — formatted debug string with every prop.
77
77
  - `id` — `String`, auto-generated from `StableId` if `@id` was nil. The generated form
78
78
  is `"#{component_name}-#{StableId.next_id_in_sequence}"`.
@@ -96,18 +96,36 @@ From `Vident::Component` (`lib/vident/component.rb`):
96
96
  | `classes` | `_Union(String, _Array(String))` | `[]` | Appended on top of all other class sources. |
97
97
  | `html_options` | `Hash` | `{}` | Merged onto root; highest class-source precedence. |
98
98
 
99
- From `Vident::StimulusComponent` (`lib/vident/stimulus_component.rb`):
99
+ From `Vident::Component` via the `StimulusDeclaring` / `StimulusParsing` capability mixins:
100
100
 
101
- | Prop | Type | Default |
102
- | ----------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------ |
103
- | `stimulus_controllers` | `_Array(_Union(String, Symbol, StimulusController, StimulusControllerCollection))` | `[default_controller_path]` unless `no_stimulus_controller`, else `[]` |
104
- | `stimulus_actions` | `_Array(_Union(String, Symbol, Array, Hash, StimulusAction, StimulusAction::Descriptor, StimulusActionCollection))` | `[]` |
105
- | `stimulus_targets` | `_Array(_Union(String, Symbol, Array, Hash, StimulusTarget, StimulusTargetCollection))` | `[]` |
106
- | `stimulus_outlets` | `_Array(_Union(String, Symbol, StimulusOutlet, StimulusOutletCollection))` | `[]` |
107
- | `stimulus_outlet_host` | `_Nilable(Vident::Component)` | `nil` |
108
- | `stimulus_values` | `_Union(_Hash(Symbol, _Any), Array, StimulusValue, StimulusValueCollection)` | `{}` |
109
- | `stimulus_params` | `_Union(_Hash(Symbol, _Any), Array, StimulusParam, StimulusParamCollection)` | `{}` |
110
- | `stimulus_classes` | `_Union(_Hash(Symbol, String), Array, StimulusClass, StimulusClassCollection)` | `{}` |
101
+ | Prop | Type | Default |
102
+ | ----------------------- | --------------------------------- | ------------------------------------ |
103
+ | `stimulus_controllers` | `Vident::Types::StimulusControllers` | `[default_controller_path]` unless `no_stimulus_controller`, else `[]` |
104
+ | `stimulus_actions` | `Vident::Types::StimulusActions` | `[]` |
105
+ | `stimulus_targets` | `Vident::Types::StimulusTargets` | `[]` |
106
+ | `stimulus_outlets` | `Vident::Types::StimulusOutlets` | `[]` |
107
+ | `stimulus_outlet_host` | `_Nilable(Vident::Component)` | `nil` |
108
+ | `stimulus_values` | `Vident::Types::StimulusValues` | `{}` |
109
+ | `stimulus_params` | `Vident::Types::StimulusParams` | `{}` |
110
+ | `stimulus_classes` | `Vident::Types::StimulusClasses` | `{}` |
111
+
112
+ `Vident::Types::*` are the canonical Literal type unions for each prop kind (file: `lib/vident/types.rb`). The unions are:
113
+
114
+ - `StimulusControllers` → `_Array(_Union(String, Symbol, Vident::Stimulus::Controller))`
115
+ - `StimulusActions` → `_Array(_Union(String, Symbol, Array, Hash, Vident::Stimulus::Action))`
116
+ - `StimulusTargets` → `_Array(_Union(String, Symbol, Array, Vident::Stimulus::Target))`
117
+ - `StimulusOutlets` → `_Array(_Union(String, Symbol, Array, Vident::Stimulus::Outlet))`
118
+ - `StimulusValues` → `_Union(_Hash(Symbol, _Any), Array, Vident::Stimulus::Value)`
119
+ - `StimulusParams` → `_Union(_Hash(Symbol, _Any), Array, Vident::Stimulus::Param)`
120
+ - `StimulusClasses` → `_Union(_Hash(Symbol, _Any), Array, Vident::Stimulus::ClassMap)`
121
+
122
+ Exposed publicly so user components can reuse them when adding matching props:
123
+
124
+ ```ruby
125
+ class MyComponent < Vident::ViewComponent::Base
126
+ prop :extra_actions, Vident::Types::StimulusActions, default: -> { [] }
127
+ end
128
+ ```
111
129
 
112
130
  ---
113
131
 
@@ -120,9 +138,12 @@ All of these live on `Vident::Component`'s class body (via included modules).
120
138
  or an immediate value; callable is required when the default is non-frozen (hash, array).
121
139
  - `no_stimulus_controller` — sets a class ivar that drops the implied controller from
122
140
  the `stimulus_controllers` default. Use when the component is purely presentational
123
- and needs no paired `_controller.js`.
141
+ and needs no paired `_controller.js`. Inherited by subclasses.
142
+ - `has_stimulus_controller` — the inverse: re-enables the implied controller on a
143
+ subclass whose parent declared `no_stimulus_controller`. Idempotent; order relative
144
+ to `stimulus do` blocks does not matter.
124
145
  - `stimulus_controller?` — `Boolean`, `true` by default; becomes `false` after a
125
- `no_stimulus_controller` declaration.
146
+ `no_stimulus_controller` declaration and `true` again after `has_stimulus_controller`.
126
147
  - `stimulus_identifier_path` — the `name.underscore` of the class (e.g.
127
148
  `"dashboard/release_card_component"`). Falls back to `"anonymous_component"` for
128
149
  anonymous classes.
@@ -136,84 +157,149 @@ All of these live on `Vident::Component`'s class body (via included modules).
136
157
  `:"foo-component:dataReady"`. Also an instance method.
137
158
  - `stimulus_scoped_event_on_window(event)` — same, with `@window` suffix. Also an
138
159
  instance method.
139
- - `stimulus(&block)` — the DSL entry point. Opens a `Vident::StimulusBuilder` block
160
+ - `stimulus(&block)` — the DSL entry point. Opens a `Vident::Internals::DSL` block
140
161
  evaluator. See section 3.
141
162
 
142
163
  Not intended for application code:
143
164
 
144
- - `stimulus_dsl_attributes(component_instance)` — returns the DSL's emitted attribute
145
- hash for a specific instance (so procs resolve against it).
146
- - `stimulus_dsl_builder` — the builder accessor; `protected`, used only by inheritance
147
- merging.
165
+ - `declarations` — frozen `Vident::Internals::Declarations` aggregate (own + inherited);
166
+ `protected`, consumed by the Resolver at render time.
148
167
 
149
168
  ---
150
169
 
151
170
  ## 3. `stimulus do ... end` block
152
171
 
153
- Evaluated by `Vident::StimulusBuilder` (`lib/vident/stimulus_builder.rb`). Multiple
172
+ Evaluated by `Vident::Internals::DSL` (`lib/vident/internals/dsl.rb`). Multiple
154
173
  `stimulus do` blocks on the same class accumulate. A subclass's blocks are merged with
155
174
  every parent's blocks on first access (subclass entries appended to positional kinds;
156
175
  subclass wins on conflicts for keyed kinds).
157
176
 
158
- Every DSL method returns `self` so calls chain but there's no real reason to chain
159
- inside a `do ... end` block. All methods accept procs anywhere a value is expected;
160
- procs are evaluated via `instance_exec` on the component instance at render time.
177
+ Every DSL method returns `self` (for the singular primitives `action`/`target`, the
178
+ fluent **builder** is returned instead chain methods also return self). All DSL
179
+ entries may use a `Proc` anywhere a value is expected; procs are evaluated via
180
+ `instance_exec` on the component instance at render time (or at `after_initialize`
181
+ for purely static entries).
161
182
 
162
183
  ### Methods on the builder
163
184
 
164
- - `actions(*entries)` positional. Each entry is one of:
185
+ **Controllers.** Primary form is the singular `controller`; plural `controllers`
186
+ accumulates paths without the `as:` alias.
187
+
188
+ - `controller(path, as: alias_sym = nil)` — declare a cross-controller path on
189
+ the root element. Optional `as:` registers an alias looked up by
190
+ `action(...).on_controller(alias_sym)` / `on_controller: alias_sym`. Paths
191
+ may be `String` (`"admin/users"`) or `Symbol` (`:admin_users`).
192
+ - `controllers(*paths)` — one entry per path. Array entries splat into the
193
+ singular parser (so `[path, as: sym]` tuples work when building
194
+ programmatically).
195
+ - `no_stimulus_controller` — class-level, **not** inside the block. Suppresses
196
+ the implied controller. Raises `Vident::DeclarationError` if any DSL entries
197
+ were subsequently added.
198
+
199
+ **Actions.** Primary form is the singular `action(*args, **meta)` which returns
200
+ an `Internals::ActionBuilder`. Chain methods pre-applied via kwargs are equivalent
201
+ to calling the setters explicitly.
202
+
203
+ - `action(*args, **meta) -> ActionBuilder` — builder state:
204
+ - Positional `*args` shapes (`base_descriptor` pattern-matches):
205
+ - `(Symbol)` → method on implied (no event)
206
+ - `(Symbol, Symbol)` → `(event, method)` on implied
207
+ - `(Symbol, String, Symbol)` → `(event, controller_path, method)`
208
+ - `(Hash)` → full descriptor (`:method`, `:event`, `:controller`, `:options`, `:keyboard`, `:window`)
209
+ - Kwargs `**meta` (equivalent to the fluent chain methods):
210
+ - `on:` (Symbol/String) → event
211
+ - `call_method:` (Symbol/String) → override the method name
212
+ - `modifier:` (Symbol or Array) → Stimulus options whitelist (see §4.2)
213
+ - `keyboard:` (String) → `keydown.<key>` filter suffix
214
+ - `window:` (Boolean) → `@window` suffix
215
+ - `on_controller:` (Symbol) → resolve against a `controller ..., as: sym` alias
216
+ - `when:` (Proc / callable) → render-time predicate; `false`/`nil` drops the entry
217
+ - Unknown kwargs raise `ArgumentError`.
218
+ - Chain methods on the returned builder: `.on(event)`, `.call_method(name)`,
219
+ `.modifier(*opts)`, `.keyboard(str)`, `.window`, `.on_controller(sym)`,
220
+ `.when(callable = nil, &block)`. Each returns the builder.
221
+ - `actions(*entries)` — legacy plural form, still accepted. Each entry is one of:
165
222
  - `Symbol` → `implied#<jsSymbol>`
166
223
  - `[Symbol, Symbol]` → `<event>-><implied>#<jsMethod>`
167
224
  - `[Symbol, String, Symbol]` → `<event>-><stimulized-path>#<jsMethod>`
168
- - `String` containing `#` → parsed literally (pass-through); `event->ctrl#method`
169
- or `ctrl#method` supported.
170
- - `Hash` (desugared to a `Descriptor`) — keys: `:method` (required), `:event`,
171
- `:controller`, `:options` (`Array<Symbol>`), `:keyboard` (`String`),
172
- `:window` (`Boolean`). See section 4.2 for the `options:` whitelist.
173
- - `Vident::StimulusAction::Descriptor` typed equivalent of the Hash form.
174
- - `Proc` evaluated at render time; `nil` / `false` return drops the entry.
175
- - `targets(*entries)` positional. Each entry is one of:
225
+ - `String` containing `#` → parsed literally (pass-through).
226
+ - `Hash` descriptor keys as above.
227
+ - `Proc` evaluated at render time; `nil`/`false` drops.
228
+
229
+ **Alias resolution.** When an action descriptor's `:controller` is a `Symbol`, the
230
+ resolver looks it up in the class's declared alias map (`controller X, as: sym`
231
+ entries) and substitutes the full path before parsing. Unknown alias
232
+ `Vident::DeclarationError`. Alias resolution also runs on runtime inputs
233
+ (`stimulus_actions:` prop, `root_element_attributes[:stimulus_actions]`) that
234
+ carry a Hash with a `Symbol` `:controller`.
235
+
236
+ **Targets.** Singular `target` returns a `TargetBuilder` whose only chain method
237
+ is `.when`; plural `targets` accepts the same positional shapes as before.
238
+
239
+ - `target(*args) -> TargetBuilder` — chain `.when(callable = nil, &block)` for
240
+ conditional inclusion; without a chain, the builder passes `*args` through.
241
+ - `targets(*entries)` — each entry is one of:
176
242
  - `Symbol` → target on the implied controller
177
243
  - `String` → pass-through target name
178
244
  - `[String, Symbol]` → target on the named cross-controller
179
- - `Proc` — evaluated at render time; `nil` drops the entry.
180
- - `values(**kvs)` — keyed. Values may be `String`/`Number`/`Boolean` (stringified),
181
- `Array`/`Hash` (JSON-serialised), `Vident::StimulusNull` (emits literal `"null"`),
182
- or a `Proc` resolving to any of the above. A resolved `nil` omits the attribute.
183
- - `params(**kvs)` — keyed. Same serialisation rules as `values`.
184
- - `classes(**kvs)` — keyed. Value is `String` or `Array(String)`; array joined with
185
- single space. A `Proc` may resolve to either. A resolved `nil` omits the attribute.
186
- - `outlets(positional_hash = nil, **kvs)` — keyed. Value is a `String` CSS selector.
187
- `outlets({"admin--users" => ".sel"})` accepts a positional Hash so identifiers
188
- containing `--` (not valid Ruby kwarg keys) work. Procs are **not** supported here
189
- the builder skips proc resolution for outlets; pass cross-controller outlets via
190
- the `stimulus_outlets:` prop or `root_element_attributes` instead.
191
- - `values_from_props(*prop_names)` keyed, sidecar to `values`. Mirrors each prop's
192
- current `@ivar` value at render time. Prop names are Symbols.
193
-
194
- No `controllers` method exists in the DSL. Controllers are set via the `stimulus_controllers:`
195
- prop, `root_element_attributes[:stimulus_controllers]`, or `no_stimulus_controller`.
245
+ - `Proc` — `nil` return drops the entry.
246
+
247
+ **Keyed primitives.** `values`, `params`, `classes`, `outlets` keep the plural
248
+ kwargs form and add singular `value`/`param`/`class_map`/`outlet` that take
249
+ `(name, *args, **meta)`:
250
+
251
+ - `values(**kvs)` / `value(name, *args, **meta)` keyed. Values may be
252
+ `String`/`Number`/`Boolean` (stringified), `Array`/`Hash` (JSON-serialised),
253
+ `Vident::StimulusNull` (emits literal `"null"`), or a `Proc` resolving to
254
+ any of the above. A resolved `nil` omits the attribute. Singular supports
255
+ `value :count, static: 0` and `value :clicked_count, from_prop: true` meta
256
+ forms.
257
+ - `params(**kvs)` / `param(name, *args, **meta)` same serialisation rules.
258
+ - `classes(**kvs)` / `class_map(name, *args, **meta)` value is `String` or
259
+ `Array(String)`; array joined with single space.
260
+ - `outlets(positional_hash = nil, **kvs)` / `outlet(name, *args, **meta)`
261
+ value is a `String` CSS selector, a `Proc` returning one, or a pre-built
262
+ outlet value object. `outlets({"admin--users" => ".sel"})` accepts a
263
+ positional Hash so identifiers containing `--` (not valid Ruby kwarg keys)
264
+ work.
265
+ - `values_from_props(*prop_names)` — keyed, sidecar to `values`. Mirrors each
266
+ prop's current `@ivar` value at render time. Prop names are Symbols.
196
267
 
197
268
  ### What the builder emits
198
269
 
199
- `to_attributes(component_instance)` returns a Hash keyed by `:stimulus_actions`,
200
- `:stimulus_targets`, `:stimulus_values`, `:stimulus_params`, `:stimulus_classes`,
201
- `:stimulus_outlets`, plus `:stimulus_values_from_props` (an Array of prop-name Symbols)
202
- if `values_from_props` was used. Only primitives with entries are included.
270
+ `to_declarations` (called on the `Vident::Internals::DSL` instance when the block
271
+ closes) returns a frozen `Vident::Internals::Declarations` struct. The struct is a
272
+ `Data.define(...)` value object with these fields all frozen arrays:
273
+
274
+ | Field | Content |
275
+ | ------------------ | -------------------------------------------------------------------------- |
276
+ | `controllers` | `Array` of `Declaration` entries (each wraps a path + optional `as:` alias). |
277
+ | `actions` | `Array` of `Declaration` entries, one per `action(...)` call. |
278
+ | `targets` | `Array` of `Declaration` entries, one per `target(...)` call. |
279
+ | `outlets` | `Array` of `[key, Declaration]` pairs (keyed; last-write-wins on same key).|
280
+ | `values` | `Array` of `[key, Declaration]` pairs. |
281
+ | `params` | `Array` of `[key, Declaration]` pairs. |
282
+ | `class_maps` | `Array` of `[key, Declaration]` pairs. |
283
+ | `values_from_props`| `Array(Symbol)` — prop names listed via `values_from_props`. |
284
+
285
+ The struct supports `merge(other)` (subclass block merged over superclass) and
286
+ `any?`. Entries remain as raw `Declaration` tuples — parsing into
287
+ `Vident::Stimulus::*` value objects is deferred to the Resolver at render time.
288
+ Application code does not call `to_declarations` directly.
203
289
 
204
290
  ---
205
291
 
206
- ## 4. Instance-level Stimulus helpers (`Vident::StimulusAttributes`)
292
+ ## 4. Instance-level Stimulus helpers (`Vident::Capabilities::StimulusParsing`)
207
293
 
208
- Included into every component via `StimulusComponent`. File:
209
- `lib/vident/stimulus_attributes.rb`.
294
+ Included into every component via `Vident::Component`. File:
295
+ `lib/vident/capabilities/stimulus_parsing.rb`.
210
296
 
211
297
  ### 4.1 Plural parsers `stimulus_<plural>(*args)`
212
298
 
213
299
  Seven methods: `stimulus_controllers`, `stimulus_actions`, `stimulus_targets`,
214
300
  `stimulus_outlets`, `stimulus_values`, `stimulus_params`, `stimulus_classes`.
215
301
 
216
- Each returns a collection object (`StimulusActionCollection`, etc.) whose `#to_h`
302
+ Each returns a collection object (`Vident::Stimulus::Collection`, etc.) whose `#to_h`
217
303
  serialises to a `Hash` of `data-*` keys → values. Arg handling per input:
218
304
 
219
305
  - no args or all-blank → empty collection
@@ -227,12 +313,12 @@ serialises to a `Hash` of `data-*` keys → values. Arg handling per input:
227
313
 
228
314
  ### 4.2 Singular builders `stimulus_<singular>(*args)`
229
315
 
230
- Each singular builder accepts the set of argument shapes its `parse_arguments`
231
- implementation supports. Raises `ArgumentError` on unsupported shape or arity.
316
+ Each singular builder delegates to the corresponding value class's `.parse(*args, implied:, component_id:)`
317
+ class method. Raises `ArgumentError` (or `Vident::ParseError`) on unsupported shape or arity.
232
318
 
233
319
  - `stimulus_controller(*)` — 0 or 1 arg. 0 args returns the implied controller; 1 arg
234
320
  is a controller path `String`/`Symbol`.
235
- - `stimulus_action(*)` — 1/2/3 args. See `StimulusAction::parse_arguments` for all
321
+ - `stimulus_action(*)` — 1/2/3 args. See `Vident::Stimulus::Action.parse` for all
236
322
  accepted forms. `options:` whitelist (raises otherwise):
237
323
  `[:once, :prevent, :stop, :passive, :"!passive", :capture, :self]`.
238
324
  - `stimulus_target(*)` — 1 or 2 args. `(Symbol)` / `(String)` → implied controller;
@@ -258,26 +344,27 @@ Seven methods, one per primitive. Merge new attributes into the per-kind collect
258
344
  ivar (e.g. `@stimulus_actions_collection`). Typical use: inside
259
345
  `after_component_initialize`, compute runtime attributes and add them.
260
346
 
261
- **Splat asymmetry vs DSL.** The DSL's `actions [:click, :handle]` treats the Array as
262
- one action descriptor (event + method). The mutator `add_stimulus_actions([:click, :handle])`
263
- splats the Array and treats it as two separate symbol actions. To pass an
264
- Array-shaped single action through the mutator, wrap with a pre-built value
265
- (`stimulus_action(:click, :handle)`) or double-wrap (`[[:click, :handle]]`).
347
+ **Array input is one entry.** `add_stimulus_actions([:click, :handle])` treats the
348
+ Array as *one* action descriptor (event + method pair), matching the DSL's
349
+ `actions [:click, :handle]` semantics. The V1 splat asymmetry where the mutator
350
+ treated the Array as two separate symbol actions was fixed in V2. To pass a
351
+ pre-built action object, construct it first:
352
+ `add_stimulus_actions(stimulus_action(:click, :handle))`.
266
353
 
267
- ### 4.4 Value serialisation (`StimulusAttributeBase#serialize_value`)
354
+ ### 4.4 Value serialisation
268
355
 
269
356
  `Array` and `Hash` → JSON. Everything else → `to_s`. `Vident::StimulusNull.to_s`
270
357
  returns the literal string `"null"`. A `nil` reaches the DSL/prop layer and
271
- is dropped by `StimulusBuilder#resolve_hash_filtering_nil` before serialisation, so
272
- the data attribute is omitted (not emitted as empty).
358
+ is dropped by the Resolver before serialisation, so the data attribute is omitted
359
+ (not emitted as empty).
273
360
 
274
361
  ### 4.5 Name-shaping helpers
275
362
 
276
- - `StimulusAttributeBase.stimulize_path(path)` `"admin/users"` `"admin--users"`;
277
- each path segment is `dasherize`d and segments joined with `--`.
278
- - `StimulusAttributeBase.js_name(name)` — `camelize(:lower)`; `:my_thing` → `"myThing"`.
363
+ `Vident::Stimulus::Naming` is a `module_function` module — call its methods directly:
279
364
 
280
- Both also available as private instance methods on any `StimulusAttributeBase` subclass.
365
+ - `Vident::Stimulus::Naming.stimulize_path(path)` `"admin/users"` `"admin--users"`;
366
+ each path segment is `dasherize`d and segments joined with `--`.
367
+ - `Vident::Stimulus::Naming.js_name(name)` — `camelize(:lower)`; `:my_thing` → `"myThing"`.
281
368
 
282
369
  ### 4.6 Scoped events
283
370
 
@@ -287,6 +374,48 @@ Both also available as private instance methods on any `StimulusAttributeBase` s
287
374
  - Class method `stimulus_scoped_event_on_window(event)` — same with `@window` suffix.
288
375
  - Both also exist as instance methods that delegate to the class method.
289
376
 
377
+ ### 4.7 Class-level builders
378
+
379
+ Class methods parallel to the instance singulars, useful when you need a
380
+ Stimulus value object without a component instance (Turbo-Stream partials,
381
+ JSON responses, system-test selectors).
382
+
383
+ - `MyComponent.stimulus_controller` — no args; returns the implied `Vident::Stimulus::Controller`.
384
+ - `MyComponent.stimulus_target(Symbol|String)` — returns `Vident::Stimulus::Target`.
385
+ - `MyComponent.stimulus_action(*args)` — same grammar as the instance singular, but cross-controller forms (`[String, Symbol]`, `[Symbol, String, Symbol]`) raise `Vident::ParseError`.
386
+ - `MyComponent.stimulus_value(name, value)` — two-arg form only; the three-arg cross-controller form raises.
387
+ - `MyComponent.stimulus_param(name, value)` — same constraint.
388
+ - `MyComponent.stimulus_class(name, css)` — same constraint.
389
+ - `MyComponent.stimulus_outlet(name, selector)` — **selector required**; single-arg auto-selector form raises `Vident::ParseError` (no `component_id` at class level). For cross-controller outlets, call `Vident::Stimulus::Outlet.parse(...)` directly.
390
+
391
+ Class-level output matches instance-level where both apply:
392
+
393
+ ```ruby
394
+ ButtonComponent.stimulus_target(:submit).to_h ==
395
+ ButtonComponent.new.stimulus_target(:submit).to_h # => true
396
+ ```
397
+
398
+ The implied controller is memoised per-class on the singleton; subclasses inherit the identifier path but get their own memo.
399
+
400
+ ### 4.8 Root-element composition helpers
401
+
402
+ Two instance methods that return what `root_element(...)` would emit — for components that render their root tag via a third-party helper (e.g. `InlineSvg::inline_svg_tag`).
403
+
404
+ - `root_element_class_list(extra_classes = nil)` — returns a `String`. Applies the full 6-tier class cascade (`component_name`, `root_element_classes`, `root_element_attributes[:classes]`, `html_options[:class]`, `@classes` prop, then `extra_classes`) plus Tailwind-merging.
405
+ - `root_element_data_attributes` — returns a `Hash` with Symbol keys. Seals the Draft into a Plan (idempotent) and runs the AttributeWriter, yielding the same `data-controller` / `data-action` / `data-*-target` / etc. hash that `root_element(...)` would emit.
406
+
407
+ ```ruby
408
+ def svg_attributes
409
+ {
410
+ id: @id,
411
+ class: root_element_class_list,
412
+ data: root_element_data_attributes
413
+ }
414
+ end
415
+ ```
416
+
417
+ Both honour `no_stimulus_controller` (no `data-controller` in the hash; component-identifier CSS class still emitted, matching `root_element`).
418
+
290
419
  ---
291
420
 
292
421
  ## 5. `root_element_attributes` accepted keys
@@ -343,44 +472,44 @@ def child_element(tag_name,
343
472
 
344
473
  ---
345
474
 
346
- ## 7. `Vident::StimulusBuilder` primitives
475
+ ## 7. `Vident::Internals::DSL` primitives
347
476
 
348
477
  For use in advanced cases (passing typed descriptors across components, building
349
- reusable shared helpers). File: `lib/vident/stimulus_action.rb`.
478
+ reusable shared helpers). Value classes live under `lib/vident/stimulus/`.
350
479
 
351
- ### `Vident::StimulusAction::Descriptor`
480
+ ### Hash descriptor form
352
481
 
353
- A `::Literal::Data` value object with the same shape as the Hash form accepted by
354
- `actions`:
482
+ There is no separate `Descriptor` class in V2. The Hash form accepted by `actions`
483
+ (and `stimulus_actions:`) is parsed directly into `Vident::Stimulus::Action`. Accepted keys:
355
484
 
356
- | Prop | Type | Default |
485
+ | Key | Type | Default |
357
486
  | ------------- | ------------------------------------- | ------- |
358
- | `method` | `_Union(Symbol, String)` | |
359
- | `event` | `_Nilable(_Union(Symbol, String))` | `nil` |
360
- | `controller` | `_Nilable(String)` | `nil` |
361
- | `options` | `_Array(Symbol)` | `[]` |
362
- | `keyboard` | `_Nilable(String)` | `nil` |
363
- | `window` | `_Boolean` | `false` |
487
+ | `method:` | `Symbol \| String` | required |
488
+ | `event:` | `Symbol \| String \| nil` | `nil` |
489
+ | `controller:` | `String \| nil` | `nil` |
490
+ | `options:` | `Array(Symbol)` — see §4.2 whitelist | `[]` |
491
+ | `keyboard:` | `String \| nil` | `nil` |
492
+ | `window:` | `Boolean` | `false` |
364
493
 
365
494
  ### `Vident::StimulusNull`
366
495
 
367
496
  Frozen singleton object. `inspect` → `"Vident::StimulusNull"`; `to_s` → `"null"`.
368
497
  See SKILL.md §1.4 for the usage contract.
369
498
 
370
- ### Collection classes
499
+ ### Collection class
371
500
 
372
- Each primitive has a `StimulusXCollection < StimulusCollectionBase`:
501
+ All primitive kinds share one parametric class: `Vident::Stimulus::Collection`,
502
+ parametrised on a `Kind` record from `Vident::Internals::Registry`.
373
503
 
374
- - Base methods: `<<(item)`, `to_a`, `to_h` (abstract; each subclass implements),
375
- `empty?`, `any?`, `merge(*others)`, `self.merge(*collections)`.
376
- - `StimulusActionCollection#to_h` `{action: "…"}` with entries joined by space.
377
- - `StimulusControllerCollection#to_h` → `{controller: "…"}` with non-empty entries
378
- joined by space.
379
- - `StimulusTargetCollection#to_h` → one key per controller-target attribute;
380
- multiple targets on the same controller are joined with a single space.
381
- - `StimulusValueCollection`, `StimulusParamCollection`, `StimulusClassCollection` →
382
- merged per-data-attribute hash.
383
- - `StimulusOutletCollection` → same.
504
+ - Methods: `each`, `to_a`, `size`, `length`, `empty?`, `any?`, `to_h`, `to_hash`,
505
+ `merge(other)` (single same-kind Collection; raises `ArgumentError` on mismatch).
506
+ - `#to_h` shape per kind:
507
+ - `actions` → `{action: "…"}` with entries joined by space.
508
+ - `controllers` → `{controller: "…"}` with non-empty entries joined by space.
509
+ - `targets` → one key per controller-target attribute; multiple targets on
510
+ the same controller joined with a single space.
511
+ - `values`, `params`, `class_maps` → merged per-data-attribute Hash.
512
+ - `outlets` → same.
384
513
 
385
514
  ---
386
515
 
@@ -398,7 +527,7 @@ File: `lib/vident/caching.rb`.
398
527
  into this class's `component_modified_time`, so sub-component edits bust the
399
528
  parent's cache.
400
529
  - `component_modified_time` — memoised in `Rails.env.production?`, otherwise recomputed
401
- on every call. Raises `StandardError` if the host class has no
530
+ on every call. Raises `Vident::ConfigurationError` if the host class has no
402
531
  `cache_component_modified_time` (base classes provide it).
403
532
 
404
533
  ### Instance methods
@@ -407,13 +536,22 @@ File: `lib/vident/caching.rb`.
407
536
  - `cacheable?` — `respond_to?(:cache_key)`.
408
537
  - `cache_key` — defined when `with_cache_key` has been called; returns
409
538
  `"#{class.name}/#{cache_keys_for_sources(...).join("/")}"`, optionally suffixed with
410
- `ENV["RAILS_CACHE_ID"]`. Raises `StandardError` if the computed key is blank.
539
+ `ENV["RAILS_CACHE_ID"]`. Raises `Vident::ConfigurationError` if the computed key is blank.
411
540
  - `cache_key_modifier` — returns `ENV["RAILS_CACHE_ID"]` (may be nil).
412
541
 
413
542
  `with_cache_key` without any attrs is valid — the call still appends
414
543
  `:component_modified_time` and `:to_h`, so the cache key reflects the template mtime
415
544
  plus the component's full prop hash.
416
545
 
546
+ ### Fragment-caching the render: `cache_component`
547
+
548
+ Available on both adapter base classes (`Vident::Phlex::HTML` and `Vident::ViewComponent::Base`). Wraps a block of render output with Rails.cache using the Vident-computed `cache_key`:
549
+
550
+ - `cache_component(*extra_keys, **options, &block)` — on Phlex, delegates to `Phlex::SGML#cache([cache_key, *extra_keys], **options, &block)`. On ViewComponent, uses `Rails.cache.fetch([cache_key, *extra_keys], **options) { capture(&block) }`.
551
+ - Raises `Vident::ConfigurationError` if the component is not cacheable (no `with_cache_key` declared).
552
+ - `extra_keys` let the caller add per-render state to the cache key without modifying `with_cache_key`.
553
+ - Phlex usage: inside `view_template`. ViewComponent usage: inside a `def call` method; sidecar ERB templates can use Rails' native `<% cache cache_key do %> ... <% end %>` instead.
554
+
417
555
  ---
418
556
 
419
557
  ## 9. `Vident::StableId`
@@ -467,15 +605,15 @@ Included into every component. File: `lib/vident/tailwind.rb`.
467
605
  the `tailwind_merge` gem is loaded; otherwise returns `nil`.
468
606
  - `tailwind_merge_available?` — `true` iff `::TailwindMerge::Merger` is defined.
469
607
 
470
- `Vident::ClassListBuilder` invokes `tailwind_merger.merge(class_string)` automatically
471
- at the final stage of its `build(...)` call when a merger is provided. No per-component
608
+ `Vident::Internals::ClassListBuilder` invokes `tailwind_merger.merge(class_string)` automatically
609
+ at the final stage of its `call(...)` when a merger is provided. No per-component
472
610
  opt-in is required beyond adding the gem to the Gemfile.
473
611
 
474
612
  ---
475
613
 
476
614
  ## 11. `class_list_for_stimulus_classes`
477
615
 
478
- Instance method on every component. File: `lib/vident/component_class_lists.rb`.
616
+ Instance method on every component. File: `lib/vident/capabilities/class_list_building.rb`.
479
617
 
480
618
  ```ruby
481
619
  class_list_for_stimulus_classes(*names) -> String
@@ -493,26 +631,32 @@ Names may be `Symbol` or `String`; both are normalised via `dasherize`.
493
631
  ## 12. Rails engine hooks
494
632
 
495
633
  - `Vident::Engine` (`lib/vident/engine.rb`) — autoloaded when Rails is defined.
496
- Loads `Vident::Generators::InstallGenerator`.
497
- - `Vident::Phlex::Engine`, `Vident::ViewComponent::Engine` — thin Rails::Engine
498
- subclasses from the sub-gems; no explicit initializer.
634
+ Registers Zeitwerk inflections for Vident's non-standard file names (`"dsl"` →
635
+ `"DSL"`, `"html"` → `"HTML"`) so `Vident::Internals::DSL` and
636
+ `Vident::Phlex::HTML` resolve correctly. It does not load any generators at
637
+ engine init — `Vident::Generators::InstallGenerator` is autoloaded on demand
638
+ when `bin/rails generate vident:install` is invoked.
499
639
 
500
640
  ---
501
641
 
502
642
  ## 13. What's not in the public API
503
643
 
504
- The following show up in `lib/vident/*.rb` but are explicitly internal:
644
+ The following show up in `lib/vident/` but are explicitly internal:
505
645
 
506
- - `Vident::Stimulus::PRIMITIVES`, `Vident::Stimulus::Primitive`,
507
- `Vident::Stimulus::KeyedPrimitive`, `Vident::Stimulus::PositionalPrimitive`,
508
- `Vident::Stimulus::Naming` the registry that drives every plural parser,
509
- mutator, and DSL primitive. Don't rely on these modules in application code.
510
- - `Vident::StimulusDataAttributeBuilder` used internally by `root_element_attributes`
646
+ - `Vident::Internals::Registry::KINDS` / `Vident::Internals::Registry::Kind` — the
647
+ registry that drives every plural parser, mutator, and DSL primitive. Don't rely on
648
+ these in application code.
649
+ - `Vident::Stimulus::Naming` pure naming helpers (`stimulize_path`, `js_name`)
650
+ consumed by value classes. The two `module_function` methods documented in §4.5
651
+ are callable directly (`Vident::Stimulus::Naming.stimulize_path(...)`) but the
652
+ module itself is not designed for subclassing or further extension.
653
+ - `Vident::Internals::AttributeWriter` — used internally by `root_element_attributes`
511
654
  resolution and `child_element`. Takes a collection-per-primitive kwarg hash and
512
655
  merges their `to_h` outputs.
513
- - `Vident::ClassListBuilder` — invoked internally by `ComponentClassLists#render_classes`.
514
- - `Vident::ComponentAttributeResolver`, `Vident::ComponentClassLists`,
515
- `Vident::StimulusHelper`, `Vident::ChildElementHelper`, `Vident::StimulusComponent` —
656
+ - `Vident::Internals::ClassListBuilder` — invoked internally by
657
+ `Vident::Capabilities::ClassListBuilding#class_list_for_stimulus_classes`.
658
+ - `Vident::Capabilities::ChildElementRendering`, `Vident::Capabilities::RootElementRendering`,
659
+ `Vident::Capabilities::StimulusMutation`, `Vident::Capabilities::StimulusDraft` —
516
660
  included into components; their private/internal methods are not API.
517
- - `Vident::StimulusComponent.stimulus_identifier_from_path(path)` — still callable
518
- but kept only as a back-compat shim for `StimulusAttributeBase.stimulize_path`.
661
+ - `Vident::Stimulus::Naming.stimulize_path(path)` — the canonical path → identifier
662
+ helper. (V1's `stimulus_identifier_from_path` on `Vident::Component` was removed in V2.)
@@ -176,7 +176,7 @@ export default class extends Controller {
176
176
  }
177
177
  ```
178
178
 
179
- ### Detail panel (StimulusNull + keyboard modifier action)
179
+ ### Detail panel (StimulusNull + keyboard modifier action + alias resolution)
180
180
 
181
181
  ```ruby
182
182
  module Dashboard
@@ -192,14 +192,27 @@ module Dashboard
192
192
 
193
193
  classes state: "fixed right-0 top-0 h-full w-80 border-l bg-white p-6 shadow-xl transition-transform duration-200 translate-x-full"
194
194
 
195
- # Three action entries in one `actions` call:
196
- # 1. scoped window event from ReleaseCard opens the panel
197
- # 2. Hash form with keyboard filter + @window → Escape closes it.
198
- # Expands to `keydown.esc@window->dashboard--detail-panel-component#close`.
199
- # 3. plain `:close` — the close button's local click target
200
- actions -> { [ReleaseCardComponent.stimulus_scoped_event_on_window(:selected), :handle_selected] },
201
- { event: :keydown, method: :close, keyboard: "esc", window: true },
202
- :close
195
+ # Secondary controller stacked on the same root, given a short alias so
196
+ # later action entries can refer to it by `:dismissable` instead of the
197
+ # full path. Emits `data-controller="dashboard--detail-panel-component
198
+ # dashboard--dismissable"`.
199
+ controller "dashboard_v2/dismissable", as: :dismissable
200
+
201
+ # 1. Proc + scoped window event opens the panel when a card emits `selected`.
202
+ actions -> { [ReleaseCardComponent.stimulus_scoped_event_on_window(:selected), :handle_selected] }
203
+
204
+ # 2. Kwargs shorthand for the keyboard filter. Equivalent to:
205
+ # `action(:close).on(:keydown).keyboard("esc").window`.
206
+ # Emits `keydown.esc@window->dashboard--detail-panel-component#close`.
207
+ action :close, on: :keydown, keyboard: "esc", window: true
208
+
209
+ # 3. Fluent chain routed through the `:dismissable` alias. Emits
210
+ # `keydown.backspace@window->dashboard--dismissable#close` instead of
211
+ # the implied controller — alias resolved by Internals::Resolver.
212
+ action(:close).on(:keydown).keyboard("backspace").window.on_controller(:dismissable)
213
+
214
+ # 4. Plain `:close` — wired to the close button's local click target.
215
+ action :close
203
216
  end
204
217
 
205
218
  def view_template
@@ -308,7 +321,7 @@ parent's action:
308
321
  <% end %>
309
322
  ```
310
323
 
311
- `greeter.stimulus_action(:click, :greet)` returns a `Vident::StimulusAction` whose
324
+ `greeter.stimulus_action(:click, :greet)` returns a `Vident::Stimulus::Action` whose
312
325
  `controller` is the parent's (greeter's) identifier, so the click handler on the child's
313
326
  button routes to `greeter-with-trigger-component#greet`, not to the child.
314
327