hibiki_rails 0.3.0 → 0.5.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +340 -0
  3. data/README.md +76 -1
  4. data/app/assets/javascripts/hibiki.js +244 -10
  5. data/lib/generators/hibiki/rails/css_variant.rb +192 -0
  6. data/lib/generators/hibiki/rails/generator_helpers.rb +20 -1
  7. data/lib/generators/hibiki/rails/island/island_generator.rb +0 -8
  8. data/lib/generators/hibiki/rails/scaffold/USAGE +37 -0
  9. data/lib/generators/hibiki/rails/scaffold/scaffold_generator.rb +75 -0
  10. data/lib/generators/hibiki/rails/scaffold_controller/USAGE +39 -0
  11. data/lib/generators/hibiki/rails/scaffold_controller/scaffold_controller_generator.rb +310 -0
  12. data/lib/generators/hibiki/rails/scaffold_controller/templates/daisyui/views/_pagination.html.erb.tt +72 -0
  13. data/lib/generators/hibiki/rails/scaffold_controller/templates/none/views/_pagination.html.erb.tt +60 -0
  14. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/daisyui/views/pagination.rb.tt +89 -0
  15. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/none/views/pagination.rb.tt +88 -0
  16. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/controls.rb.tt +132 -0
  17. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/edit.rb.tt +24 -0
  18. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/field_error.rb.tt +20 -0
  19. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/form.rb.tt +71 -0
  20. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/index.rb.tt +59 -0
  21. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt +76 -0
  22. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/new.rb.tt +22 -0
  23. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row.rb.tt +94 -0
  24. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row_form.rb.tt +102 -0
  25. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/show.rb.tt +44 -0
  26. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/tailwind/views/pagination.rb.tt +99 -0
  27. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/channel.rb.tt +329 -0
  28. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/controller.rb.tt +89 -0
  29. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/form.rb.tt +40 -0
  30. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/hibiki_busy.css.tt +106 -0
  31. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/member_channel.rb.tt +61 -0
  32. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/query.rb.tt +204 -0
  33. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/row.rb.tt +27 -0
  34. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_controls.html.erb.tt +87 -0
  35. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_field_error.html.erb.tt +8 -0
  36. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_form.html.erb.tt +52 -0
  37. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_list.html.erb.tt +62 -0
  38. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row.html.erb.tt +67 -0
  39. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row_form.html.erb.tt +85 -0
  40. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/edit.html.erb.tt +13 -0
  41. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/index.html.erb.tt +42 -0
  42. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/new.html.erb.tt +12 -0
  43. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/show.html.erb.tt +27 -0
  44. data/lib/generators/hibiki/rails/scaffold_controller/templates/tailwind/views/_pagination.html.erb.tt +66 -0
  45. data/lib/generators/hibiki/rails/scaffold_helpers.rb +161 -0
  46. data/lib/generators/hibiki/rails/scaffold_model_injection.rb +102 -0
  47. data/lib/generators/hibiki/rails/scaffold_parent_injection.rb +185 -0
  48. data/lib/generators/hibiki/rails/scaffold_parent_notices.rb +96 -0
  49. data/lib/generators/hibiki/rails/scaffold_phlex_helpers.rb +276 -0
  50. data/lib/generators/hibiki/rails/scaffold_post_install.rb +191 -0
  51. data/lib/generators/hibiki/rails/scaffold_schema.rb +544 -0
  52. data/lib/generators/hibiki/rails/scaffold_transport_stylesheet.rb +141 -0
  53. data/lib/generators/hibiki/rails/scaffold_view_helpers.rb +231 -0
  54. data/lib/hibiki/rails/channel.rb +37 -5
  55. data/lib/hibiki/rails/version.rb +1 -1
  56. metadata +56 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45614c5c98f4439de6f22da45a49d163ab7a06eccf53ed36108ee04d6b5c7f78
