hibiki_rails 0.4.0 → 0.5.1

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +15 -2
  4. data/lib/generators/hibiki/rails/css_variant.rb +7 -4
  5. data/lib/generators/hibiki/rails/generator_helpers.rb +1 -2
  6. data/lib/generators/hibiki/rails/install/install_generator.rb +1 -2
  7. data/lib/generators/hibiki/rails/install/templates/hibiki_controller.js.tt +3 -6
  8. data/lib/generators/hibiki/rails/island/templates/channel.rb.tt +1 -3
  9. data/lib/generators/hibiki/rails/island/templates/display.html.erb.tt +1 -2
  10. data/lib/generators/hibiki/rails/island/templates/island.html.erb.tt +2 -6
  11. data/lib/generators/hibiki/rails/phlex/phlex_generator.rb +3 -4
  12. data/lib/generators/hibiki/rails/phlex/templates/channel.rb.tt +1 -4
  13. data/lib/generators/hibiki/rails/phlex/templates/component.rb.tt +3 -8
  14. data/lib/generators/hibiki/rails/phlex/templates/island_component.rb.tt +3 -5
  15. data/lib/generators/hibiki/rails/scaffold/USAGE +9 -5
  16. data/lib/generators/hibiki/rails/scaffold/scaffold_generator.rb +2 -0
  17. data/lib/generators/hibiki/rails/scaffold_controller/USAGE +15 -13
  18. data/lib/generators/hibiki/rails/scaffold_controller/scaffold_controller_generator.rb +64 -6
  19. data/lib/generators/hibiki/rails/scaffold_controller/templates/daisyui/views/_pagination.html.erb.tt +9 -37
  20. data/lib/generators/hibiki/rails/scaffold_controller/templates/none/views/_pagination.html.erb.tt +9 -24
  21. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/daisyui/views/pagination.rb.tt +54 -0
  22. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/none/views/pagination.rb.tt +56 -0
  23. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/controls.rb.tt +99 -0
  24. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/edit.rb.tt +24 -0
  25. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/field_error.rb.tt +15 -0
  26. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/form.rb.tt +63 -0
  27. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/index.rb.tt +45 -0
  28. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt +51 -0
  29. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/new.rb.tt +22 -0
  30. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row.rb.tt +70 -0
  31. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row_form.rb.tt +81 -0
  32. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/show.rb.tt +39 -0
  33. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/tailwind/views/pagination.rb.tt +60 -0
  34. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/channel.rb.tt +33 -131
  35. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/controller.rb.tt +14 -4
  36. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/form.rb.tt +5 -16
  37. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/hibiki_busy.css.tt +63 -0
  38. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/member_channel.rb.tt +11 -22
  39. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/query.rb.tt +16 -76
  40. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/row.rb.tt +3 -11
  41. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_controls.html.erb.tt +10 -41
  42. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_form.html.erb.tt +1 -5
  43. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_list.html.erb.tt +7 -33
  44. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row.html.erb.tt +10 -33
  45. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row_form.html.erb.tt +11 -34
  46. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/index.html.erb.tt +7 -26
  47. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/show.html.erb.tt +3 -9
  48. data/lib/generators/hibiki/rails/scaffold_controller/templates/tailwind/views/_pagination.html.erb.tt +9 -25
  49. data/lib/generators/hibiki/rails/scaffold_model_injection.rb +8 -21
  50. data/lib/generators/hibiki/rails/scaffold_parent_injection.rb +4 -21
  51. data/lib/generators/hibiki/rails/scaffold_parent_notices.rb +6 -9
  52. data/lib/generators/hibiki/rails/scaffold_phlex_helpers.rb +274 -0
  53. data/lib/generators/hibiki/rails/scaffold_post_install.rb +27 -15
  54. data/lib/generators/hibiki/rails/scaffold_schema.rb +11 -8
  55. data/lib/generators/hibiki/rails/scaffold_transport_stylesheet.rb +141 -0
  56. data/lib/generators/hibiki/rails/scaffold_view_helpers.rb +88 -23
  57. data/lib/generators/hibiki/rails/stimulus/templates/channel.rb.tt +1 -3
  58. data/lib/generators/hibiki/rails/stimulus/templates/display.html.erb.tt +1 -2
  59. data/lib/generators/hibiki/rails/stimulus/templates/island.html.erb.tt +1 -3
  60. data/lib/hibiki/rails/channel.rb +8 -4
  61. data/lib/hibiki/rails/version.rb +1 -1
  62. metadata +21 -6
  63. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_busy.html.erb.tt +0 -103
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b21e4afc4b4e74e30f3c32286a1a75b30052e55145d25210bb3cecfbb3bc24c
4
- data.tar.gz: 5c26612918d2b81c3a6a6a134710331ba656b25e9441856627ddf6b3ee093e71
3
+ metadata.gz: cd126c85e743e5ec9c89e54a1ffaba4cb04e6b6b0ef89868d60023168a0936a7
4
+ data.tar.gz: 00b4dfa1606348df1039cf5b69d1129c170fd418966b3db120964b71eba1e1c6
5
5
  SHA512:
