vident 1.0.1 → 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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +49 -18
  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/vident/internals/action_builder.rb +97 -0
  22. data/lib/vident/internals/attribute_writer.rb +17 -0
  23. data/lib/vident/internals/class_list_builder.rb +62 -0
  24. data/lib/vident/internals/declaration.rb +13 -0
  25. data/lib/vident/internals/declarations.rb +64 -0
  26. data/lib/vident/internals/draft.rb +47 -0
  27. data/lib/vident/internals/dsl.rb +172 -0
  28. data/lib/vident/internals/plan.rb +9 -0
  29. data/lib/vident/internals/registry.rb +37 -0
  30. data/lib/vident/internals/resolver.rb +316 -0
  31. data/lib/vident/internals/target_builder.rb +23 -0
  32. data/lib/vident/stable_id.rb +3 -3
  33. data/lib/vident/stimulus/action.rb +127 -0
  34. data/lib/vident/stimulus/base.rb +26 -0
  35. data/lib/vident/stimulus/class_map.rb +57 -0
  36. data/lib/vident/stimulus/collection.rb +40 -0
  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/vident/stimulus/outlet.rb +93 -0
  42. data/lib/vident/stimulus/param.rb +56 -0
  43. data/lib/vident/stimulus/target.rb +48 -0
  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 +133 -21
  51. data/skills/vident/api-reference.md +662 -0
  52. data/skills/vident/examples.md +505 -0
  53. metadata +40 -28
  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 -87
  57. data/lib/vident/component_class_lists.rb +0 -34
  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 -119
  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