4
- data.tar.gz: c1dd28764cc7462dbb7ceaa5d0b76e1a0371bd8cc5523fe54545533d7b759c36
3
+ metadata.gz: 5c47d1cec132b52f2a741ca77596ca90e524b348f385b0db5d2d2992ae0dba0c
4
+ data.tar.gz: 44505ec6d36cb48589f8d924819d3d6ee02b2fb0092b67e0bd0c9b8af316da1b
5
5
  SHA512:
6
- metadata.gz: 4908f9d337c86d575287ff2d21d3d74cd112334e893e8b042cb07e7e8a1de141ddd892b17b4b86d90ac2008ce8c7d9f4390fd1ebdd1cc1cafe88e75c6530c360
7
- data.tar.gz: 392976be271a02c7b5e354ec0d3e46117e1a8772cb0a68b99d4570f1f95309bc7abac3e0cd3ca38961eaea5eb849d4e4343effa55911629d367555949ac93408
6
+ metadata.gz: 85985021ba53ce7e11eb4b6713905e8abee6c9e0c6babd36a1fd8debb06887994827cce847ae82a20df822d53ff14398be3f4515f416354ad1058f7562151037
7
+ data.tar.gz: 031d605405895afed00c8119eabf62e7e545213a059f2d7e424f78fc08623939e550231168d829131f494d7bec3e9fa352f151cf41514700d1c8a90e3a68f6a0
data/CHANGELOG.md ADDED
@@ -0,0 +1,340 @@
1
+ # Changelog
2
+
3
+ The gem and the npm package are released in lockstep and share these version
4
+ numbers — `app/assets/javascripts/hibiki.js` is a single copy served both ways,
5
+ so importmap and bundler apps always resolve identical client code.
6
+
7
+ ## 0.5.0 — 2026-08-05
8
+
9
+ ### Added
10
+
11
+ **`--phlex` on both scaffold generators.** `bin/rails g hibiki:rails:scaffold
12
+ Book title:string --phlex` emits Phlex components under `app/views/books/*.rb`,
13
+ namespaced `Views::`, instead of ERB templates. Purely additive: without the
14
+ flag nothing about the generated output changes, byte for byte.
15
+
16
+ Only the view layer moves. The channels, the query object, the ReactiveForm,
17
+ the model injections, every action and the whole `data-hibiki-*` protocol are
18
+ the same either way — the controller gains an explicit `render Views::…` at
19
+ each of its six render sites, and the two `broadcast_morph` calls swap
20
+ `partial:`/`locals:` for `renderable:`, and that is the entire difference
21
+ outside the templates.
22
+
23
+ Needs `phlex-rails` and `bin/rails g phlex:install`. The generator warns when
24
+ either is missing and writes the files anyway, so the scaffold can come first.
25
+
26
+ Four things worth knowing, because they are not what an ERB reader expects.
27
+ Phlex renders `String`, `Symbol`, `Integer` and `Float` and raises on anything
28
+ else, so date, time and decimal columns are emitted with an explicit `to_s`.
29
+ Phlex omits a `false`-valued attribute entirely, so the page control's
30
+ `data-turbo` is the string `"false"`. Phlex emits no whitespace between
31
+ siblings, so the components space their inline neighbours explicitly. And
32
+ `options_for_select` outputs directly and raises if its return value is passed
33
+ on, so both select sites take their options from a block.
34
+
35
+ The page control is still the one component with a per-`--css` fork, in both
36
+ trees. Under Phlex the plain-Tailwind fork's *reason* dissolves — a hoisted
37
+ template local becomes an ordinary constant — but it stays forked so the two
38
+ trees match file for file and a future `--css` decision stays a diff rather
39
+ than a judgement call.
40
+
41
+ ### Changed
42
+
43
+ **The loading and connection recipes are an asset, not a partial.** They were a
44
+ 103-line inline `<style>` emitted as `app/views/<resource>/_busy.html.erb` and
45
+ rendered once per page; they are now
46
+ `app/assets/stylesheets/hibiki_busy.css`, written once per app.
47
+
48
+ Per resource was always wrong: every rule keys on an attribute the client
49
+ stamps and none of them mentions a model, so a two-resource app carried two
50
+ byte-identical copies. It also put CSS somewhere a Content-Security-Policy that
51
+ forbids inline styles would reject.
52
+
53
+ The generator wires it for you: a cssbundling or tailwindcss-rails entry
54
+ stylesheet gets an `@import`, a layout already using
55
+ `stylesheet_link_tag :app` or `:all` needs nothing, and anything else gets a
56
+ `stylesheet_link_tag` injected into the layout. Only when none of those applies
57
+ does it print the line to add. Every branch is idempotent.
58
+
59
+ **If you re-run the generator on an app scaffolded before this**, the old
60
+ `_busy.html.erb` stays on disk — a generator never deletes — and nothing
61
+ renders it any more. The post-install output names it; delete it.
62
+
63
+ The rules are deliberately unlayered, and the file says so: two of them set
64
+ `display` on elements that also carry Tailwind utilities, and unlayered
65
+ declarations beat `@layer utilities` whatever the link order.
66
+
67
+ ### Fixed
68
+
69
+ **The npm package no longer drags in a second copy of `@rails/actioncable`.**
70
+ It moves from `dependencies` to `peerDependencies` at `>= 7.0`, matching
71
+ turbo-rails' own range.
72
+
73
+ Why there were two: `@rails/actioncable`'s npm `latest` dist-tag is 7.2.302 even
74
+ though 8.x is published, and resolvers prefer `latest` when it satisfies the
75
+ range. So turbo-rails' `>=7.0` took 7.2.302 while this package's `>= 8.0` was
76
+ forced up to 8.1.301, and both ended up in the bundle — about 16 KB of duplicate
77
+ client, and two separate module instances that could never share a consumer.
78
+ A fresh install happened to hoist a single copy; the duplicate appeared when
79
+ adding hibiki-rails to an app whose lockfile already pinned 7.2.302, which is
80
+ every existing app.
81
+
82
+ If your bundler warns about an unmet peer, install `@rails/actioncable`
83
+ explicitly — but a stock Rails app already has it via turbo-rails, and both bun
84
+ and npm 7+ auto-install a missing peer. The client uses only `createConsumer`,
85
+ `subscriptions.create` and `subscription.perform`, all stable since Action
86
+ Cable 6, so the lower floor changes nothing at runtime.
87
+
88
+ ### Changed — BREAKING
89
+
90
+ **The Rails floor is now 8.0.** `actioncable` and `railties` move from
91
+ `>= 7.1` to `>= 8.0`, and the 7.1 / 7.2 CI legs are gone. Ruby stays at `>= 3.4`.
92
+
93
+ This is a correction as much as a policy change: **generated controllers never
94
+ ran on Rails 7.** `hibiki:rails:scaffold` emits `params.expect` at two sites,
95
+ inherited from Rails 8's own scaffold, and `ActionController::Parameters#expect`
96
+ does not exist before 8.0 — so a generated controller raised `NoMethodError` on
97
+ the first request to `show`, `edit`, `update`, `create` or `destroy` on 7.1 and
98
+ 7.2. The generator suite never caught it because those specs assert on emitted
99
+ source text and never boot the result.
100
+
101
+ The alternative — branching the template on `Rails::VERSION` — was rejected: it
102
+ would also need a way to *execute* generated output on the old legs, which is
103
+ more work than dropping two CI legs for a version combination (Rails 7.x on
104
+ Ruby >= 3.4) that barely exists.
105
+
106
+ **If you are on Rails 7.1 or 7.2, stay on 0.4.0.** It remains available and is
107
+ unaffected; nothing in 0.5.0 is a security fix for it. Note that the *runtime*
108
+ half of the gem — channels, the graph, the broadcast helpers, the client — has
109
+ no known 8.0-only dependency; it is the generators whose output does. The floor
110
+ applies to the whole gem anyway, because shipping a gem whose headline generator
111
+ cannot run on its own declared floor is what got us here.
112
+
113
+ ## 0.4.0 — 2026-08-03
114
+
115
+ ### Added
116
+
117
+ **Loading and connection state, stamped by the client.** The first growth of the
118
+ `data-hibiki-*` protocol since 0.3.0, and the first attributes in it that no Ruby
119
+ helper emits — the client writes them at runtime and app CSS reads them:
120
+
121
+ ```
122
+ island root data-hibiki-busy present while an action is in flight
123
+ aria-busy="true" the same fact, for assistive tech
124
+ data-hibiki-state connecting | ready | offline | stalled
125
+ firing control data-hibiki-busy on the control that started it
126
+ ```
127
+
128
+ Everything an app wants out of that is a descendant selector —
129
+ `[data-hibiki-busy] .spinner { display: inline-block }` — so per-row and
130
+ per-button feedback needs no server state and no `{#if loading}` branch. The
131
+ Ruby surface is unchanged: no new option on `on`, `hibiki_island` or `reactive`.
132
+
133
+ **Actions are acknowledged once their batch has run**, and the ack is what clears
134
+ the indicator. It cannot be "clear on the next render": the core's equality gate
135
+ (hibiki 0.2.0) lets an ordinary action produce zero bytes — paging to the page
136
+ you are already on, a search that does not change the query, a destroy of a row
137
+ another tab already deleted — so a render-based rule hangs forever on gestures
138
+ users make all the time. The ack sits in an `ensure`, so a raising action stops
139
+ the spinner too, and a subscription that has gone away acks `dropped: true` from
140
+ the cable thread instead, which is what lets the client tell "late" from "never".
141
+
142
+ A page running the 0.3.0 client sends no sequence number and gets no ack, so
143
+ nothing about this reaches an app that has not upgraded both halves.
144
+
145
+ **Actions performed before the subscription confirms are queued, not dropped.**
146
+ ActionCable's `Subscription#perform` silently returns false on a socket that is
147
+ not open yet, and on the Turbo-broadcast path that window is about three
148
+ serialised round trips — a click in it used to vanish. The queue covers the first
149
+ connect window only: after a reconnect the server rebuilds the graph with default
150
+ state, so replaying intent formed against the old one is worse than dropping it,
151
+ and the island reads `offline` for the whole gap instead.
152
+
153
+ Three class properties on `ChannelController` are the entire tuning surface —
154
+ `busyDelay` (150 ms before a trip is worth mentioning), `busyGrace` (60 ms for a
155
+ broadcast still in flight after its ack), `busyCeiling` (10 s before a trip is
156
+ declared stalled rather than cleared silently). Deliberately not Stimulus values
157
+ and not helper options; an app that wants different numbers subclasses and
158
+ re-registers.
159
+
160
+ The scaffold generators wire five sites to all of this — the counts line, the
161
+ pagination bar, the infinite-scroll sentinel, the destroy button and the
162
+ inline-edit Save — through a generated `_busy.html.erb` the app owns.
163
+
164
+ **`hibiki:rails:scaffold` and `hibiki:rails:scaffold_controller`** — a reactive
165
+ CRUD resource generated the way `rails g scaffold` generates a plain one.
166
+
167
+ ```sh
168
+ bin/rails g hibiki:rails:scaffold Book title:string author:references
169
+ bin/rails g hibiki:rails:scaffold_controller Book # an existing model
170
+ ```
171
+
172
+ The generated index is live: search, filter, sort and pagination are signal
173
+ state rather than page loads, rows edit in place, and a write from anywhere —
174
+ another tab, another user, the plain controller, a console — repaints every open
175
+ list. Everything is derived from the model's schema (columns, types, `belongs_to`
176
+ reflections, validators) or from the same `NAME field:type` argument list Rails'
177
+ own scaffold takes.
178
+
179
+ Stock `rails g scaffold` is untouched; these live under their own namespace.
180
+
181
+ Live per-field validation is derived only from rules the form can actually
182
+ evaluate before a round trip: presence, length, and numericality bounds that
183
+ carry no `allow_nil:`/`allow_blank:` exemption for the value in hand. A
184
+ validator gated on `if:`, `unless:` or `on:` depends on the record rather than
185
+ the field, so it contributes nothing live — it still runs at commit and still
186
+ lands in `#errors`, which the same per-field slots mirror. The clauses are
187
+ generated once; add validators and re-run `scaffold_controller` to pick them up.
188
+
189
+ **Field order is yours, and choosing it costs nothing.** With no field list the
190
+ columns follow the schema, which for an app built from `schema.rb` means
191
+ alphabetical. Passing them explicitly picks the order — and against a model that
192
+ already exists the generator still reads that model for everything else, so the
193
+ live clauses, a number field's `min:`/`max:` and a `belongs_to`'s display label
194
+ all survive the choice:
195
+
196
+ ```sh
197
+ bin/rails g hibiki:rails:scaffold_controller Author name:string bio:text age:integer
198
+ ```
199
+
200
+ A field the model has no column for is still generated, from the argument list
201
+ alone, and named in the post-install output — it may be a column whose migration
202
+ is still to come, and a silently missing field is the worse failure.
203
+
204
+ **A unique index with no uniqueness validator is called out.** A database
205
+ constraint is not a validator, and the generated form can only mirror what the
206
+ model checks — so without one, a duplicate raises `ActiveRecord::RecordNotUnique`
207
+ on the graph thread instead of showing a field error, and the round trip
208
+ completes having saved nothing. The generator names the column and the exact
209
+ `validates` line, for single and composite indexes alike.
210
+
211
+ Options: `--css=daisyui|tailwind|none` (detected when absent),
212
+ `--infinite-scroll`, `--skip-pagination`, `--skip-search`, `--page-size=N`,
213
+ `--skip-routes`.
214
+
215
+ Three notes on what it does to an app you already have.
216
+
217
+ **The model is modified** — one `delegate` per `belongs_to`, plus the
218
+ `after_commit` broadcast the whole thing hangs off.
219
+
220
+ **So is each model a `belongs_to` points at.** It gains the `has_many` half
221
+ Rails' own scaffold never writes (without it the generated destroy button raises
222
+ `InvalidForeignKey`) and a ping of its own, because a row prints the parent's
223
+ label rather than its id — rename an author and every open books index would
224
+ otherwise keep the old name. `dependent:` follows the association: `:destroy`
225
+ when it is required, `:nullify` when it is `optional: true`. That ping is
226
+ collection-grained, so an open *show* page keeps the old label until reload.
227
+
228
+ Both injections are idempotent and announced, and anything you already declared
229
+ is left alone — including a `dependent:` you chose yourself.
230
+
231
+ And **restart the server afterwards**: `app/forms/` is likely new, and Rails
232
+ computes autoload paths from the `app/*` glob at boot.
233
+
234
+ ### Fixed
235
+
236
+ **The model injection missed every namespaced model.** Thor anchors
237
+ `inject_into_class` on the class name as the file spells it, and the generator
238
+ passed the demodulized one — so `app/models/admin/book.rb`, which Rails writes as
239
+ `class Admin::Book < ApplicationRecord`, never matched. Silently: Thor rewrote
240
+ the file byte-identical and the generator reported a modification. The delegate
241
+ never landed, so the show page raised on arrival.
242
+
243
+ ## 0.3.0 — 2026-07-28
244
+
245
+ ### Security
246
+
247
+ **Channel lifecycle methods were client-invocable on Rails 7.1 and 7.2.**
248
+ Upgrade if you run hibiki_rails on either. Rails 8.x apps were never affected.
249
+
250
+ ActionCable builds a channel's client-invocable actions from the public methods
251
+ the class adds. The gem subtracted its lifecycle hooks through ActionCable's
252
+ `internal_methods` hook — but **that hook only exists on Rails 8.x**; on 7.1 and
253
+ 7.2 `action_methods` never consults it, so the override was silently inert and
254
+ the hooks stayed exposed.
255
+
256
+ Reachable by any client that can open a subscription, against its own
257
+ connection's graph. There is no cross-connection or cross-user data exposure;
258
+ the impact is resource exhaustion:
259
+
260
+ - `perform("build_graph")` — exposed on **every** affected app, since
261
+ `#build_graph` is always public. It re-runs the graph build outside
262
+ `Hibiki.root`, so the effects it creates are unowned and the dispose on
263
+ unsubscribe never reaches them, while the previous root is still held. Each
264
+ call leaks; repeated calls grow memory without bound.
265
+ - `perform("subscribed")` — exposed only where an app defined `#subscribed`
266
+ public, which the ActiveRecord guide's own `after_commit` example did until
267
+ this release. It allocates a second `GraphActor` — a new **thread** — and
268
+ overwrites the reference to the first, so the original is never stopped.
269
+ Repeated calls exhaust the process's threads.
270
+
271
+ Fixed by subtracting in `action_methods` itself, which works on every supported
272
+ version:
273
+
274
+ ```ruby
275
+ HIDDEN_ACTIONS = %w[build_graph subscribed unsubscribed].freeze
276
+ def action_methods = super - HIDDEN_ACTIONS
277
+ ```
278
+
279
+ No application change is required. Writing `#subscribed` and `#unsubscribed`
280
+ private is still the better habit, because it also protects the methods this
281
+ list does not know about — the guides now show them that way.
282
+
283
+ ### Added
284
+
285
+ - `on(action, event:)` takes an **event list**, so one element can answer
286
+ several events: `on(:load_more, event: %i[click visible])`.
287
+ - **`input`** joins the delegated events, with a per-control **`debounce:`**.
288
+ `:input` carries 250 ms unless told otherwise (`debounce: 0` opts out); the
289
+ value is stamped into the markup rather than being an invisible client
290
+ default.
291
+ - **`visible`**, a pseudo-event backed by an `IntersectionObserver`, so a
292
+ load-more control can double as an infinite-scroll sentinel. It fires once
293
+ per observation and re-attaches to the replacement element after each
294
+ fragment swap.
295
+ - **`confirm:`** on `on(...)` — a `window.confirm` gate. `data-turbo-confirm`
296
+ does nothing on a hibiki control, since it is not a Turbo-driven form.
297
+ - **`reset: false`** on a submit, to keep a form's inputs. The default resets
298
+ them synchronously, before the server has replied, which is right for an
299
+ "add" form and destroys an edit form's contents on a failed commit.
300
+ - **Subscribe params**: `hibiki_island(channel, cid:, params: { record_id: })`
301
+ reach the channel as `params[:key]`, which is how a channel learns which
302
+ record its page is about. They are client-supplied and untrusted — use one
303
+ only to look up a record inside a scope the channel chooses, and never
304
+ interpolate one into a streamable name. They cannot override `channel` or
305
+ `cid`.
306
+ - The client has its own test suite (`spec/js`, vitest + happy-dom) and a CI
307
+ job, plus a pinned Rails 8.1 matrix leg.
308
+
309
+ ### Changed
310
+
311
+ - **A changed checkbox now sends its checked state as a boolean**, and a
312
+ multi-select sends an array of its selected values. Previously both sent
313
+ `control.value` — the value *attribute* — so checking and unchecking a
314
+ checkbox produced byte-identical payloads. **This is a payload shape
315
+ change**: an action reading a `change` payload for a checkbox now receives
316
+ `true`/`false` rather than `"1"`. The form-submit path is unaffected.
317
+ - `transmit_value` is **equality-gated**: an effect re-runs whenever any signal
318
+ it read changed, so a bumped version token used to re-send every reactive
319
+ value's text even when byte-identical. The block still runs unconditionally,
320
+ so dependency collection is unchanged — only the transmit is skipped. If you
321
+ render a reactive placeholder *inside* a broadcast-replaced fragment, render
322
+ it with its current value: the swap resets the DOM text and the gate now
323
+ suppresses the re-send that used to heal it.
324
+ - Each graph job runs inside `Rails.application.executor`, so
325
+ `CurrentAttributes` are reset between jobs, autoloads are safe off the main
326
+ thread, and each job gets its own query cache.
327
+ - Graph-thread errors are **logged** in development and test.
328
+ `ActiveSupport::ErrorReporter` has no subscribers by default, so in a stock
329
+ app these previously vanished entirely — no line, no stack, just a fragment
330
+ that stopped updating. Production behaviour is unchanged.
331
+
332
+ ## 0.2.0 — 2026-07-21
333
+
334
+ - Reactive values: `reactive` / `reactive_attrs` / `transmit_value`, matched
335
+ document-wide so a value can render outside the island that computes it.
336
+ - `Components::` namespacing for the Phlex generator.
337
+
338
+ ## 0.1.0 — 2026-07-18
339
+
340
+ Initial release.
data/README.md CHANGED
@@ -10,7 +10,7 @@ Turbo Streams broadcast → Turbo morphs the DOM
10
10
 