6
- metadata.gz: acf106a0cda97184c5bd9a988bab71efb8be65c76e1f5b08785f96795d7a9dc21f67a5511a9a10ffa7b5fee49d498891ce7516b926eec1868d76820fd7c42c48
7
- data.tar.gz: 5e8d7b8aba5eaf643e5c6f82dcde5c824bd8f9eb1594163dd872bc0a304a6b37a796069385ec8fdc78e8bfe48dd7d9345d4f60a20b77fe3f72760d02e9b0157e
6
+ metadata.gz: 3c1e7d75c946f79cc898c20c2c3a8457ba78c152963f8b8155dd70a26308c138b31acc4b7a732e037d6bc28e933fb542a509d2dabee2e03602727d7e5e81b6d6
7
+ data.tar.gz: 6edf4361a4edbda81b933167cb6a7e08030cb50aac694c601a22c2210036eab5c56b101a449d3ea174a5485347e7c9c6e1cc86f9da123cbfe02e0e1ed34f41dd
data/CHANGELOG.md CHANGED
@@ -4,6 +4,132 @@ The gem and the npm package are released in lockstep and share these version
4
4
  numbers — `app/assets/javascripts/hibiki.js` is a single copy served both ways,
5
5
  so importmap and bundler apps always resolve identical client code.
6
6
 
