vident 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +4 -1
  4. data/lib/vident/component_attribute_resolver.rb +27 -8
  5. data/lib/vident/component_class_lists.rb +7 -1
  6. data/lib/vident/stimulus_builder.rb +28 -11
  7. data/lib/vident/stimulus_helper.rb +4 -4
  8. data/lib/vident/version.rb +1 -1
  9. data/lib/vident2/caching.rb +93 -0
  10. data/lib/vident2/component.rb +538 -0
  11. data/lib/vident2/engine.rb +18 -0
  12. data/lib/vident2/error.rb +30 -0
  13. data/lib/vident2/internals/action_builder.rb +101 -0
  14. data/lib/vident2/internals/attribute_writer.rb +22 -0
  15. data/lib/vident2/internals/class_list_builder.rb +79 -0
  16. data/lib/vident2/internals/declaration.rb +17 -0
  17. data/lib/vident2/internals/declarations.rb +76 -0
  18. data/lib/vident2/internals/draft.rb +60 -0
  19. data/lib/vident2/internals/dsl.rb +198 -0
  20. data/lib/vident2/internals/plan.rb +12 -0
  21. data/lib/vident2/internals/registry.rb +41 -0
  22. data/lib/vident2/internals/resolver.rb +306 -0
  23. data/lib/vident2/internals/target_builder.rb +29 -0
  24. data/lib/vident2/phlex/html.rb +84 -0
  25. data/lib/vident2/phlex.rb +9 -0
  26. data/lib/vident2/stimulus/action.rb +140 -0
  27. data/lib/vident2/stimulus/class_map.rb +69 -0
  28. data/lib/vident2/stimulus/collection.rb +42 -0
  29. data/lib/vident2/stimulus/controller.rb +59 -0
  30. data/lib/vident2/stimulus/naming.rb +26 -0
  31. data/lib/vident2/stimulus/null.rb +16 -0
  32. data/lib/vident2/stimulus/outlet.rb +113 -0
  33. data/lib/vident2/stimulus/param.rb +62 -0
  34. data/lib/vident2/stimulus/target.rb +57 -0
  35. data/lib/vident2/stimulus/value.rb +77 -0
  36. data/lib/vident2/tailwind.rb +19 -0
  37. data/lib/vident2/version.rb +5 -0
  38. data/lib/vident2/view_component/base.rb +124 -0
  39. data/lib/vident2/view_component.rb +9 -0
  40. data/lib/vident2.rb +50 -0
  41. data/skills/vident/SKILL.md +11 -2
  42. data/skills/vident/api-reference.md +518 -0
  43. data/skills/vident/examples.md +492 -0
  44. metadata +35 -1