11
11
  A graph lives per cable connection (in practice: per browser tab), built when the channel subscribes and disposed when it unsubscribes. Effects subscribe to whatever signals they read; when an action writes a signal, exactly the affected effects re-render and broadcast.
12
12
 
13
- Supports Rails >= 7.1, Ruby >= 3.4.
13
+ Supports Rails >= 8.0, Ruby >= 3.4.
14
14
 
15
15
  ## Rails quick start
16
16
 
@@ -59,6 +59,53 @@ bin/rails g hibiki:rails:stimulus counter static_pages
59
59
 
60
60
  This creates a minimal working reactive component in the given view path.
61
61
 
62
+ ### Generating a whole CRUD resource
63
+
64
+ For a full reactive resource — a live index with search, filtering, sorting and
65
+ pagination, plus edit-in-place — use the scaffold generators:
66
+
67
+ ```sh
68
+ # Model, migration, route and the reactive resource, like rails g scaffold
69
+ bin/rails g hibiki:rails:scaffold Book title:string author:references
70
+
71
+ # Or, for a model you already have — the schema is read for you
72
+ bin/rails g hibiki:rails:scaffold_controller Book
73
+
74
+ # Same, but you pick the field order; everything else still comes from the model
75
+ bin/rails g hibiki:rails:scaffold_controller Book title:string author:references
76
+ ```
77
+
78
+ Your plain `rails g scaffold` is untouched. The generated markup is styled to
79
+ match your app (DaisyUI, Tailwind, or unstyled — detected automatically, or
80
+ forced with `--css=`). Run `bin/rails g hibiki:rails:scaffold --help` for the
81
+ rest of the options.
82
+
83
+ Pass `--phlex` for Phlex components under `app/views/books/*.rb` instead of ERB
84
+ templates. It needs `phlex-rails` and `bin/rails g phlex:install`, and it
85
+ changes the view layer only — the channels, the query object, the form and the
86
+ whole client protocol are the same either way. Note this is a different thing
87
+ from the `hibiki_phlex` gem, which makes a component own reactive state; here
88
+ the channel owns the state and the components are ordinary stateless views.
89
+
90
+ Listing the fields yourself only chooses their order and which ones appear — the
91
+ model still answers everything else, so the live validation, a number field's
92
+ `min:`/`max:` and a `belongs_to`'s display label all survive the choice.
93
+
94
+ Your models are edited, not just read: the one being scaffolded gains the
95
+ `after_commit` broadcast that makes writes from anywhere reach an open list, and
96
+ each model a `belongs_to` points at gains the `has_many` half plus a ping of its
97
+ own, so renaming a parent repaints the lists that print its name. Both are
98
+ idempotent, announced, and leave anything you already declared alone.
99
+
100
+ One file lands outside the resource: `app/assets/stylesheets/hibiki_busy.css`,
101
+ which styles the loading and connection state the client stamps. It is written
102
+ once per app, shared by every generated resource, and wired into your stylesheet
103
+ or layout automatically — the post-install output says which, or gives you the
104
+ line to add when it cannot tell.
105
+
106
+ Restart the server afterwards: `app/forms/` is new, and Rails works out its
107
+ autoload paths at boot.
108
+
62
109
  ### Render the reactive component