7
+ ## 0.5.1 — 2026-08-08
8
+
9
+ ### Changed
10
+
11
+ **Generator output and messages slimmed down.** The scaffold templates used to
12
+ carry long design-rationale comments into every generated file; they are now
13
+ one-to-three-line hints, with the docs holding the prose. The safety notes a
14
+ user editing the file actually needs stayed: public channel methods are
15
+ client-invocable actions, ActionCable's exact-arity rule, the untrusted
16
+ subscribe param, Phlex omitting `false`-valued attributes, and the busy
17
+ stylesheet's do-not-wrap-in-a-layer rule.
18
+
19
+ The generators' status notices were shortened the same way — they still say
20
+ what to do, just not why at essay length.
21
+
22
+ No behavior change anywhere: no code inside any template moved, and neither
23
+ the runtime nor the packaged client changed (the npm 0.5.1 exists only to keep
24
+ the lockstep rule). Re-running a scaffold with `--force` rewrites the views
25
+ with the shorter comments; that diff is the whole upgrade.
26
+
27
+ ## 0.5.0 — 2026-08-05
28
+
29
+ ### Added
30
+
31
+ **`--phlex` on both scaffold generators.** `bin/rails g hibiki:rails:scaffold
32
+ Book title:string --phlex` emits Phlex components under `app/views/books/*.rb`,
33
+ namespaced `Views::`, instead of ERB templates. Purely additive: without the
34
+ flag nothing about the generated output changes, byte for byte.
35
+
36
+ Only the view layer moves. The channels, the query object, the ReactiveForm,
37
+ the model injections, every action and the whole `data-hibiki-*` protocol are
38
+ the same either way — the controller gains an explicit `render Views::…` at
39
+ each of its six render sites, and the two `broadcast_morph` calls swap
40
+ `partial:`/`locals:` for `renderable:`, and that is the entire difference
41
+ outside the templates.
42
+
43
+ Needs `phlex-rails` and `bin/rails g phlex:install`. The generator warns when
44
+ either is missing and writes the files anyway, so the scaffold can come first.
45
+
46
+ Four things worth knowing, because they are not what an ERB reader expects.
47
+ Phlex renders `String`, `Symbol`, `Integer` and `Float` and raises on anything
48
+ else, so date, time and decimal columns are emitted with an explicit `to_s`.
49
+ Phlex omits a `false`-valued attribute entirely, so the page control's
50
+ `data-turbo` is the string `"false"`. Phlex emits no whitespace between
51
+ siblings, so the components space their inline neighbours explicitly. And
52
+ `options_for_select` outputs directly and raises if its return value is passed
53
+ on, so both select sites take their options from a block.
54
+
55
+ The page control is still the one component with a per-`--css` fork, in both
56
+ trees. Under Phlex the plain-Tailwind fork's *reason* dissolves — a hoisted
57
+ template local becomes an ordinary constant — but it stays forked so the two
58
+ trees match file for file and a future `--css` decision stays a diff rather
59
+ than a judgement call.
60
+
61
+ ### Changed
62
+
63
+ **The loading and connection recipes are an asset, not a partial.** They were a
64
+ 103-line inline `<style>` emitted as `app/views/<resource>/_busy.html.erb` and
65
+ rendered once per page; they are now
66
+ `app/assets/stylesheets/hibiki_busy.css`, written once per app.
67
+
68
+ Per resource was always wrong: every rule keys on an attribute the client
69
+ stamps and none of them mentions a model, so a two-resource app carried two
70
+ byte-identical copies. It also put CSS somewhere a Content-Security-Policy that
71
+ forbids inline styles would reject.
72
+
73
+ The generator wires it for you: a cssbundling or tailwindcss-rails entry
74
+ stylesheet gets an `@import`, a layout already using
75
+ `stylesheet_link_tag :app` or `:all` needs nothing, and anything else gets a
76
+ `stylesheet_link_tag` injected into the layout. Only when none of those applies
77
+ does it print the line to add. Every branch is idempotent.
78
+
79
+ **If you re-run the generator on an app scaffolded before this**, the old
80
+ `_busy.html.erb` stays on disk — a generator never deletes — and nothing
81
+ renders it any more. The post-install output names it; delete it.
82
+
83
+ The rules are deliberately unlayered, and the file says so: two of them set
84
+ `display` on elements that also carry Tailwind utilities, and unlayered
85
+ declarations beat `@layer utilities` whatever the link order.
86
+
87
+ ### Fixed
88
+
89
+ **The npm package no longer drags in a second copy of `@rails/actioncable`.**
90
+ It moves from `dependencies` to `peerDependencies` at `>= 7.0`, matching
91
+ turbo-rails' own range.
92
+
93
+ Why there were two: `@rails/actioncable`'s npm `latest` dist-tag is 7.2.302 even
94
+ though 8.x is published, and resolvers prefer `latest` when it satisfies the
95
+ range. So turbo-rails' `>=7.0` took 7.2.302 while this package's `>= 8.0` was
96
+ forced up to 8.1.301, and both ended up in the bundle — about 16 KB of duplicate
97
+ client, and two separate module instances that could never share a consumer.
98
+ A fresh install happened to hoist a single copy; the duplicate appeared when
99
+ adding hibiki-rails to an app whose lockfile already pinned 7.2.302, which is
100
+ every existing app.
101
+
102
+ If your bundler warns about an unmet peer, install `@rails/actioncable`
103
+ explicitly — but a stock Rails app already has it via turbo-rails, and both bun
104
+ and npm 7+ auto-install a missing peer. The client uses only `createConsumer`,
105
+ `subscriptions.create` and `subscription.perform`, all stable since Action
106
+ Cable 6, so the lower floor changes nothing at runtime.
107
+
108
+ ### Changed — BREAKING
109
+
110
+ **The Rails floor is now 8.0.** `actioncable` and `railties` move from
111
+ `>= 7.1` to `>= 8.0`, and the 7.1 / 7.2 CI legs are gone. Ruby stays at `>= 3.4`.
112
+
113
+ This is a correction as much as a policy change: **generated controllers never
114
+ ran on Rails 7.** `hibiki:rails:scaffold` emits `params.expect` at two sites,
115
+ inherited from Rails 8's own scaffold, and `ActionController::Parameters#expect`
116
+ does not exist before 8.0 — so a generated controller raised `NoMethodError` on
117
+ the first request to `show`, `edit`, `update`, `create` or `destroy` on 7.1 and
118
+ 7.2. The generator suite never caught it because those specs assert on emitted
119
+ source text and never boot the result.
120
+
121
+ The alternative — branching the template on `Rails::VERSION` — was rejected: it
122
+ would also need a way to *execute* generated output on the old legs, which is
123
+ more work than dropping two CI legs for a version combination (Rails 7.x on
124
+ Ruby >= 3.4) that barely exists.
125
+
126
+ **If you are on Rails 7.1 or 7.2, stay on 0.4.0.** It remains available and is
127
+ unaffected; nothing in 0.5.0 is a security fix for it. Note that the *runtime*
128
+ half of the gem — channels, the graph, the broadcast helpers, the client — has
129
+ no known 8.0-only dependency; it is the generators whose output does. The floor
130
+ applies to the whole gem anyway, because shipping a gem whose headline generator
131
+ cannot run on its own declared floor is what got us here.
132
+
7
133
  ## 0.4.0 — 2026-08-03
