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,505 @@
1
+ # Vident worked examples
2
+
3
+ End-to-end walkthroughs. Every example here runs against the public API verified in
4
+ `test/public_api_spec/` and the dummy app under `test/dummy/app/components/`. If a pattern
5
+ isn't shown below but is referenced in SKILL.md, it almost certainly shows up in
6
+ `test/dummy/app/components/dashboard/`.
7
+
8
+ The examples are grouped by shape:
9
+
10
+ 1. [Dashboard: outlets + scoped events + StimulusNull](#1-dashboard-outlets--scoped-events--stimulusnull) (Phlex)
11
+ 2. [Greeter with slot trigger: parent-child Stimulus wiring](#2-greeter-with-slot-trigger) (ViewComponent + Phlex)
12
+ 3. [ERB syntax: three ways to emit data attributes in a template](#3-erb-three-ways-to-emit-data-attributes)
13
+ 4. [Avatar: conditional root tag + class-list precedence](#4-avatar-conditional-root-tag--class-list-precedence) (Phlex)
14
+ 5. [Stimulus params on sibling buttons sharing one handler](#5-stimulus-params-on-sibling-buttons)
15
+
16
+ ---
17
+
18
+ ## 1. Dashboard: outlets + scoped events + StimulusNull
19
+
20
+ A page hosts many release cards; cards are filterable via a filter bar; selecting a card
21
+ opens a detail panel; promoting/cancelling a card fires a toast. Full source in
22
+ `test/dummy/app/components/dashboard/`.
23
+
24
+ ### Page (host of card outlets)
25
+
26
+ ```ruby
27
+ module Dashboard
28
+ class PageComponent < ApplicationComponent
29
+ prop :releases, _Array(Hash), default: -> { [] }
30
+ prop :active_filter, _Union(:all, :pending, :deployed, :failed), default: :all
31
+
32
+ stimulus do
33
+ values active_filter: -> { @active_filter.to_s },
34
+ count: -> { @releases.size }
35
+
36
+ # Listen to a scoped `filterChanged` event dispatched on window by FilterBar.
37
+ # Ruby side: reference the DISPATCHER's class.
38
+ actions -> { [FilterBarComponent.stimulus_scoped_event_on_window(:filter_changed), :handle_filter_changed] }
39
+ end
40
+
41
+ def view_template
42
+ root_element(class: "space-y-6") do |page|
43
+ render FilterBarComponent.new(active_filter: @active_filter, total: @releases.size)
44
+
45
+ div(class: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4") do
46
+ @releases.each do |release|
47
+ # `stimulus_outlet_host: page` is the child-registers-with-host hook:
48
+ # each card's initialize calls `page.add_stimulus_outlets(self)`, which
49
+ # writes a `data-dashboard--page-component-dashboard--release-card-component-outlet`
50
+ # onto the page root. No need to list cards in the page's `stimulus do`.
51
+ render ReleaseCardComponent.new(**release, stimulus_outlet_host: page)
52
+ end
53
+ end
54
+
55
+ render DetailPanelComponent.new
56
+ render ToastComponent.new
57
+ end
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ ```js
64
+ // dashboard/page_component_controller.js
65
+ export default class extends Controller {
66
+ static values = { activeFilter: String, count: Number }
67
+ static outlets = ["dashboard--release-card-component"]
68
+
69
+ handleFilterChanged(event) {
70
+ const { filter, query } = event.detail ?? {}
71
+ if (filter !== undefined) this.activeFilterValue = filter
72
+ this.#applyFilter(query ?? "")
73
+ }
74
+
75
+ // GOTCHA: do not iterate `this.dashboardReleaseCardComponentOutlets` inside
76
+ // dashboardReleaseCardComponentOutletConnected — Stimulus warns for each
77
+ // selector match whose controller hasn't attached yet. Iterate on real events.
78
+
79
+ #applyFilter(query) {
80
+ const q = query.trim().toLowerCase()
81
+ let visible = 0
82
+ for (const card of this.dashboardReleaseCardComponentOutlets) {
83
+ const show = (this.activeFilterValue === "all" || card.statusValue === this.activeFilterValue)
84
+ && (q === "" || card.nameValue.toLowerCase().includes(q))
85
+ card.setVisible(show)
86
+ if (show) visible += 1
87
+ }
88
+ this.countValue = visible
89
+ this.dispatch("filterApplied", { detail: { count: visible }, target: window })
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### Release card (self-registers with host, uses `classes` DSL + SSR)
95
+
96
+ ```ruby
97
+ module Dashboard
98
+ class ReleaseCardComponent < ApplicationComponent
99
+ prop :release_id, Integer
100
+ prop :name, String
101
+ prop :version, String
102
+ prop :environment, _Union(:production, :staging, :preview), default: :staging
103
+ prop :status, _Union(:pending, :deployed, :failed), default: :pending
104
+
105
+ # `stimulus_outlet_host:` is inherited from Vident::Component — no prop
106
+ # declaration needed in this class.
107
+
108
+ stimulus do
109
+ values_from_props :release_id, :name, :status
110
+
111
+ # Proc sees @status at render time; emits
112
+ # `data-<this-controller>-status-class="..."` for the JS side, AND the same
113
+ # value is inlined via `class_list_for_stimulus_classes(:status)` below
114
+ # for SSR first paint.
115
+ classes status: -> {
116
+ case @status
117
+ when :deployed then "border-green-500 bg-green-50"
118
+ when :failed then "border-red-500 bg-red-50"
119
+ else "border-yellow-400 bg-yellow-50"
120
+ end
121
+ }
122
+
123
+ actions [:click, :select]
124
+ end
125
+
126
+ def view_template
127
+ root_element(
128
+ class: "block cursor-pointer rounded-lg border-2 p-4 shadow-sm #{class_list_for_stimulus_classes(:status)}",
129
+ role: "button",
130
+ tabindex: 0
131
+ ) do |card|
132
+ # Two buttons share one `apply` handler. `event.params.kind` on the JS side
133
+ # tells them apart — see example 5 for the params idiom.
134
+ card.child_element(
135
+ :button,
136
+ stimulus_action: [:click, :apply],
137
+ stimulus_target: :promote_button,
138
+ stimulus_params: { kind: "promote" },
139
+ type: "button", class: "..."
140
+ ) { "Promote" }
141
+
142
+ card.child_element(
143
+ :button,
144
+ stimulus_action: [:click, :apply],
145
+ stimulus_target: :cancel_button,
146
+ stimulus_params: { kind: "cancel" },
147
+ type: "button", class: "..."
148
+ ) { "Cancel" }
149
+ end
150
+ end
151
+ end
152
+ end
153
+ ```
154
+
155
+ ```js
156
+ // dashboard/release_card_component_controller.js
157
+ export default class extends Controller {
158
+ static targets = ["promoteButton", "cancelButton"]
159
+ static values = { releaseId: Number, name: String, status: String }
160
+
161
+ select(event) {
162
+ if (event.target.closest("button")) return // let buttons handle themselves
163
+ this.dispatch("selected", { detail: this.#payload(), target: window })
164
+ }
165
+
166
+ apply(event) {
167
+ const kind = event.params.kind // "promote" | "cancel"
168
+ this.#disable()
169
+ this.dispatch(`${kind}d`, { detail: this.#payload(), target: window })
170
+ }
171
+
172
+ setVisible(show) { this.element.classList.toggle("hidden", !show) }
173
+
174
+ #payload() { return { releaseId: this.releaseIdValue, name: this.nameValue, status: this.statusValue } }
175
+ #disable() { this.promoteButtonTarget.disabled = true; this.cancelButtonTarget.disabled = true }
176
+ }
177
+ ```
178
+
179
+ ### Detail panel (StimulusNull + keyboard modifier action + alias resolution)
180
+
181
+ ```ruby
182
+ module Dashboard
183
+ class DetailPanelComponent < ApplicationComponent
184
+ stimulus do
185
+ # Vident::StimulusNull emits the literal string "null" as the data attribute
186
+ # value. Stimulus's Object parser runs it through JSON.parse, so `releaseValue`
187
+ # starts as JS `null` instead of the default `{}`. Use ONLY with Object/Array
188
+ # typed Stimulus values — for String/Number/Boolean the "null" string reads
189
+ # as garbage. A bare `nil` would omit the attribute entirely (Stimulus uses
190
+ # its per-type default); StimulusNull is an explicit "emit null" opt-in.
191
+ values release: -> { Vident::StimulusNull }
192
+
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
+
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
216
+ end
217
+
218
+ def view_template
219
+ root_element(class: class_list_for_stimulus_classes(:state)) do |panel|
220
+ panel.child_element(:button, stimulus_action: :close, type: "button") { "X" }
221
+ panel.child_element(:div, stimulus_target: :body, class: "mt-4 space-y-2") do
222
+ p(class: "italic text-gray-400") { "Click a release to see details." }
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ ```
229
+
230
+ ```js
231
+ // dashboard/detail_panel_component_controller.js
232
+ export default class extends Controller {
233
+ static targets = ["body"]
234
+ static values = { release: Object }
235
+
236
+ handleSelected(event) {
237
+ this.releaseValue = event.detail
238
+ this.#render()
239
+ this.element.classList.remove("translate-x-full")
240
+ }
241
+
242
+ close() { this.element.classList.add("translate-x-full") }
243
+
244
+ #render() {
245
+ const r = this.releaseValue
246
+ if (!r || !r.releaseId) return
247
+ this.bodyTarget.innerHTML = `<p>${r.name} — ${r.status}</p>`
248
+ }
249
+ }
250
+ ```
251
+
252
+ Identifier walk:
253
+ `Dashboard::ReleaseCardComponent` → class method `stimulus_identifier` returns
254
+ `"dashboard--release-card-component"`. `stimulus_scoped_event_on_window(:selected)`
255
+ returns the Symbol `:"dashboard--release-card-component:selected@window"`. On the JS
256
+ side, the card's `this.dispatch("selected", { target: window })` fires an event of type
257
+ `dashboard--release-card-component:selected` on window, matching exactly.
258
+
259
+ ---
260
+
261
+ ## 2. Greeter with slot trigger
262
+
263
+ Parent exposes a named slot; parent passes its own action descriptor into the slot at
264
+ render time so the slot triggers a method on the parent.
265
+
266
+ ### ViewComponent + ERB
267
+
268
+ ```ruby
269
+ # app/components/greeters/greeter_with_trigger_component.rb
270
+ module Greeters
271
+ class GreeterWithTriggerComponent < Vident::ViewComponent::Base
272
+ renders_one :trigger, GreeterButtonComponent
273
+
274
+ def root_element_attributes
275
+ {
276
+ stimulus_classes: {
277
+ pre_click: "text-md text-gray-500",
278
+ post_click: "text-xl text-blue-700"
279
+ }
280
+ }
281
+ end
282
+
283
+ # Default fallback — used when the consumer doesn't pass a custom trigger.
284
+ def default_trigger
285
+ GreeterButtonComponent.new(
286
+ before_clicked_message: "Click me to greet.",
287
+ after_clicked_message: "Greeted! Click to reset.",
288
+ stimulus_actions: [stimulus_action(:click, :greet)]
289
+ )
290
+ end
291
+ end
292
+ end
293
+ ```
294
+
295
+ ```erb
296
+ <%= root_element do |greeter| %>
297
+ <input type="text"
298
+ <%= greeter.as_stimulus_target(:name) %>
299
+ class="shadow appearance-none border rounded py-2 px-3">
300
+
301
+ <% if trigger? %>
302
+ <%= trigger %>
303
+ <% end %>
304
+
305
+ <%= greeter.child_element(:span, stimulus_target: :output,
306
+ class: "ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}") do %>
307
+ ...
308
+ <% end %>
309
+ <% end %>
310
+ ```
311
+
312
+ At the render site, the consumer can override the trigger while still wiring it to the
313
+ parent's action:
314
+
315
+ ```erb
316
+ <%= render GreeterWithTriggerComponent.new do |greeter| %>
317
+ <% greeter.with_trigger(
318
+ before_clicked_message: "Custom label",
319
+ stimulus_actions: [greeter.stimulus_action(:click, :greet)]
320
+ ) %>
321
+ <% end %>
322
+ ```
323
+
324
+ `greeter.stimulus_action(:click, :greet)` returns a `Vident::Stimulus::Action` whose
325
+ `controller` is the parent's (greeter's) identifier, so the click handler on the child's
326
+ button routes to `greeter-with-trigger-component#greet`, not to the child.
327
+
328
+ ### Phlex version
329
+
330
+ Same component, Phlex syntax:
331
+
332
+ ```ruby
333
+ module PhlexGreeters
334
+ class GreeterWithTriggerComponent < ApplicationComponent
335
+ def trigger(**args)
336
+ @trigger ||= GreeterButtonComponent.new(**args)
337
+ end
338
+
339
+ private
340
+
341
+ def trigger_or_default(greeter)
342
+ return render(@trigger) if @trigger
343
+
344
+ render(trigger(
345
+ before_clicked_message: "Greet",
346
+ stimulus_actions: [greeter.stimulus_action(:click, :greet)]
347
+ ))
348
+ end
349
+
350
+ def root_element_attributes
351
+ { stimulus_classes: { pre_click: "text-md text-gray-500", post_click: "text-xl text-blue-700" } }
352
+ end
353
+
354
+ def view_template(&)
355
+ vanish(&) # capture & discard the block content so consumers can call `#trigger` inside it
356
+ root_element do |greeter|
357
+ input(type: "text", data: { **greeter.stimulus_target(:name) })
358
+ trigger_or_default(greeter)
359
+ greeter.child_element(:span, stimulus_target: :output,
360
+ class: "ml-4 #{greeter.class_list_for_stimulus_classes(:pre_click)}")
361
+ end
362
+ end
363
+ end
364
+ end
365
+ ```
366
+
367
+ ---
368
+
369
+ ## 3. ERB: three ways to emit data attributes
370
+
371
+ ViewComponent/ERB users have three stylistic choices for attaching Stimulus wiring to
372
+ a hand-authored HTML tag. All three are equivalent; pick one per file for consistency.
373
+
374
+ ```erb
375
+ <%= root_element do |greeter| %>
376
+ <%# (a) Inline `as_stimulus_*` helpers — embed the raw data-* attributes directly in the HTML tag. %>
377
+ <%# Most compatible with better_html only if you allow embedded expressions inside tag bodies. %>
378
+ <input type="text"
379
+ <%= greeter.as_stimulus_target(:name) %>
380
+ class="...">
381
+ <button <%= greeter.as_stimulus_action([:click, :greet]) %>
382
+ class="...">
383
+ <%= @cta %>
384
+ </button>
385
+
386
+ <%# (b) Rails `content_tag` with `data:` spread — works anywhere `content_tag` does, %>
387
+ <%# plays nicely with strict HTML linters. Singular helpers return a Hash shape %>
388
+ <%# like { "data-greeter-target" => "name" }, spread with `**`. %>
389
+ <%= content_tag(:input, nil, type: "text", data: { **greeter.stimulus_target(:name) }) %>
390
+ <%= content_tag(:button, @cta, data: { **greeter.stimulus_action([:click, :greet]) }) %>
391
+
392
+ <%# (c) Vident's `child_element` helper — one call, tag + stimulus_* kwargs + block. %>
393
+ <%# Plural kwargs (`stimulus_actions:`) take an Enumerable; singular take one entry. %>
394
+ <%= greeter.child_element(:input, stimulus_target: :name, type: "text", class: "...") %>
395
+ <%= greeter.child_element(:button, stimulus_action: [:click, :greet], class: "...") do %>
396
+ <%= @cta %>
397
+ <% end %>
398
+ <% end %>
399
+ ```
400
+
401
+ Phlex users have two choices — `child_element` (identical) and the native Phlex tag
402
+ methods with `data: { **component.stimulus_target(:name) }`.
403
+
404
+ ---
405
+
406
+ ## 4. Avatar: conditional root tag + class-list precedence
407
+
408
+ Shows `element_tag:` varying by prop, `no_stimulus_controller`, `with_cache_key`, and
409
+ the full class-list precedence via `root_element_classes`.
410
+
411
+ ```ruby
412
+ module Phlex
413
+ class AvatarComponent < ApplicationComponent
414
+ no_stimulus_controller # don't emit the implied `data-controller`
415
+ with_cache_key # relies on ApplicationComponent `include Vident::Caching` — see api-reference.md
416
+
417
+ prop :url, _Nilable(String), predicate: :private, reader: :public
418
+ prop :initials, String, reader: :public
419
+ prop :shape, Symbol, default: :circle, reader: :public
420
+ prop :border, _Boolean, default: false, predicate: :private, reader: :public
421
+ prop :size, Symbol, default: :normal, reader: :public
422
+
423
+ private
424
+
425
+ def view_template
426
+ root_element do
427
+ span(class: "#{text_size_class} font-medium leading-none text-white") { @initials } unless image_avatar?
428
+ end
429
+ end
430
+
431
+ # Flip the root to <img> when a URL is given.
432
+ def root_element_attributes
433
+ {
434
+ element_tag: image_avatar? ? :img : :div,
435
+ html_options: default_html_options
436
+ }
437
+ end
438
+
439
+ def default_html_options
440
+ if image_avatar?
441
+ { class: "inline-block object-contain", src: @url, alt: "Profile image" }
442
+ else
443
+ { class: "inline-flex items-center justify-center bg-gray-500" }
444
+ end
445
+ end
446
+
447
+ # Lower precedence than `html_options[:class]` — wins only when `html_options` has no `:class`.
448
+ def root_element_classes
449
+ [size_classes, shape_class, (@border ? "border" : "")]
450
+ end
451
+
452
+ def image_avatar? = @url.present?
453
+ def shape_class = (@shape == :circle) ? "rounded-full" : "rounded-md"
454
+ def size_classes = { tiny: "w-6 h-6", small: "w-8 h-8", medium: "w-12 h-12" }[@size] || "w-10 h-10"
455
+ def text_size_class = (@size == :tiny || @size == :small) ? "text-xs" : "text-medium"
456
+ end
457
+ end
458
+ ```
459
+
460
+ Because this AvatarComponent sets `html_options[:class]` in `root_element_attributes`,
461
+ `root_element_classes` is NOT applied — `html_options[:class]` wins per the precedence
462
+ rules in SKILL.md §4. If you want the `root_element_classes` values kept AND extra
463
+ overrides, merge them yourself (see `PhlexGreeters::InheritedGreeterComponent` in the
464
+ dummy app for a `tailwind_merge`-aware merge).
465
+
466
+ ---
467
+
468
+ ## 5. Stimulus params on sibling buttons
469
+
470
+ Both buttons fire the same `apply` action on the parent card controller; the
471
+ per-button `stimulus_params:` tells the handler which one fired via `event.params.kind`:
472
+
473
+ ```ruby
474
+ card.child_element(:button,
475
+ stimulus_action: [:click, :apply],
476
+ stimulus_params: { kind: "promote" }) { "Promote" }
477
+
478
+ card.child_element(:button,
479
+ stimulus_action: [:click, :apply],
480
+ stimulus_params: { kind: "cancel" }) { "Cancel" }
481
+ ```
482
+
483
+ ```js
484
+ apply(event) {
485
+ const kind = event.params.kind // "promote" | "cancel"
486
+ this.dispatch(`${kind}d`, { detail: this.#payload(), target: window })
487
+ }
488
+ ```
489
+
490
+ **Element-scoped, not action-scoped.** In Stimulus, params live on the element, so every
491
+ action on the same element sees the same `event.params`. Vident mirrors this: `params`
492
+ is a sibling of `actions` in the DSL, not nested inside it. If you need per-action
493
+ params, split the buttons. This is usually preferable anyway — the shared-handler
494
+ pattern above is a tiny bit RPC-ish and is shown because params are useful to know
495
+ about, not because one action/two params is the recommended shape.
496
+
497
+ ---
498
+
499
+ ## Where to read more
500
+
501
+ - `test/dummy/app/components/dashboard/` — the full dashboard (5 components + JS) is
502
+ Vident's reference example. Every feature in SKILL.md is exercised there.
503
+ - `test/public_api_spec/specs/core_dsl.rb` — one locked-behaviour test per input shape
504
+ of every DSL primitive. Useful when unsure about an edge case.
505
+ - `test/dummy/app/components/greeters/` (ERB) and `test/dummy/app/components/phlex_greeters/` (Phlex) — side-by-side renditions of the same component in both engines.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vident
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
@@ -86,41 +86,53 @@ files:
86
86
  - lib/generators/vident/install/templates/vident.rb
87
87
  - lib/vident.rb
88
88
  - lib/vident/caching.rb
89
- - lib/vident/child_element_helper.rb
90
- - lib/vident/class_list_builder.rb
89
+ - lib/vident/capabilities/caching.rb
90
+ - lib/vident/capabilities/child_element_rendering.rb
91
+ - lib/vident/capabilities/class_list_building.rb
92
+ - lib/vident/capabilities/declarable.rb
93
+ - lib/vident/capabilities/identifiable.rb
94
+ - lib/vident/capabilities/inspectable.rb
95
+ - lib/vident/capabilities/root_element_rendering.rb
96
+ - lib/vident/capabilities/stimulus_data_emitting.rb
97
+ - lib/vident/capabilities/stimulus_declaring.rb
98
+ - lib/vident/capabilities/stimulus_draft.rb
99
+ - lib/vident/capabilities/stimulus_mutation.rb
100
+ - lib/vident/capabilities/stimulus_parsing.rb
101
+ - lib/vident/capabilities/tailwind.rb
91
102
  - lib/vident/component.rb
92
- - lib/vident/component_attribute_resolver.rb
93
- - lib/vident/component_class_lists.rb
94
103
  - lib/vident/engine.rb
104
+ - lib/vident/error.rb
105
+ - lib/vident/internals/action_builder.rb
106
+ - lib/vident/internals/attribute_writer.rb
107
+ - lib/vident/internals/class_list_builder.rb
108
+ - lib/vident/internals/declaration.rb
109
+ - lib/vident/internals/declarations.rb
110
+ - lib/vident/internals/draft.rb
111
+ - lib/vident/internals/dsl.rb
112
+ - lib/vident/internals/plan.rb
113
+ - lib/vident/internals/registry.rb
114
+ - lib/vident/internals/resolver.rb
115
+ - lib/vident/internals/target_builder.rb
95
116
  - lib/vident/stable_id.rb
96
- - lib/vident/stimulus.rb
117
+ - lib/vident/stimulus/action.rb
118
+ - lib/vident/stimulus/base.rb
119
+ - lib/vident/stimulus/class_map.rb
120
+ - lib/vident/stimulus/collection.rb
121
+ - lib/vident/stimulus/combinable.rb
122
+ - lib/vident/stimulus/controller.rb
97
123
  - lib/vident/stimulus/naming.rb
98
- - lib/vident/stimulus/primitive.rb
99
- - lib/vident/stimulus_action.rb
100
- - lib/vident/stimulus_action_collection.rb
101
- - lib/vident/stimulus_attribute_base.rb
102
- - lib/vident/stimulus_attributes.rb
103
- - lib/vident/stimulus_builder.rb
104
- - lib/vident/stimulus_class.rb
105
- - lib/vident/stimulus_class_collection.rb
106
- - lib/vident/stimulus_collection_base.rb
107
- - lib/vident/stimulus_component.rb
108
- - lib/vident/stimulus_controller.rb
109
- - lib/vident/stimulus_controller_collection.rb
110
- - lib/vident/stimulus_data_attribute_builder.rb
111
- - lib/vident/stimulus_helper.rb
124
+ - lib/vident/stimulus/null.rb
125
+ - lib/vident/stimulus/outlet.rb
126
+ - lib/vident/stimulus/param.rb
127
+ - lib/vident/stimulus/target.rb
128
+ - lib/vident/stimulus/value.rb
112
129
  - lib/vident/stimulus_null.rb
113
- - lib/vident/stimulus_outlet.rb
114
- - lib/vident/stimulus_outlet_collection.rb
115
- - lib/vident/stimulus_param.rb
116
- - lib/vident/stimulus_param_collection.rb
117
- - lib/vident/stimulus_target.rb
118
- - lib/vident/stimulus_target_collection.rb
119
- - lib/vident/stimulus_value.rb
120
- - lib/vident/stimulus_value_collection.rb
121
130
  - lib/vident/tailwind.rb
131
+ - lib/vident/types.rb
122
132
  - lib/vident/version.rb
123
133
  - skills/vident/SKILL.md
134
+ - skills/vident/api-reference.md
135
+ - skills/vident/examples.md
124
136
  homepage: https://github.com/stevegeek/vident
125
137
  licenses:
126
138
  - MIT
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Vident
4
- module ChildElementHelper
5
- # Explicit kwargs (14 of them, 1 plural + 1 singular per primitive) are the
6
- # public API — keep them so call-sites get typo-checking and IDE support.
7
- # The body is registry-driven via `Stimulus::PRIMITIVES`.
8
- def child_element(
9
- tag_name,
10
- stimulus_controllers: nil,
11
- stimulus_targets: nil,
12
- stimulus_actions: nil,
13
- stimulus_outlets: nil,
14
- stimulus_values: nil,
15
- stimulus_params: nil,
16
- stimulus_classes: nil,
17
- stimulus_controller: nil,
18
- stimulus_target: nil,
19
- stimulus_action: nil,
20
- stimulus_outlet: nil,
21
- stimulus_value: nil,
22
- stimulus_param: nil,
23
- stimulus_class: nil,
24
- **options,
25
- &block
26
- )
27
- inputs = {
28
- controllers: [stimulus_controllers, stimulus_controller],
29
- actions: [stimulus_actions, stimulus_action],
30
- targets: [stimulus_targets, stimulus_target],
31
- outlets: [stimulus_outlets, stimulus_outlet],
32
- values: [stimulus_values, stimulus_value],
33
- params: [stimulus_params, stimulus_param],
34
- classes: [stimulus_classes, stimulus_class]
35
- }
36
-
37
- collections = Stimulus::PRIMITIVES.to_h do |primitive|
38
- plural, singular = inputs.fetch(primitive.name)
39
- child_element_attribute_must_be_collection!(plural, primitive.key.to_s)
40
- args = primitive.keyed? ? [plural || singular] : child_element_wrap_single_stimulus_attribute(plural, singular)
41
- [primitive.name, send(primitive.key, *Array.wrap(args))]
42
- end
43
-
44
- data_attrs = StimulusDataAttributeBuilder.new(**collections).build
45
- generate_child_element(tag_name, data_attrs, options, &block)
46
- end
47
-
48
- private
49
-
50
- def child_element_attribute_must_be_collection!(collection, name)
51
- return unless collection
52
- raise ArgumentError, "'#{name}:' must be an enumerable. Did you mean '#{name.to_s.singularize}:'?" unless collection.is_a?(Enumerable)
53
- end
54
-
55
- def child_element_wrap_single_stimulus_attribute(plural, singular)
56
- return plural if plural
57
- singular.nil? ? nil : [singular]
58
- end
59
-
60
- def generate_child_element(tag_name, stimulus_data_attributes, options, &block)
61
- raise NoMethodError, "Not implemented"
62
- end
63
- end
64
- end