@@ -0,0 +1,662 @@
1
+ # Vident API reference
2
+
3
+ Public surface of `vident`, `vident-view_component`, and `vident-phlex`, verified against
4
+ the current code in `lib/vident/`. Every method's argument shapes, return shape, and
5
+ raise-conditions are documented here — SKILL.md is the tutorial; this file is the spec.
6
+
7
+ If something is missing, it isn't public. `lib/vident/*.rb` is the source of truth.
8
+
9
+ ---
10
+
11
+ ## 1. Base classes
12
+
13
+ ### `Vident::ViewComponent::Base < ::ViewComponent::Base`
14
+
15
+ Inherits everything from `::ViewComponent::Base` and includes `Vident::Component`.
16
+ File: `lib/vident/view_component/base.rb`.
17
+
18
+ Adds:
19
+
20
+ - `root_element(**overrides, &block)` — renders the component's root tag. `overrides`
21
+ are passed as HTML options (merged with `root_element_attributes`, `html_options`,
22
+ class precedence rules from SKILL.md §4). Self-closing tags (`:area`, `:br`, `:col`,
23
+ `:embed`, `:hr`, `:img`, `:input`, `:link`, `:meta`, `:param`, `:source`, `:track`,
24
+ `:wbr`) are emitted without children.
25
+ - 14 `as_stimulus_*` helpers — return an HTML-safe `String` of raw `data-*` attributes
26
+ suitable for embedding inside an HTML tag in ERB. Signatures match the corresponding
27
+ `stimulus_*` method (see section 4):
28
+ - Plural: `as_stimulus_controllers`, `as_stimulus_actions`, `as_stimulus_targets`,
29
+ `as_stimulus_outlets`, `as_stimulus_values`, `as_stimulus_params`, `as_stimulus_classes`.
30
+ - Singular: `as_stimulus_controller`, `as_stimulus_action`, `as_stimulus_target`,
31
+ `as_stimulus_outlet`, `as_stimulus_value`, `as_stimulus_param`, `as_stimulus_class`.
32
+ - Class-level cache support: `template_path`, `component_path`, `components_base_path`,
33
+ `cache_component_modified_time`, `cache_sidecar_view_modified_time`,
34
+ `cache_rb_component_modified_time` — used by `Vident::Caching` to chain
35
+ template mtimes into a component's cache key.
36
+
37
+ ### `Vident::Phlex::HTML < ::Phlex::HTML`
38
+
39
+ Includes `Vident::Component`. File: `lib/vident/phlex/html.rb`.
40
+
41
+ Adds:
42
+
43
+ - `root_element(**overrides, &block)` — Phlex equivalent. Dispatches to the tag method
44
+ named by `root_element_tag_type` (default `:div`). The block runs first, then the tag
45
+ wraps the captured content so DSL methods called inside the block see resolved state
46
+ before outer tag options are computed.
47
+ - Tag whitelist — `check_valid_html_tag!` enforces `STANDARD_ELEMENTS + VOID_ELEMENTS`
48
+ (see file for the full set). Passing an unknown tag to `element_tag:` or to
49
+ `child_element` raises `ArgumentError`.
50
+ - Source-file tracking — the class-level `inherited` hook records each subclass's source
51
+ file in `component_source_file_path` so `Vident::Caching` can pick up an mtime.
52
+ - No `as_stimulus_*` helpers — Phlex has its own tag DSL; use `child_element` or spread
53
+ `data: { **component.stimulus_target(:name) }` inline.
54
+
55
+ ### `Vident::Component` (module)
56
+
57
+ Included into both base classes. File: `lib/vident/component.rb`.
58
+
59
+ Public class methods:
60
+
61
+ - `prop_names` — `Array(Symbol)`, list of every declared prop (including inherited).
62
+
63
+ Public instance methods:
64
+
65
+ - `after_component_initialize` — empty override hook. Runs after props are assigned and
66
+ Vident has prepared its stimulus collections. Do not override `after_initialize` unless
67
+ you `super` — Literal calls it to wire everything up.
68
+ - `root_element_classes` — override to return `String | Array(String) | nil`. Lower
69
+ precedence than `html_options[:class]` and `root_element_attributes[:classes]`
70
+ (see SKILL.md §4).
71
+ - `root_element_attributes` — override to return a Hash. Accepted keys (all optional):
72
+ `:element_tag` (Symbol), `:html_options` (Hash), `:id` (String), `:classes`
73
+ (String | Array), and any of the seven `stimulus_<plural>:` / `stimulus_<singular>:`
74
+ keys documented in section 5.
75
+ - `with(overrides = {})` — returns a new instance, `self.class.new(**to_h.merge(overrides))`. `clone(overrides = {})` is a backward-compat alias.
76
+ - `inspect(klass_name = "Component")` — formatted debug string with every prop.
77
+ - `id` — `String`, auto-generated from `StableId` if `@id` was nil. The generated form
78
+ is `"#{component_name}-#{StableId.next_id_in_sequence}"`.
79
+ - `prop_names` — instance-method alias for the class method.
80
+
81
+ Not public (override at your own risk, used internally):
82
+
83
+ - `root_element(&block)` — raises in the base; the ViewComponent / Phlex subclasses
84
+ implement it.
85
+ - `root_element_tag_type` — returns `@element_tag || :div`.
86
+ - `random_id` — memoised generator (cached per instance).
87
+
88
+ ### Built-in props (every component)
89
+
90
+ From `Vident::Component` (`lib/vident/component.rb`):
91
+
92
+ | Prop | Type | Default | Notes |
93
+ | -------------- | ----------------------------------- | -------------- | ------------------------------------------------ |
94
+ | `element_tag` | `Symbol` | `:div` | Root HTML tag. |
95
+ | `id` | `_Nilable(String)` | auto | Auto-generated via `StableId` when not provided. |
96
+ | `classes` | `_Union(String, _Array(String))` | `[]` | Appended on top of all other class sources. |
97
+ | `html_options` | `Hash` | `{}` | Merged onto root; highest class-source precedence. |
98
+
99
+ From `Vident::Component` via the `StimulusDeclaring` / `StimulusParsing` capability mixins:
100
+
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
+ ```
129
+
130
+ ---
131
+
132
+ ## 2. Class-level DSL
133
+
134
+ All of these live on `Vident::Component`'s class body (via included modules).
135
+
136
+ - `prop(name, type, **literal_options)` — from the Literal gem. See
137
+ https://literal.fun/ for option details. `default:` may be a callable (`lambda/proc`)
138
+ or an immediate value; callable is required when the default is non-frozen (hash, array).
139
+ - `no_stimulus_controller` — sets a class ivar that drops the implied controller from
140
+ the `stimulus_controllers` default. Use when the component is purely presentational
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.
145
+ - `stimulus_controller?` — `Boolean`, `true` by default; becomes `false` after a
146
+ `no_stimulus_controller` declaration and `true` again after `has_stimulus_controller`.
147
+ - `stimulus_identifier_path` — the `name.underscore` of the class (e.g.
148
+ `"dashboard/release_card_component"`). Falls back to `"anonymous_component"` for
149
+ anonymous classes.
150
+ - `stimulus_identifier` — `stimulize_path(stimulus_identifier_path)` — the kebab-cased
151
+ identifier (`"dashboard--release-card-component"`). Also available as an instance method.
152
+ - `component_name` — memoised alias for `stimulus_identifier`. Also available as an
153
+ instance method. Used as the first class on the root element and as the outlet name
154
+ seed.
155
+ - `stimulus_scoped_event(event)` — `Symbol` of the form `:"<component_name>:<jsName>"`.
156
+ E.g. `FooComponent.stimulus_scoped_event(:data_ready)` →
157
+ `:"foo-component:dataReady"`. Also an instance method.
158
+ - `stimulus_scoped_event_on_window(event)` — same, with `@window` suffix. Also an
159
+ instance method.
160
+ - `stimulus(&block)` — the DSL entry point. Opens a `Vident::Internals::DSL` block
161
+ evaluator. See section 3.
162
+
163
+ Not intended for application code:
164
+
165
+ - `declarations` — frozen `Vident::Internals::Declarations` aggregate (own + inherited);
166
+ `protected`, consumed by the Resolver at render time.
167
+
168
+ ---
169
+
170
+ ## 3. `stimulus do ... end` block
171
+
172
+ Evaluated by `Vident::Internals::DSL` (`lib/vident/internals/dsl.rb`). Multiple
173
+ `stimulus do` blocks on the same class accumulate. A subclass's blocks are merged with
174
+ every parent's blocks on first access (subclass entries appended to positional kinds;
175
+ subclass wins on conflicts for keyed kinds).
176
+
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).
182
+
183
+ ### Methods on the builder
184
+
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:
222
+ - `Symbol` → `implied#<jsSymbol>`
223
+ - `[Symbol, Symbol]` → `<event>-><implied>#<jsMethod>`
224
+ - `[Symbol, String, Symbol]` → `<event>-><stimulized-path>#<jsMethod>`
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:
242
+ - `Symbol` → target on the implied controller
243
+ - `String` → pass-through target name
244
+ - `[String, Symbol]` → target on the named cross-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.
267
+
268
+ ### What the builder emits
269
+
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.
289
+
290
+ ---
291
+
292
+ ## 4. Instance-level Stimulus helpers (`Vident::Capabilities::StimulusParsing`)
293
+
294
+ Included into every component via `Vident::Component`. File:
295
+ `lib/vident/capabilities/stimulus_parsing.rb`.
296
+
297
+ ### 4.1 Plural parsers `stimulus_<plural>(*args)`
298
+
299
+ Seven methods: `stimulus_controllers`, `stimulus_actions`, `stimulus_targets`,
300
+ `stimulus_outlets`, `stimulus_values`, `stimulus_params`, `stimulus_classes`.
301
+
302
+ Each returns a collection object (`Vident::Stimulus::Collection`, etc.) whose `#to_h`
303
+ serialises to a `Hash` of `data-*` keys → values. Arg handling per input:
304
+
305
+ - no args or all-blank → empty collection
306
+ - single pre-built collection → returned as-is
307
+ - `Array` → splatted into the singular builder
308
+ - `Hash` (for **keyed** primitives: outlets, values, params, classes)
309
+ → expanded per-pair, each pair becomes one value object
310
+ - `Hash` (for **positional** primitives: controllers, actions, targets)
311
+ → passed as a single-arg descriptor (Action's `{event:, method:, ...}` form)
312
+ - pre-built value object → preserved
313
+
314
+ ### 4.2 Singular builders `stimulus_<singular>(*args)`
315
+
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.
318
+
319
+ - `stimulus_controller(*)` — 0 or 1 arg. 0 args returns the implied controller; 1 arg
320
+ is a controller path `String`/`Symbol`.
321
+ - `stimulus_action(*)` — 1/2/3 args. See `Vident::Stimulus::Action.parse` for all
322
+ accepted forms. `options:` whitelist (raises otherwise):
323
+ `[:once, :prevent, :stop, :passive, :"!passive", :capture, :self]`.
324
+ - `stimulus_target(*)` — 1 or 2 args. `(Symbol)` / `(String)` → implied controller;
325
+ `(String, Symbol)` → cross-controller + name.
326
+ - `stimulus_outlet(*)` — 1/2/3 args.
327
+ - `(Symbol)` or `(String)` → identifier, auto-generated selector
328
+ `"#<component_id> [data-controller~=<identifier>]"`.
329
+ - `(Array[identifier, selector])` — explicit selector.
330
+ - `(component_instance)` — instance responding to `#stimulus_identifier` or
331
+ `#implied_controller_name`; auto-selector built from its identifier.
332
+ - `(String|Symbol, String)` → outlet-name + selector on implied controller.
333
+ - `(String, Symbol, String)` → cross-controller + outlet-name + selector.
334
+ - `stimulus_value(name, value)` or `stimulus_value(controller_path, name, value)` —
335
+ 2 or 3 args.
336
+ - `stimulus_param(name, value)` or `stimulus_param(controller_path, name, value)` —
337
+ 2 or 3 args.
338
+ - `stimulus_class(name, classes)` or `stimulus_class(controller_path, name, classes)` —
339
+ 2 or 3 args; `classes` is `String` or `Array(String)`.
340
+
341
+ ### 4.3 Mutators `add_stimulus_<plural>(input)`
342
+
343
+ Seven methods, one per primitive. Merge new attributes into the per-kind collection
344
+ ivar (e.g. `@stimulus_actions_collection`). Typical use: inside
345
+ `after_component_initialize`, compute runtime attributes and add them.
346
+
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))`.
353
+
354
+ ### 4.4 Value serialisation
355
+
356
+ `Array` and `Hash` → JSON. Everything else → `to_s`. `Vident::StimulusNull.to_s`
357
+ returns the literal string `"null"`. A `nil` reaches the DSL/prop layer and
358
+ is dropped by the Resolver before serialisation, so the data attribute is omitted
359
+ (not emitted as empty).
360
+
361
+ ### 4.5 Name-shaping helpers
362
+
363
+ `Vident::Stimulus::Naming` is a `module_function` module — call its methods directly:
364
+
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"`.
368
+
369
+ ### 4.6 Scoped events
370
+
371
+ - Class method `stimulus_scoped_event(event)` — returns `Symbol`
372
+ `:"<component_name>:<jsName>"`. **Call on the dispatcher's class**, not on the
373
+ listener's.
374
+ - Class method `stimulus_scoped_event_on_window(event)` — same with `@window` suffix.
375
+ - Both also exist as instance methods that delegate to the class method.
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
+
419
+ ---
420
+
421
+ ## 5. `root_element_attributes` accepted keys
422
+
423
+ Override `root_element_attributes` (instance method on your component) to return any
424
+ subset of:
425
+
426
+ | Key | Type | Effect |
427
+ | --------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------- |
428
+ | `:element_tag` | `Symbol` | Overrides the root element tag. |
429
+ | `:html_options` | `Hash` | Merged onto the root; highest-precedence source for `:class`. |
430
+ | `:classes` | `String \| Array(String)` | Second-highest `:class` source (see SKILL.md §4). |
431
+ | `:id` | `String` | Sets the root element's id. |
432
+ | `:stimulus_controllers`| Same shape as `stimulus_controllers` prop | Merged into the controllers collection. |
433
+ | `:stimulus_actions` | Same shape as `stimulus_actions` prop | Merged into the actions collection. |
434
+ | `:stimulus_targets` | Same shape as `stimulus_targets` prop | Merged into the targets collection. |
435
+ | `:stimulus_outlets` | Same shape as `stimulus_outlets` prop | Merged into the outlets collection. |
436
+ | `:stimulus_values` | Same shape as `stimulus_values` prop | Merged into the values collection. |
437
+ | `:stimulus_params` | Same shape as `stimulus_params` prop | Merged into the params collection. |
438
+ | `:stimulus_classes` | Same shape as `stimulus_classes` prop | Merged into the classes collection. |
439
+
440
+ Precedence (lower wins if both set):
441
+
442
+ 1. `stimulus do` DSL
443
+ 2. `stimulus_*` props (the `render Foo.new(stimulus_actions: ...)` path)
444
+ 3. `root_element_attributes` return value
445
+ 4. `add_stimulus_*` called after those (e.g. in `after_component_initialize`)
446
+
447
+ ---
448
+
449
+ ## 6. `child_element`
450
+
451
+ Renders a single child tag with `stimulus_*` kwargs compiled into `data-*` attributes.
452
+
453
+ ```ruby
454
+ def child_element(tag_name,
455
+ stimulus_controllers: nil, stimulus_controller: nil,
456
+ stimulus_actions: nil, stimulus_action: nil,
457
+ stimulus_targets: nil, stimulus_target: nil,
458
+ stimulus_outlets: nil, stimulus_outlet: nil,
459
+ stimulus_values: nil, stimulus_value: nil,
460
+ stimulus_params: nil, stimulus_param: nil,
461
+ stimulus_classes: nil, stimulus_class: nil,
462
+ **options, &block)
463
+ ```
464
+
465
+ - Plural kwargs take an `Enumerable`; passing a non-Enumerable raises
466
+ `ArgumentError` with a message pointing at the singular name.
467
+ - Singular kwargs take a single entry.
468
+ - `**options` passes through as HTML options.
469
+ - For ViewComponent's renderer, self-closing tags are emitted without the block.
470
+ - For Phlex's renderer, the tag name is validated against
471
+ `Vident::Phlex::HTML::VALID_TAGS`; unknown tags raise `ArgumentError`.
472
+
473
+ ---
474
+
475
+ ## 7. `Vident::Internals::DSL` primitives
476
+
477
+ For use in advanced cases (passing typed descriptors across components, building
478
+ reusable shared helpers). Value classes live under `lib/vident/stimulus/`.
479
+
480
+ ### Hash descriptor form
481
+
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:
484
+
485
+ | Key | Type | Default |
486
+ | ------------- | ------------------------------------- | ------- |
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` |
493
+
494
+ ### `Vident::StimulusNull`
495
+
496
+ Frozen singleton object. `inspect` → `"Vident::StimulusNull"`; `to_s` → `"null"`.
497
+ See SKILL.md §1.4 for the usage contract.
498
+
499
+ ### Collection class
500
+
501
+ All primitive kinds share one parametric class: `Vident::Stimulus::Collection`,
502
+ parametrised on a `Kind` record from `Vident::Internals::Registry`.
503
+
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.
513
+
514
+ ---
515
+
516
+ ## 8. `Vident::Caching`
517
+
518
+ Opt-in: `include Vident::Caching` + `with_cache_key(...)` in the component class.
519
+ File: `lib/vident/caching.rb`.
520
+
521
+ ### Class methods
522
+
523
+ - `with_cache_key(*attrs, name: :_collection)` — declares which attributes feed into
524
+ `cache_key`. The call appends `:component_modified_time` and `:to_h` (when
525
+ available) to the given attrs, then calls `named_cache_key_includes(name, *attrs.uniq)`.
526
+ - `depends_on(*klasses)` — chains other Vident components' `component_modified_time`
527
+ into this class's `component_modified_time`, so sub-component edits bust the
528
+ parent's cache.
529
+ - `component_modified_time` — memoised in `Rails.env.production?`, otherwise recomputed
530
+ on every call. Raises `Vident::ConfigurationError` if the host class has no
531
+ `cache_component_modified_time` (base classes provide it).
532
+
533
+ ### Instance methods
534
+
535
+ - `component_modified_time` — delegates to the class method.
536
+ - `cacheable?` — `respond_to?(:cache_key)`.
537
+ - `cache_key` — defined when `with_cache_key` has been called; returns
538
+ `"#{class.name}/#{cache_keys_for_sources(...).join("/")}"`, optionally suffixed with
539
+ `ENV["RAILS_CACHE_ID"]`. Raises `Vident::ConfigurationError` if the computed key is blank.
540
+ - `cache_key_modifier` — returns `ENV["RAILS_CACHE_ID"]` (may be nil).
541
+
542
+ `with_cache_key` without any attrs is valid — the call still appends
543
+ `:component_modified_time` and `:to_h`, so the cache key reflects the template mtime
544
+ plus the component's full prop hash.
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
+
555
+ ---
556
+
557
+ ## 9. `Vident::StableId`
558
+
559
+ File: `lib/vident/stable_id.rb`.
560
+
561
+ ### Errors
562
+
563
+ - `Vident::StableId::GeneratorNotSetError` — raised by `STRICT` when no per-thread
564
+ sequence generator is set.
565
+ - `Vident::StableId::StrategyNotConfiguredError` — raised when any component calls
566
+ `next_id_in_sequence` before `StableId.strategy=` has been set.
567
+
568
+ ### Strategies (both callables accepting `(generator_or_nil) -> String`)
569
+
570
+ - `STRICT` — raises `GeneratorNotSetError` if the generator is nil. Use in
571
+ development/production paired with the `before_action` seed in `ApplicationController`.
572
+ - `RANDOM_FALLBACK` — returns `Random.hex(16)` when the generator is nil; otherwise
573
+ returns `generator.next.join("-")`. Use in test/previews/jobs/mailers.
574
+
575
+ ### Class methods
576
+
577
+ - `strategy` / `strategy=` — get/set the configured callable.
578
+ - `set_current_sequence_generator(seed:)` — seeds a per-thread generator. Raises
579
+ `ArgumentError` on `seed: nil`. Seed is MD5-hashed then fed to `Random.new`, so any
580
+ `String`-coercible seed works.
581
+ - `clear_current_sequence_generator` — clears the per-thread generator.
582
+ - `with_sequence_generator(seed:) { ... }` — scoped seed for a block (used by jobs,
583
+ mailers, Metal endpoints). Restores the previous generator on exit.
584
+ - `next_id_in_sequence` — delegates to the configured `strategy`.
585
+
586
+ ### Installation
587
+
588
+ `bin/rails generate vident:install` (file:
589
+ `lib/generators/vident/install/install_generator.rb`):
590
+
591
+ 1. Writes `config/initializers/vident.rb` setting `strategy` to `RANDOM_FALLBACK` in
592
+ test and `STRICT` everywhere else.
593
+ 2. Injects `before_action` + `after_action` into `ApplicationController` (idempotent —
594
+ skips if a previous install patched it).
595
+ 3. Copies `skills/vident/SKILL.md` from the gem to `.claude/skills/vident/SKILL.md`
596
+ in the host app (skipped if already present).
597
+
598
+ ---
599
+
600
+ ## 10. `Vident::Tailwind`
601
+
602
+ Included into every component. File: `lib/vident/tailwind.rb`.
603
+
604
+ - `tailwind_merger` — returns a thread-cached `::TailwindMerge::Merger` instance if
605
+ the `tailwind_merge` gem is loaded; otherwise returns `nil`.
606
+ - `tailwind_merge_available?` — `true` iff `::TailwindMerge::Merger` is defined.
607
+
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
610
+ opt-in is required beyond adding the gem to the Gemfile.
611
+
612
+ ---
613
+
614
+ ## 11. `class_list_for_stimulus_classes`
615
+
616
+ Instance method on every component. File: `lib/vident/capabilities/class_list_building.rb`.
617
+
618
+ ```ruby
619
+ class_list_for_stimulus_classes(*names) -> String
620
+ ```
621
+
622
+ Returns the resolved `data-*-class` values for the named stimulus-class entries,
623
+ deduplicated and (when `tailwind_merger` is available) Tailwind-merged. Intended
624
+ for inlining into `class=` on SSR so the first render has the same visual state the
625
+ JS controller will toggle on/off.
626
+
627
+ Names may be `Symbol` or `String`; both are normalised via `dasherize`.
628
+
629
+ ---
630
+
631
+ ## 12. Rails engine hooks
632
+
633
+ - `Vident::Engine` (`lib/vident/engine.rb`) — autoloaded when Rails is defined.
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.
639
+
640
+ ---
641
+
642
+ ## 13. What's not in the public API
643
+
644
+ The following show up in `lib/vident/` but are explicitly internal:
645
+
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`
654
+ resolution and `child_element`. Takes a collection-per-primitive kwarg hash and
655
+ merges their `to_h` outputs.
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` —
660
+ included into components; their private/internal methods are not API.
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.)