8
134
 
9
135
  ### Added
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
 
@@ -80,6 +80,13 @@ match your app (DaisyUI, Tailwind, or unstyled — detected automatically, or
80
80
  forced with `--css=`). Run `bin/rails g hibiki:rails:scaffold --help` for the
81
81
  rest of the options.
82
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
+
83
90
  Listing the fields yourself only chooses their order and which ones appear — the
84
91
  model still answers everything else, so the live validation, a number field's
85
92
  `min:`/`max:` and a `belongs_to`'s display label all survive the choice.
@@ -90,6 +97,12 @@ each model a `belongs_to` points at gains the `has_many` half plus a ping of its
90
97
  own, so renaming a parent repaints the lists that print its name. Both are
91
98
  idempotent, announced, and leave anything you already declared alone.
92
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
+
93
106
  Restart the server afterwards: `app/forms/` is new, and Rails works out its
94
107
  autoload paths at boot.
95
108
 
@@ -107,7 +120,7 @@ Congratulations! Now you have your first reactive component!
107
120
 
108
121
  Documentation site: <https://planetaska.github.io/hibiki/rails-introduction/>
109
122
 
110
- Release notes and upgrade advice: [CHANGELOG.md](CHANGELOG.md). **If you run Rails 7.1 or 7.2, read the 0.3.0 entry** — it fixes channel lifecycle methods that were client-invocable on those versions.
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.
111
124
 
112
125
  ## Development
113
126
 
@@ -121,10 +121,13 @@ module Hibiki
121
121
  badge_warning: "inline-flex items-center rounded-full bg-amber-100 px-2 py-1 " \
122
122
  "text-xs font-medium text-amber-800",
123
123
  # `inline-block` is load-bearing, not decoration: a bare span is
124
- # inline, where h-3/w-3 do nothing. It is safe against the
125
- # visibility rules because this stylesheet is linked in <head> and
126
- # the transport <style> is in the body, so equal specificity goes
127
- # hibiki's way.
124
+ # inline, where h-3/w-3 do nothing. It also collides with
125
+ # .hbk-control-busy's `display: none` at equal specificity — the
126
+ # transport stylesheet wins because it is UNLAYERED and Tailwind's
127
+ # utilities are in @layer utilities, which beats them whatever the
128
+ # link order. (Before 0.5.0 the rules were a <style> in the body and
129
+ # won on document order instead; that is why the stylesheet's header
130
+ # now says not to wrap it in a layer.)
128
131
  spinner: "inline-block h-3 w-3 animate-spin rounded-full border-2 " \
129
132
  "border-current border-r-transparent align-[-0.15em]",
130
133
  warning_text: "text-amber-600",
@@ -71,8 +71,7 @@ module Hibiki
71
71
  def wiring_hint
72
72
  return if hibiki_registered? && helpers_included?
73
73
 
74
- say_status :hint, "the packaged client is not fully wired " \
75
- "(register line and/or Helpers include) — run: " \
74
+ say_status :hint, "the packaged client is not fully wired. Run: " \
76
75
  "bin/rails g hibiki:rails:install", :yellow
77
76
  end
78
77
  end
@@ -103,8 +103,7 @@ module Hibiki
103
103
 
104
104
  def bundler_note
105
105
  say_status :skip, "#{IMPORTMAP} not found — with a JS bundler, " \