@@ -0,0 +1,518 @@
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
+ - `clone(overrides = {})` — returns a new instance, `self.class.new(**to_h.merge(**overrides))`.
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::StimulusComponent` (`lib/vident/stimulus_component.rb`):
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)` | `{}` |
111
+
112
+ ---
113
+
114
+ ## 2. Class-level DSL
115
+
116
+ All of these live on `Vident::Component`'s class body (via included modules).
117
+
118
+ - `prop(name, type, **literal_options)` — from the Literal gem. See
119
+ https://literal.fun/ for option details. `default:` may be a callable (`lambda/proc`)
120
+ or an immediate value; callable is required when the default is non-frozen (hash, array).
121
+ - `no_stimulus_controller` — sets a class ivar that drops the implied controller from
122
+ the `stimulus_controllers` default. Use when the component is purely presentational
123
+ and needs no paired `_controller.js`.
124
+ - `stimulus_controller?` — `Boolean`, `true` by default; becomes `false` after a
125
+ `no_stimulus_controller` declaration.
126
+ - `stimulus_identifier_path` — the `name.underscore` of the class (e.g.
127
+ `"dashboard/release_card_component"`). Falls back to `"anonymous_component"` for
128
+ anonymous classes.
129
+ - `stimulus_identifier` — `stimulize_path(stimulus_identifier_path)` — the kebab-cased
130
+ identifier (`"dashboard--release-card-component"`). Also available as an instance method.
131
+ - `component_name` — memoised alias for `stimulus_identifier`. Also available as an
132
+ instance method. Used as the first class on the root element and as the outlet name
133
+ seed.
134
+ - `stimulus_scoped_event(event)` — `Symbol` of the form `:"<component_name>:<jsName>"`.
135
+ E.g. `FooComponent.stimulus_scoped_event(:data_ready)` →
136
+ `:"foo-component:dataReady"`. Also an instance method.
137
+ - `stimulus_scoped_event_on_window(event)` — same, with `@window` suffix. Also an
138
+ instance method.
139
+ - `stimulus(&block)` — the DSL entry point. Opens a `Vident::StimulusBuilder` block
140
+ evaluator. See section 3.
141
+
142
+ Not intended for application code:
143
+
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.
148
+
149
+ ---
150
+
151
+ ## 3. `stimulus do ... end` block
152
+
153
+ Evaluated by `Vident::StimulusBuilder` (`lib/vident/stimulus_builder.rb`). Multiple
154
+ `stimulus do` blocks on the same class accumulate. A subclass's blocks are merged with
155
+ every parent's blocks on first access (subclass entries appended to positional kinds;
156
+ subclass wins on conflicts for keyed kinds).
157
+
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.
161
+
162
+ ### Methods on the builder
163
+
164
+ - `actions(*entries)` — positional. Each entry is one of:
165
+ - `Symbol` → `implied#<jsSymbol>`
166
+ - `[Symbol, Symbol]` → `<event>-><implied>#<jsMethod>`
167
+ - `[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:
176
+ - `Symbol` → target on the implied controller
177
+ - `String` → pass-through target name
178
+ - `[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`.
196
+
197
+ ### What the builder emits
198
+
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.
203
+
204
+ ---
205
+
206
+ ## 4. Instance-level Stimulus helpers (`Vident::StimulusAttributes`)
207
+
208
+ Included into every component via `StimulusComponent`. File:
209
+ `lib/vident/stimulus_attributes.rb`.
210
+
211
+ ### 4.1 Plural parsers `stimulus_<plural>(*args)`
212
+
213
+ Seven methods: `stimulus_controllers`, `stimulus_actions`, `stimulus_targets`,
214
+ `stimulus_outlets`, `stimulus_values`, `stimulus_params`, `stimulus_classes`.
215
+
216
+ Each returns a collection object (`StimulusActionCollection`, etc.) whose `#to_h`
217
+ serialises to a `Hash` of `data-*` keys → values. Arg handling per input:
218
+
219
+ - no args or all-blank → empty collection
220
+ - single pre-built collection → returned as-is
221
+ - `Array` → splatted into the singular builder
222
+ - `Hash` (for **keyed** primitives: outlets, values, params, classes)
223
+ → expanded per-pair, each pair becomes one value object
224
+ - `Hash` (for **positional** primitives: controllers, actions, targets)
225
+ → passed as a single-arg descriptor (Action's `{event:, method:, ...}` form)
226
+ - pre-built value object → preserved
227
+
228
+ ### 4.2 Singular builders `stimulus_<singular>(*args)`
229
+
230
+ Each singular builder accepts the set of argument shapes its `parse_arguments`
231
+ implementation supports. Raises `ArgumentError` on unsupported shape or arity.
232
+
233
+ - `stimulus_controller(*)` — 0 or 1 arg. 0 args returns the implied controller; 1 arg
234
+ is a controller path `String`/`Symbol`.
235
+ - `stimulus_action(*)` — 1/2/3 args. See `StimulusAction::parse_arguments` for all
236
+ accepted forms. `options:` whitelist (raises otherwise):
237
+ `[:once, :prevent, :stop, :passive, :"!passive", :capture, :self]`.
238
+ - `stimulus_target(*)` — 1 or 2 args. `(Symbol)` / `(String)` → implied controller;
239
+ `(String, Symbol)` → cross-controller + name.
240
+ - `stimulus_outlet(*)` — 1/2/3 args.
241
+ - `(Symbol)` or `(String)` → identifier, auto-generated selector
242
+ `"#<component_id> [data-controller~=<identifier>]"`.
243
+ - `(Array[identifier, selector])` — explicit selector.
244
+ - `(component_instance)` — instance responding to `#stimulus_identifier` or
245
+ `#implied_controller_name`; auto-selector built from its identifier.
246
+ - `(String|Symbol, String)` → outlet-name + selector on implied controller.
247
+ - `(String, Symbol, String)` → cross-controller + outlet-name + selector.
248
+ - `stimulus_value(name, value)` or `stimulus_value(controller_path, name, value)` —
249
+ 2 or 3 args.
250
+ - `stimulus_param(name, value)` or `stimulus_param(controller_path, name, value)` —
251
+ 2 or 3 args.
252
+ - `stimulus_class(name, classes)` or `stimulus_class(controller_path, name, classes)` —
253
+ 2 or 3 args; `classes` is `String` or `Array(String)`.
254
+
255
+ ### 4.3 Mutators `add_stimulus_<plural>(input)`
256
+
257
+ Seven methods, one per primitive. Merge new attributes into the per-kind collection
258
+ ivar (e.g. `@stimulus_actions_collection`). Typical use: inside
259
+ `after_component_initialize`, compute runtime attributes and add them.
260
+
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]]`).
266
+
267
+ ### 4.4 Value serialisation (`StimulusAttributeBase#serialize_value`)
268
+
269
+ `Array` and `Hash` → JSON. Everything else → `to_s`. `Vident::StimulusNull.to_s`
270
+ 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).
273
+
274
+ ### 4.5 Name-shaping helpers
275
+
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"`.
279
+
280
+ Both also available as private instance methods on any `StimulusAttributeBase` subclass.
281
+
282
+ ### 4.6 Scoped events
283
+
284
+ - Class method `stimulus_scoped_event(event)` — returns `Symbol`
285
+ `:"<component_name>:<jsName>"`. **Call on the dispatcher's class**, not on the
286
+ listener's.
287
+ - Class method `stimulus_scoped_event_on_window(event)` — same with `@window` suffix.
288
+ - Both also exist as instance methods that delegate to the class method.
289
+
290
+ ---
291
+
292
+ ## 5. `root_element_attributes` accepted keys
293
+
294
+ Override `root_element_attributes` (instance method on your component) to return any
295
+ subset of:
296
+
297
+ | Key | Type | Effect |
298
+ | --------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------- |
299
+ | `:element_tag` | `Symbol` | Overrides the root element tag. |
300
+ | `:html_options` | `Hash` | Merged onto the root; highest-precedence source for `:class`. |
301
+ | `:classes` | `String \| Array(String)` | Second-highest `:class` source (see SKILL.md §4). |
302
+ | `:id` | `String` | Sets the root element's id. |
303
+ | `:stimulus_controllers`| Same shape as `stimulus_controllers` prop | Merged into the controllers collection. |
304
+ | `:stimulus_actions` | Same shape as `stimulus_actions` prop | Merged into the actions collection. |
305
+ | `:stimulus_targets` | Same shape as `stimulus_targets` prop | Merged into the targets collection. |
306
+ | `:stimulus_outlets` | Same shape as `stimulus_outlets` prop | Merged into the outlets collection. |
307
+ | `:stimulus_values` | Same shape as `stimulus_values` prop | Merged into the values collection. |
308
+ | `:stimulus_params` | Same shape as `stimulus_params` prop | Merged into the params collection. |
309
+ | `:stimulus_classes` | Same shape as `stimulus_classes` prop | Merged into the classes collection. |
310
+
311
+ Precedence (lower wins if both set):
312
+
313
+ 1. `stimulus do` DSL
314
+ 2. `stimulus_*` props (the `render Foo.new(stimulus_actions: ...)` path)
315
+ 3. `root_element_attributes` return value
316
+ 4. `add_stimulus_*` called after those (e.g. in `after_component_initialize`)
317
+
318
+ ---
319
+
320
+ ## 6. `child_element`
321
+
322
+ Renders a single child tag with `stimulus_*` kwargs compiled into `data-*` attributes.
323
+
324
+ ```ruby
325
+ def child_element(tag_name,
326
+ stimulus_controllers: nil, stimulus_controller: nil,
327
+ stimulus_actions: nil, stimulus_action: nil,
328
+ stimulus_targets: nil, stimulus_target: nil,
329
+ stimulus_outlets: nil, stimulus_outlet: nil,
330
+ stimulus_values: nil, stimulus_value: nil,
331
+ stimulus_params: nil, stimulus_param: nil,
332
+ stimulus_classes: nil, stimulus_class: nil,
333
+ **options, &block)
334
+ ```
335
+
336
+ - Plural kwargs take an `Enumerable`; passing a non-Enumerable raises
337
+ `ArgumentError` with a message pointing at the singular name.
338
+ - Singular kwargs take a single entry.
339
+ - `**options` passes through as HTML options.
340
+ - For ViewComponent's renderer, self-closing tags are emitted without the block.
341
+ - For Phlex's renderer, the tag name is validated against
342
+ `Vident::Phlex::HTML::VALID_TAGS`; unknown tags raise `ArgumentError`.
343
+
344
+ ---
345
+
346
+ ## 7. `Vident::StimulusBuilder` primitives
347
+
348
+ For use in advanced cases (passing typed descriptors across components, building
349
+ reusable shared helpers). File: `lib/vident/stimulus_action.rb`.
350
+
351
+ ### `Vident::StimulusAction::Descriptor`
352
+
353
+ A `::Literal::Data` value object with the same shape as the Hash form accepted by
354
+ `actions`:
355
+
356
+ | Prop | Type | Default |
357
+ | ------------- | ------------------------------------- | ------- |
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` |
364
+
365
+ ### `Vident::StimulusNull`
366
+
367
+ Frozen singleton object. `inspect` → `"Vident::StimulusNull"`; `to_s` → `"null"`.
368
+ See SKILL.md §1.4 for the usage contract.
369
+
370
+ ### Collection classes
371
+
372
+ Each primitive has a `StimulusXCollection < StimulusCollectionBase`:
373
+
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.
384
+
385
+ ---
386
+
387
+ ## 8. `Vident::Caching`
388
+
389
+ Opt-in: `include Vident::Caching` + `with_cache_key(...)` in the component class.
390
+ File: `lib/vident/caching.rb`.
391
+
392
+ ### Class methods
393
+
394
+ - `with_cache_key(*attrs, name: :_collection)` — declares which attributes feed into
395
+ `cache_key`. The call appends `:component_modified_time` and `:to_h` (when
396
+ available) to the given attrs, then calls `named_cache_key_includes(name, *attrs.uniq)`.
397
+ - `depends_on(*klasses)` — chains other Vident components' `component_modified_time`
398
+ into this class's `component_modified_time`, so sub-component edits bust the
399
+ parent's cache.
400
+ - `component_modified_time` — memoised in `Rails.env.production?`, otherwise recomputed
401
+ on every call. Raises `StandardError` if the host class has no
402
+ `cache_component_modified_time` (base classes provide it).
403
+
404
+ ### Instance methods
405
+
406
+ - `component_modified_time` — delegates to the class method.
407
+ - `cacheable?` — `respond_to?(:cache_key)`.
408
+ - `cache_key` — defined when `with_cache_key` has been called; returns
409
+ `"#{class.name}/#{cache_keys_for_sources(...).join("/")}"`, optionally suffixed with
410
+ `ENV["RAILS_CACHE_ID"]`. Raises `StandardError` if the computed key is blank.
411
+ - `cache_key_modifier` — returns `ENV["RAILS_CACHE_ID"]` (may be nil).
412
+
413
+ `with_cache_key` without any attrs is valid — the call still appends
414
+ `:component_modified_time` and `:to_h`, so the cache key reflects the template mtime
415
+ plus the component's full prop hash.
416
+
417
+ ---
418
+
419
+ ## 9. `Vident::StableId`
420
+
421
+ File: `lib/vident/stable_id.rb`.
422
+
423
+ ### Errors
424
+
425
+ - `Vident::StableId::GeneratorNotSetError` — raised by `STRICT` when no per-thread
426
+ sequence generator is set.
427
+ - `Vident::StableId::StrategyNotConfiguredError` — raised when any component calls
428
+ `next_id_in_sequence` before `StableId.strategy=` has been set.
429
+
430
+ ### Strategies (both callables accepting `(generator_or_nil) -> String`)
431
+
432
+ - `STRICT` — raises `GeneratorNotSetError` if the generator is nil. Use in
433
+ development/production paired with the `before_action` seed in `ApplicationController`.
434
+ - `RANDOM_FALLBACK` — returns `Random.hex(16)` when the generator is nil; otherwise
435
+ returns `generator.next.join("-")`. Use in test/previews/jobs/mailers.
436
+
437
+ ### Class methods
438
+
439
+ - `strategy` / `strategy=` — get/set the configured callable.
440
+ - `set_current_sequence_generator(seed:)` — seeds a per-thread generator. Raises
441
+ `ArgumentError` on `seed: nil`. Seed is MD5-hashed then fed to `Random.new`, so any
442
+ `String`-coercible seed works.
443
+ - `clear_current_sequence_generator` — clears the per-thread generator.
444
+ - `with_sequence_generator(seed:) { ... }` — scoped seed for a block (used by jobs,
445
+ mailers, Metal endpoints). Restores the previous generator on exit.
446
+ - `next_id_in_sequence` — delegates to the configured `strategy`.
447
+
448
+ ### Installation
449
+
450
+ `bin/rails generate vident:install` (file:
451
+ `lib/generators/vident/install/install_generator.rb`):
452
+
453
+ 1. Writes `config/initializers/vident.rb` setting `strategy` to `RANDOM_FALLBACK` in
454
+ test and `STRICT` everywhere else.
455
+ 2. Injects `before_action` + `after_action` into `ApplicationController` (idempotent —
456
+ skips if a previous install patched it).
457
+ 3. Copies `skills/vident/SKILL.md` from the gem to `.claude/skills/vident/SKILL.md`
458
+ in the host app (skipped if already present).
459
+
460
+ ---
461
+
462
+ ## 10. `Vident::Tailwind`
463
+
464
+ Included into every component. File: `lib/vident/tailwind.rb`.
465
+
466
+ - `tailwind_merger` — returns a thread-cached `::TailwindMerge::Merger` instance if
467
+ the `tailwind_merge` gem is loaded; otherwise returns `nil`.
468
+ - `tailwind_merge_available?` — `true` iff `::TailwindMerge::Merger` is defined.
469
+
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
472
+ opt-in is required beyond adding the gem to the Gemfile.
473
+
474
+ ---
475
+
476
+ ## 11. `class_list_for_stimulus_classes`
477
+
478
+ Instance method on every component. File: `lib/vident/component_class_lists.rb`.
479
+
480
+ ```ruby
481
+ class_list_for_stimulus_classes(*names) -> String
482
+ ```
483
+
484
+ Returns the resolved `data-*-class` values for the named stimulus-class entries,
485
+ deduplicated and (when `tailwind_merger` is available) Tailwind-merged. Intended
486
+ for inlining into `class=` on SSR so the first render has the same visual state the
487
+ JS controller will toggle on/off.
488
+
489
+ Names may be `Symbol` or `String`; both are normalised via `dasherize`.
490
+
491
+ ---
492
+
493
+ ## 12. Rails engine hooks
494
+
495
+ - `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.
499
+
500
+ ---
501
+
502
+ ## 13. What's not in the public API
503
+
504
+ The following show up in `lib/vident/*.rb` but are explicitly internal:
505
+
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`
511
+ resolution and `child_element`. Takes a collection-per-primitive kwarg hash and
512
+ 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` —
516
+ 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`.