63
110
 
64
111
  The generated components are just Rails partials (or Phlex components, if you used the Phlex generator), so you can render one anywhere like any other partial:
@@ -73,6 +120,8 @@ Congratulations! Now you have your first reactive component!
73
120
 
74
121
  Documentation site: <https://planetaska.github.io/hibiki/rails-introduction/>
75
122
 
123
+ Release notes and upgrade advice: [CHANGELOG.md](CHANGELOG.md). **Rails 7.1 and 7.2 are supported up to 0.4.0 only** — 0.5.0 raises the floor to 8.0. If you are staying on 0.4.0 with Rails 7.x, read the 0.3.0 entry: it fixes channel lifecycle methods that were client-invocable on those versions.
124
+
76
125
  ## Development
77
126
 
78
127
  ```
@@ -84,3 +133,29 @@ Both are what CI runs. The Ruby suite boots a minimal inline Rails app (`spec/su
84
133
 
85
134
  The gem and the npm package are **released in lockstep**: `app/assets/javascripts/hibiki.js` is the single copy — the engine puts it on the asset path and `package.json` points `main`/`module`/`exports` at it — so importmap and bundler apps must never be able to resolve different client code. Bump `lib/hibiki/rails/version.rb` and `package.json` in the same commit, and publish both. The version table lives in [the JS client docs](https://planetaska.github.io/hibiki/the-js-client/).
86
135
 
136
+ ## Contributing
137
+
138
+ Bug reports and pull requests are welcome at <https://github.com/planetaska/hibiki-rails>.
139
+
140
+ A few things that make a change easier to accept:
141
+
142
+ - **`bundle exec rake` and `bun run test` both green.** They are what CI runs, and the client half is easy to forget — most changes here touch one side, but the wire protocol is shared by both.
143
+ - **A regression spec first** for anything that was a bug. `spec/js/` covers the client, `spec/generators/` the generated output.
144
+ - **Generated code follows `rubocop-rails-omakase`**, which is what a stock Rails app lints with — not this gem's own style. The templates are written to satisfy the app's linter, not ours.
145
+ - **The `data-hibiki-*` attributes are a private contract** between the Ruby helpers and the vendored JS, and the two halves ship in one version — so a change to either side belongs in one commit with the other. Two of them, `data-hibiki-busy` and `data-hibiki-state`, are written by the client rather than by a helper, and apps select on them from CSS; those need a CHANGELOG entry even when no Ruby changes.
146
+
147
+ ### Running against a checkout
148
+
149
+ The gem side needs nothing special: point an app's Gemfile at your clone with `gem "hibiki_rails", path: "../hibiki-rails"`.
150
+
151
+ The npm side has one trap. `bun link` from an app resolves the linked package's own imports from the **symlink's realpath**, not from the app — so `hibiki.js` looks for `@rails/actioncable` inside your checkout rather than inside the app that linked it, and if the checkout has no `node_modules` the import fails at build time with nothing pointing at the cause.
152
+
153
+ ```sh
154
+ cd hibiki-rails && bun install # before linking, not after
155
+ ```
156
+
157
+ This affects only development against a clone. Anyone installing the published package resolves normally and never sees it.
158
+
159
+ ## License
160
+
161
+ [MIT](LICENSE.txt)