106
- "npm/yarn add hibiki-rails instead (it pulls in " \
107
- "@rails/actioncable)", :yellow
106
+ "npm/yarn/bun add hibiki-rails instead", :yellow
108
107
  end
109
108
  end
110
109
  end
@@ -1,7 +1,4 @@
1
- // Registers the packaged hibiki client (the "hibiki-rails" module: the
2
- // engine's importmap pin, or the npm package in bundler apps) as the
3
- // "hibiki" Stimulus controller — the Ruby helpers hardcode that
4
- // identifier in data-controller. File-backed on purpose: `bin/rails
5
- // stimulus:manifest:update` re-derives this exact registration from the
6
- // filename, so it survives manifest rewrites.
1
+ // Registers the packaged hibiki client as "hibiki" Stimulus controller.
2
+ // `bin/rails stimulus:manifest:update` re-derives this,
3
+ // so it survives manifest rewrites.
7
4
  export { default } from "hibiki-rails"
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Generated by hibiki:rails:island — a working mini-example: one state,
4
- # one derived, one action, one broadcasting effect. The concern owns the
5
- # actor, the root, the per-action batch, and the [channel_name, cid]
6
- # stream convention; this class is just the graph and its actions.
4
+ # one derived, one action, one broadcasting effect.
7
5
  class <%= class_name %>Channel < ApplicationCable::Channel
8
6
  include Hibiki::Rails::Channel
9
7
 
@@ -1,5 +1,4 @@
1
- <%%# The root id is the broadcast replacement key — keep it in sync with the
2
- channel effect's target. %>
1
+ <%%# The root id is the replacement key. Keep in sync with channel effect's target. %>
3
2
  <p id="<%= display_dom_id %>">
4
3
  count: <strong><%%= count %></strong> &middot; doubled: <strong><%%= doubled %></strong>
5
4
  </p>
@@ -1,13 +1,9 @@
1
- <%%# Stamped through Hibiki::Rails::Helpers (hibiki_island / on) — never
2
- write the underlying data attributes by hand, they are private to the
3
- gem and version with its packaged JS. %>
1
+ <%%# Reactive island generated by hibiki_rails %>
4
2
  <%% cid = local_assigns.fetch(:cid) { SecureRandom.uuid } %>
5
3
  <%%= tag.div(**hibiki_island(<%= class_name %>Channel, cid:)) do %>
6
4
  <%%= turbo_stream_from "<%= stream_name %>", cid %>
7
5
 
8
- <%%# Paint-avoidance placeholder only: the controller subscribes AFTER the
9
- Turbo stream above confirms, so the graph's first broadcast replaces
10
- this. %>
6
+ <%%# Placeholder only: the graph's first broadcast replaces this. %>
11
7
  <%%= render "<%= view_dir %>/<%= file_name %>_display", count: 0, doubled: 0 %>
12
8
 
13
9
  <p><%%= tag.button("+1", **on(:increment)) %></p>
@@ -27,9 +27,8 @@ module Hibiki
27
27
  def warn_without_hibiki_phlex
28
28
  require "hibiki/phlex"
29
29
  rescue LoadError
30
- say_status :warn, "hibiki_phlex is not in this bundle — the generated channel " \
31
- "calls Hibiki::Phlex.render_effect. Add `gem \"hibiki_phlex\"` " \
32
- "(and `gem \"phlex-rails\"` to render components from views).", :yellow
30
+ say_status :warn, "hibiki_phlex is not in this bundle. " \
31
+ "Add `gem \"hibiki_phlex\"`.", :yellow
33
32
  end
34
33
 
35
34
  def create_channel
@@ -64,7 +63,7 @@ module Hibiki
64
63
  def register_hint
65
64
  return if hibiki_registered?
66
65
 
67
- say_status :hint, "the packaged client is not registered — run: " \
66
+ say_status :hint, "the packaged client is not registered. Run: " \
68
67
  "bin/rails g hibiki:rails:install", :yellow
69
68
  end
70
69
  end
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Generated by hibiki:rails:phlex — the component owns the state
4
- # (Hibiki::Reactive); the channel owns the transport. One render effect,
5
- # re-rendering THE SAME INSTANCE, its HTML transmitted over the channel's
6
- # own subscription and swapped in by the fragment's root DOM id.
3
+ # Generated by hibiki:rails:phlex
7
4
  class <%= class_name %>Channel < ApplicationCable::Channel
8
5
  include Hibiki::Rails::Channel
9
6
 
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Generated by hibiki:rails:phlex — a working mini-example. The component
4
- # is a plain Ruby object: Hibiki::Reactive gives it per-instance signals
5
- # read as ordinary method calls (no .value anywhere), Rerenderable lets
6
- # the channel's render effect re-render this same instance, and Helpers
7
- # stamps the client's wire protocol.
3
+ # Generated by hibiki:rails:phlex
8
4
  class Components::<%= class_name %> < Phlex::HTML
9
5
  include Hibiki::Reactive
10
6
  include Hibiki::Phlex::Rerenderable
@@ -14,11 +10,10 @@ class Components::<%= class_name %> < Phlex::HTML
14
10
  derived(:doubled) { count * 2 }
15
11
 
16
12
  def view_template
17
- # The root id is the swap key for transmitted fragments — page-unique.
13
+ # The root id is the swap key for transmitted fragments; keep page-unique.
18
14
  div(id: "<%= component_dom_id %>") do
19
15
  p { "count: #{count} · doubled: #{doubled}" }
20
- # Inside the swapped fragment on purpose: the island's root-scoped
21
- # delegation keeps the button working across replacements.
16
+ # The island's delegation keeps the button working across replacements.
22
17
  button(**on(:increment)) { "+1" }
23
18
  end
24
19
  end
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The island wrapper: one channel subscription per render, identified by
4
- # a per-page-load cid. Render from any page:
5
- #
3
+ # Generated by hibiki:rails:phlex
4
+ # Render from any page:
6
5
  # <%%= render Components::<%= class_name %>Island.new %>
7
6
  class Components::<%= class_name %>Island < Phlex::HTML
8
7
  include Hibiki::Rails::Helpers
9
8
 
10
9
  def view_template
11
10
  div(**hibiki_island(<%= class_name %>Channel, cid: SecureRandom.uuid)) do
12
- # A throwaway instance as a paint-avoidance placeholder; the
13
- # channel's long-lived instance takes over from its first transmit.
11
+ # Placeholder; the channel takes over from the first transmit.
14
12
  render Components::<%= class_name %>.new
15
13
  end
16
14
  end
@@ -8,9 +8,9 @@ Description:
8
8
  Everything is derived from the field list you pass, exactly as Rails'
9
9
  scaffold derives its own output. Stock `rails g scaffold` is untouched.
10
10
 
11
- The generated index is live: search, filter, sort and pagination are signal
12
- state, edits happen in place, and a write from anywhere — another tab,
13
- another user, the plain controller, a console — repaints every open list.
11
+ The generated index is live: search, filter, sort, and pagination are all
12
+ reactive, edits happen in place, and changes from anywhere update every open
13
+ list.
14
14
 
15
15
  Example:
16
16
  bin/rails generate hibiki:rails:scaffold Book title:string author:references
@@ -25,8 +25,12 @@ Example:
25
25
  app/forms/book_form.rb
26
26
  app/controllers/books_controller.rb
27
27
  app/views/books/*
28
+ app/assets/stylesheets/hibiki_busy.css
28
29
  And add to config/routes.rb:
29
30
  resources :books
30
31
 
31
- Restart the server afterwards: Rails computes autoload paths from the
32
- app/* glob at boot, and app/forms is new.
32
+ Restart the server afterwards if it is running — app/forms is new.
33
+
34
+ Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
35
+ ERB templates. The view layer is the only thing it changes; it needs the
36
+ phlex-rails gem and `bin/rails g phlex:install`.
@@ -45,6 +45,8 @@ module Hibiki
45
45
  class_option :skip_search, type: :boolean, default: false,
46
46
  desc: "Omit the search box and the LIKE terms behind it"
47
47
  class_option :page_size, type: :numeric, default: 20, desc: "Rows per page"
48
+ class_option :phlex, type: :boolean, default: false,
49
+ desc: "Emit Phlex components under app/views instead of ERB templates"
48
50
 
49
51
  def initialize(...)
50
52
  super
@@ -1,21 +1,18 @@
1
1
  Description:
2
- Generates a reactive CRUD resource for a model that ALREADY exists — the
3
- overlay half of hibiki:rails:scaffold.
2
+ Generates a reactive CRUD resource for a model that already exists.
4
3
 
5
- With no field list the model's own schema is read: columns and types,
6
- belongs_to reflections, and the validators behind per-field live errors.
7
- Pass fields explicitly to override that, or when the table has not been
8
- migrated yet.
4
+ With no field list, the model's schema is used to determine columns, types,
5
+ associations, and validators. Pass fields explicitly to override this or
6
+ when the table hasn't been migrated yet.
9
7
 
10
- The generated index is live: search, filter, sort and pagination are signal
11
- state, edits happen in place, and a write from anywhere repaints every open
8
+ The generated index is live: search, filter, sort, and pagination are all
9
+ reactive, edits happen in place, and changes from anywhere update every open
12
10
  list.
13
11
 
14
- Two models are MODIFIED. This one gets a delegate per belongs_to plus the
15
- after_commit broadcast the whole thing hangs off; each model a belongs_to
16
- POINTS AT gets the has_many half Rails' scaffold never writes, and a ping,
17
- because a row prints the parent's label rather than its id. Both are
18
- idempotent, both are announced, and anything already declared is left alone.
12
+ Two models are MODIFIED. This one gets a delegate for each belongs_to and
13
+ an after_commit broadcast. The related models get the missing has_many
14
+ association and a ping so changes are reflected here. Existing declarations
15
+ are left alone.
19
16
 
20
17
  Example:
21
18
  bin/rails generate hibiki:rails:scaffold_controller Book
@@ -28,7 +25,12 @@ Example:
28
25
  app/forms/book_form.rb
29
26
  app/controllers/books_controller.rb
30
27
  app/views/books/*
28
+ app/assets/stylesheets/hibiki_busy.css
31
29
  And modify:
32
30
  app/models/book.rb
33
31
  app/models/author.rb # each model a belongs_to points at
34
32
  config/routes.rb
33
+
34
+ Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
35
+ ERB templates. The view layer is the only thing it changes; it needs the
36
+ phlex-rails gem and `bin/rails g phlex:install`.
@@ -8,7 +8,9 @@ require_relative "../scaffold_view_helpers"
8
8
  require_relative "../scaffold_model_injection"
9
9
  require_relative "../scaffold_parent_injection"
10
10
  require_relative "../scaffold_parent_notices"
11
+ require_relative "../scaffold_phlex_helpers"
11
12
  require_relative "../scaffold_post_install"
13
+ require_relative "../scaffold_transport_stylesheet"
12
14
  require_relative "../scaffold_schema"
13
15
  require_relative "../css_variant"
14
16
 
@@ -29,10 +31,12 @@ module Hibiki
29
31
  include GeneratorHelpers
30
32
  include ScaffoldHelpers
31
33
  include ScaffoldViewHelpers
34
+ include ScaffoldPhlexHelpers
32
35
  include ScaffoldModelInjection
33
36
  include ScaffoldParentInjection
34
37
  include ScaffoldParentNotices
35
38
  include ScaffoldPostInstall
39
+ include ScaffoldTransportStylesheet
36
40
 
37
41
  TEMPLATE_ROOT = File.expand_path("templates", __dir__)
38
42
 
@@ -55,6 +59,8 @@ module Hibiki
55
59
  desc: "Rows per page"
56
60
  class_option :skip_routes, type: :boolean,
57
61
  desc: "Don't add routes to config/routes.rb"
62
+ class_option :phlex, type: :boolean, default: false,
63
+ desc: "Emit Phlex components under app/views instead of ERB templates"
58
64
 
59
65
  # Deliberately NO check_class_collision on the controller. Rails' own
60
66
  # scaffold_controller assumes a brand-new resource; this one's primary
@@ -62,6 +68,13 @@ module Hibiki
62
68
  # controller existing is the normal case rather than an error. Thor's
63
69
  # per-file conflict prompt is the right granularity for that.
64
70
 
71
+ # First, so a --phlex app missing its wiring reads that at the TOP of
72
+ # the output rather than under forty created-file lines. It only warns,
73
+ # so the files are written either way — see ScaffoldPhlexHelpers.
74
+ def check_phlex_wiring
75
+ warn_without_phlex_rails
76
+ end
77
+
65
78
  # Resolving the schema first means a missing model or an unmigrated
66
79
  # table aborts before anything is written, rather than half-way through.
67
80
  def resolve_schema
@@ -72,6 +85,14 @@ module Hibiki
72
85
  @new_app_dirs = %w[app/forms app/channels app/models app/views].reject { exists?(it) }
73
86
  end
74
87
 
88
+ # One file per app, not per resource, and the only thing this generator
89
+ # writes outside app/{channels,models,forms,views,controllers}. It
90
+ # carries no model knowledge at all — the rules key on the client's
91
+ # attributes — so a second resource finds it already there.
92
+ def create_stylesheet
93
+ create_transport_stylesheet
94
+ end
95
+
75
96
  def create_channels
76
97
  template "channel.rb.tt", collection_channel_path
77
98
  template "member_channel.rb.tt", member_channel_path
@@ -91,7 +112,9 @@ module Hibiki
91
112
  end
92
113
 
93
114
  def create_views
94
- %w[index show new edit _form _list _controls _field_error _busy].each do |view|
115
+ return create_phlex_views if phlex?
116
+
117
+ %w[index show new edit _form _list _controls _field_error].each do |view|
95
118
  template "views/#{view}.html.erb.tt", view_path("#{view}.html.erb")
96
119
  end
97
120
 
@@ -149,18 +172,48 @@ module Hibiki
149
172
 
150
173
  private
151
174
 
175
+ # No underscore, no .html.erb, and no _row/_row_form rename: all three
176
+ # are Rails PARTIAL conventions, and a Phlex component is reached by
177
+ # constant. Dropping the rename also drops a step — the file is
178
+ # row.rb because Zeitwerk wants basename.camelize, and calling it
179
+ # book.rb would put Views::Books::Book one reformat away from shadowing
180
+ # the model inside its own namespace.
181
+ #
182
+ # Private, because a public method on a Thor generator is a command.
183
+ def create_phlex_views
184
+ %w[index show new edit form list row row_form controls field_error].each do |view|
185
+ template "views/#{view}.rb.tt", view_path("#{view}.rb")
186
+ end
187
+
188
+ template "views/pagination.rb.tt", view_path("pagination.rb") unless infinite?
189
+ end
190
+
152
191
  # Ordered so an app's own lib/templates/... override wins first, then
153
- # the per-variant fork, then the one shared template set. Only
154
- # _pagination has a per-variant file — a tag-name change under `none`
155
- # and a shared-local extraction under `tailwind`, neither of which a
156
- # class-token map can express — so everything else falls through to
157
- # shared/ and there is exactly one template set to maintain.
192
+ # the view layer, then the per-variant fork, then the one shared
193
+ # template set. Only the page control has a per-variant file — a
194
+ # tag-name change under `none` and a shared-local extraction under
195
+ # `tailwind`, neither of which a class-token map can express — so
196
+ # everything else falls through to shared/.
197
+ #
198
+ # The two layers' templates never collide (list.rb.tt vs
199
+ # _list.html.erb.tt), so the phlex roots could have lived beside the
200
+ # ERB ones. They are a subtree instead because `ls` is where this
201
+ # phase's permanent cost should be visible: every view change is now
202
+ # two templates, forever.
158
203
  def source_paths
159
204
  @source_paths ||= [*self.class.source_paths_for_search,
205
+ *phlex_source_paths,
160
206
  File.join(TEMPLATE_ROOT, css_variant.to_s),
161
207
  File.join(TEMPLATE_ROOT, "shared")]
162
208
  end
163
209
 
210
+ def phlex_source_paths
211
+ return [] unless phlex?
212
+
213
+ [File.join(TEMPLATE_ROOT, "phlex", css_variant.to_s),
214
+ File.join(TEMPLATE_ROOT, "phlex", "shared")]
215
+ end
216
+
164
217
  # Explicit attributes always win, matching Rails' own precedence; the
165
218
  # schema decides the column list only when the argument list is empty.
166
219
  # That order also happens to be the only workable one, because
@@ -246,6 +299,11 @@ module Hibiki
246
299
 
247
300
  def infinite? = options[:infinite_scroll]
248
301
  def searchable? = schema.searchable? && !options[:skip_search]
302
+
303
+ # The view layer. It reaches the templates and nothing else: the graph,
304
+ # the query object, the form, the actions and the whole data-hibiki
305
+ # protocol are the same either way.
306
+ def phlex? = options[:phlex]
249
307
  end
250
308
  end
251
309
  end