hibiki_rails 0.5.0 → 0.6.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +117 -0
  3. data/lib/generators/hibiki/rails/css_variant.rb +11 -2
  4. data/lib/generators/hibiki/rails/generator_helpers.rb +1 -2
  5. data/lib/generators/hibiki/rails/install/install_generator.rb +1 -2
  6. data/lib/generators/hibiki/rails/install/templates/hibiki_controller.js.tt +3 -6
  7. data/lib/generators/hibiki/rails/island/templates/channel.rb.tt +1 -3
  8. data/lib/generators/hibiki/rails/island/templates/display.html.erb.tt +1 -2
  9. data/lib/generators/hibiki/rails/island/templates/island.html.erb.tt +2 -6
  10. data/lib/generators/hibiki/rails/phlex/phlex_generator.rb +3 -4
  11. data/lib/generators/hibiki/rails/phlex/templates/channel.rb.tt +1 -4
  12. data/lib/generators/hibiki/rails/phlex/templates/component.rb.tt +3 -8
  13. data/lib/generators/hibiki/rails/phlex/templates/island_component.rb.tt +3 -5
  14. data/lib/generators/hibiki/rails/scaffold/USAGE +4 -6
  15. data/lib/generators/hibiki/rails/scaffold_controller/USAGE +9 -14
  16. data/lib/generators/hibiki/rails/scaffold_controller/scaffold_controller_generator.rb +18 -20
  17. data/lib/generators/hibiki/rails/scaffold_controller/templates/daisyui/views/_pagination.html.erb.tt +21 -43
  18. data/lib/generators/hibiki/rails/scaffold_controller/templates/none/views/_pagination.html.erb.tt +21 -30
  19. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/daisyui/views/pagination.rb.tt +21 -51
  20. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/none/views/pagination.rb.tt +22 -49
  21. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/controls.rb.tt +9 -42
  22. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/field_error.rb.tt +8 -8
  23. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/form.rb.tt +4 -24
  24. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/form_errors.rb.tt +29 -0
  25. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/index.rb.tt +7 -21
  26. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt +24 -35
  27. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row.rb.tt +11 -35
  28. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row_form.rb.tt +14 -35
  29. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/show.rb.tt +3 -8
  30. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/tailwind/views/pagination.rb.tt +21 -55
  31. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/channel.rb.tt +35 -130
  32. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/controller.rb.tt +2 -4
  33. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/form.rb.tt +5 -16
  34. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/hibiki_busy.css.tt +20 -63
  35. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/member_channel.rb.tt +15 -23
  36. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/query.rb.tt +22 -80
  37. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_controls.html.erb.tt +10 -41
  38. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_field_error.html.erb.tt +6 -1
  39. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_form.html.erb.tt +2 -16
  40. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_form_errors.html.erb.tt +24 -0
  41. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_list.html.erb.tt +16 -34
  42. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row.html.erb.tt +11 -33
  43. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row_form.html.erb.tt +12 -35
  44. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/index.html.erb.tt +7 -21
  45. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/show.html.erb.tt +3 -9
  46. data/lib/generators/hibiki/rails/scaffold_controller/templates/tailwind/views/_pagination.html.erb.tt +21 -31
  47. data/lib/generators/hibiki/rails/scaffold_helpers.rb +13 -6
  48. data/lib/generators/hibiki/rails/scaffold_model_injection.rb +12 -50
  49. data/lib/generators/hibiki/rails/scaffold_parent_injection.rb +4 -21
  50. data/lib/generators/hibiki/rails/scaffold_parent_notices.rb +6 -9
  51. data/lib/generators/hibiki/rails/scaffold_phlex_helpers.rb +10 -10
  52. data/lib/generators/hibiki/rails/scaffold_post_install.rb +15 -16
  53. data/lib/generators/hibiki/rails/scaffold_schema.rb +4 -11
  54. data/lib/generators/hibiki/rails/scaffold_shared_views.rb +76 -0
  55. data/lib/generators/hibiki/rails/scaffold_transport_stylesheet.rb +23 -11
  56. data/lib/generators/hibiki/rails/stimulus/templates/channel.rb.tt +1 -3
  57. data/lib/generators/hibiki/rails/stimulus/templates/display.html.erb.tt +1 -2
  58. data/lib/generators/hibiki/rails/stimulus/templates/island.html.erb.tt +1 -3
  59. data/lib/hibiki/rails/engine.rb +8 -0
  60. data/lib/hibiki/rails/swallowed_write_warning.rb +43 -0
  61. data/lib/hibiki/rails/version.rb +1 -1
  62. data/lib/hibiki/rails.rb +30 -0
  63. metadata +7 -4
  64. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/row.rb.tt +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c47d1cec132b52f2a741ca77596ca90e524b348f385b0db5d2d2992ae0dba0c
4
- data.tar.gz: 44505ec6d36cb48589f8d924819d3d6ee02b2fb0092b67e0bd0c9b8af316da1b
3
+ metadata.gz: d708875aa67bf5d9b9137ac8d9bb05cf23604db936011706d068ec0b7116429c
4
+ data.tar.gz: 941b16f996127706d699e8812cf712b755c0f833f7a04c9e71912844421645ba
5
5
  SHA512:
6
- metadata.gz: 85985021ba53ce7e11eb4b6713905e8abee6c9e0c6babd36a1fd8debb06887994827cce847ae82a20df822d53ff14398be3f4515f416354ad1058f7562151037
7
- data.tar.gz: 031d605405895afed00c8119eabf62e7e545213a059f2d7e424f78fc08623939e550231168d829131f494d7bec3e9fa352f151cf41514700d1c8a90e3a68f6a0
6
+ metadata.gz: 74175d980c70f1b4ea1bb9ba593ef9940326eccfce6c783ceefdebfb2a0659706faa1040393759549366c5880ea23459740e0f3f280e64fb47a4bc32a371f69c
7
+ data.tar.gz: c88a903f15b13516310beb32a7ed741942c49ae65823a6422fb36823400a9fea420ae08bd2fe047bc1d2e6ff3e38cef8f86c0665eeee6014e0f495c880ad90a4
data/CHANGELOG.md CHANGED
@@ -4,6 +4,123 @@ 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.6.0 — 2026-08-10
8
+
9
+ ### Added
10
+
11
+ **`Hibiki::Rails.record_equals` — a per-signal comparator for ActiveRecord
12
+ records.** `ActiveRecord#==` compares class and id only, so a stale record is
13
+ `==` to its edited reload and a signal write carrying the fresh one is silently
14
+ dropped. Pass the comparator per signal and the write goes through:
15
+
16
+ ```ruby
17
+ state(:items, equals: Hibiki::Rails.record_equals) { fetch }
18
+ ```
19
+
20
+ It compares class + `attributes`, recurses through arrays, and falls back to
21
+ `==` for everything else. It needs hibiki 0.3.0's `equals:` (see the dependency
22
+ change below), which is honored at both of the graph's equality gates — the
23
+ write gate and the flush-time check — so a change that passes the comparator
24
+ actually reaches the page. This is opt-in sugar that makes the snapshot pattern
25
+ forgiving; "records stop at the boundary" stays the documented default, and a
26
+ comparator still cannot see an in-place mutation that never enters the write
27
+ path.
28
+
29
+ **A development-mode warning for the write that pattern swallows.** When a
30
+ `State` write is dropped by the default `==` but the old and new values'
31
+ `attributes` differ — the classic silent-stale-UI debugging session — the log
32
+ now says so and points at the docs. Development only (never test or
33
+ production), zero semantic change: the write is still dropped.
34
+
35
+ ### Changed
36
+
37
+ **The scaffold's row projection is gone — records now cross the boundary as
38
+ frozen snapshots.** `app/models/book_row.rb` is no longer generated. The query
39
+ object's `rows` returns real records, hardened at the boundary —
40
+
41
+ ```ruby
42
+ def rows
43
+ @rows ||= window_scope.strict_loading.map { it.readonly!; it.freeze }
44
+ end
45
+ ```
46
+
47
+ — and the channels' `rows`/`row` deriveds compare them with
48
+ `equals: Hibiki::Rails.record_equals`. What the `Data` projection's structural
49
+ `==` used to provide, the comparator provides; what it could never provide, the
50
+ freeze triple does: an attribute write raises `FrozenError`, `save` raises
51
+ `ActiveRecord::ReadOnlyRecord`, and an unpreloaded association walk raises
52
+ `ActiveRecord::StrictLoadingViolationError` instead of firing a lazy query off
53
+ the graph thread. Views print a `belongs_to` label as `book.author&.name`
54
+ directly, so **the scaffold no longer injects a `delegate` per `belongs_to`
55
+ into the model** — the model injection is the `after_commit` ping alone. The
56
+ member channel's fetch preloads its associations (`includes(...) +
57
+ strict_loading`) for the same reason.
58
+
59
+ Existing scaffolded apps keep working untouched: their `book_row.rb` and
60
+ delegates are app code, and the runtime reads none of it. Re-running a scaffold
61
+ with `--force` moves the resource over; the generator never deletes, so the
62
+ orphaned `*_row.rb` stays on disk for you to remove.
63
+
64
+ **The hibiki dependency floor is `~> 0.3`** (per-signal `equals:`, shipped in
65
+ hibiki 0.3.0). Everything 0.2 provided still holds; generated channels now rely
66
+ on the comparator being consulted at both equality gates.
67
+
68
+ **The page control and the field-error line are shared partials now.** Each
69
+ scaffold used to write its own copy per resource; both are presentation-only,
70
+ so they are emitted once per app instead — `app/views/shared/_pagination.html.erb`
71
+ and `_field_error.html.erb` (under `--phlex`: `Views::Shared::Pagination` and
72
+ `Views::Shared::FieldError` in `app/views/shared/`). Everything resource-shaped
73
+ reaches the page control as locals from the list partial. A second scaffold
74
+ finds the files present and leaves them; if they were generated under a
75
+ different `--css` style, a notice says so instead of silently restyling every
76
+ other resource's control.
77
+
78
+ **Pagination renders above the list as well as below**, so a long page starts
79
+ with a control in reach. Both copies live inside the re-rendered fragment and
80
+ carry distinct ids (`books_pagination_top` / `books_pagination`), so idiomorph
81
+ updates them in place.
82
+
83
+ Re-running a scaffold with `--force` moves the list over to the shared
84
+ partials and prints a notice naming the now-dead per-resource copies — the
85
+ generator never deletes them itself.
86
+
87
+ **The form's error summary is a shared partial too** —
88
+ `app/views/shared/_form_errors.html.erb` (under `--phlex`:
89
+ `Views::Shared::FormErrors`), rendered by every scaffolded full-page form. The
90
+ resource name comes off the record at render time, so one file serves every
91
+ scaffold. Under `--css=tailwind` and `--css=daisyui` the block is styled as a
92
+ red alert panel instead of the scaffold-stock `style="color: red"`, which
93
+ `--css=none` keeps.
94
+
95
+ ### Fixed
96
+
97
+ **The `hibiki_busy.css` import lands directly below `@import "tailwindcss";`**
98
+ instead of at the end of the entry stylesheet — appending placed it after
99
+ `@plugin` lines, and CSS requires every `@import` before any other statement.
100
+ Also fixed: re-running a scaffold in a tailwindcss-rails app
101
+ (`app/assets/tailwind/application.css`) duplicated the import on every run,
102
+ because the idempotence probe checked the wrong path spelling.
103
+
104
+ ## 0.5.1 — 2026-08-08
105
+
106
+ ### Changed
107
+
108
+ **Generator output and messages slimmed down.** The scaffold templates used to
109
+ carry long design-rationale comments into every generated file; they are now
110
+ one-to-three-line hints, with the docs holding the prose. The safety notes a
111
+ user editing the file actually needs stayed: public channel methods are
112
+ client-invocable actions, ActionCable's exact-arity rule, the untrusted
113
+ subscribe param, Phlex omitting `false`-valued attributes, and the busy
114
+ stylesheet's do-not-wrap-in-a-layer rule.
115
+
116
+ The generators' status notices were shortened the same way — they still say
117
+ what to do, just not why at essay length.
118
+
119
+ No behavior change anywhere: no code inside any template moved, and neither
120
+ the runtime nor the packaged client changed (the npm 0.5.1 exists only to keep
121
+ the lockstep rule). Re-running a scaffold with `--force` rewrites the views
122
+ with the shorter comments; that diff is the whole upgrade.
123
+
7
124
  ## 0.5.0 — 2026-08-05
8
125
 
9
126
  ### Added
@@ -87,10 +87,19 @@ module Hibiki
87
87
  spinner: "loading loading-spinner loading-xs",
88
88
  warning_text: "text-warning",
89
89
  error_text: "text-error text-sm mt-1",
90
+ # The full-form error summary. Plain Tailwind on purpose, so both
91
+ # styled variants share it via the merge — an addition (2026-08-09),
92
+ # not a transcription: the reference apps carried the scaffold-stock
93
+ # `style="color: red"` block, which --css=none still emits.
94
+ error_summary: "rounded-md border border-red-200 bg-red-50 p-4 mb-6",
95
+ error_summary_title: "text-sm font-semibold text-red-800",
96
+ error_summary_list: "mt-2 list-disc list-inside text-sm text-red-700",
90
97
  muted: "opacity-60 italic",
91
98
  muted_inline: "opacity-60",
92
99
  counts: "ml-auto text-sm opacity-70",
93
- pagination_nav: "join mt-2 mx-auto flex justify-center w-fit"
100
+ # my-, not mt-: the control renders above the list as well as below,
101
+ # and the top copy needs the gap on its other side.
102
+ pagination_nav: "join my-2 mx-auto flex justify-center w-fit"
94
103
  }.freeze
95
104
 
96
105
  # DaisyUI is a plugin over Tailwind, and the merge says so literally:
@@ -135,7 +144,7 @@ module Hibiki
135
144
  muted: "text-gray-500 italic",
136
145
  muted_inline: "text-gray-400",
137
146
  counts: "ml-auto text-sm text-gray-500",
138
- pagination_nav: "mt-2 mx-auto flex justify-center w-fit -space-x-px rounded-md shadow-sm"
147
+ pagination_nav: "my-2 mx-auto flex justify-center w-fit -space-x-px rounded-md shadow-sm"
139
148
  ).freeze
140
149
 
141
150
  # Every lookup misses, so every class argument is omitted entirely.
@@ -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
@@ -19,7 +19,6 @@ Example:
19
19
  db/migrate/XXXXXXXXXXXX_create_books.rb
20
20
  app/models/book.rb
21
21
  app/models/book_query.rb
22
- app/models/book_row.rb
23
22
  app/channels/books_channel.rb
24
23
  app/channels/book_channel.rb
25
24
  app/forms/book_form.rb
@@ -29,8 +28,7 @@ Example:
29
28
  And add to config/routes.rb:
30
29
  resources :books
31
30
 
32
- Restart the server afterwards: Rails computes autoload paths from the
33
- app/* glob at boot, and app/forms is new.
31
+ Restart the server afterwards if it is running — app/forms is new.
34
32
 
35
33
  Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
36
34
  ERB templates. The view layer is the only thing it changes; it needs the
@@ -1,28 +1,23 @@
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 an after_commit broadcast. The
13
+ related models get the missing has_many association and a ping so changes
14
+ are reflected here. Existing declarations are left alone.
19
15
 
20
16
  Example:
21
17
  bin/rails generate hibiki:rails:scaffold_controller Book
22
18
 
23
19
  This will create:
24
20
  app/models/book_query.rb
25
- app/models/book_row.rb
26
21
  app/channels/books_channel.rb
27
22
  app/channels/book_channel.rb
28
23
  app/forms/book_form.rb
@@ -10,6 +10,7 @@ require_relative "../scaffold_parent_injection"
10
10
  require_relative "../scaffold_parent_notices"
11
11
  require_relative "../scaffold_phlex_helpers"
12
12
  require_relative "../scaffold_post_install"
13
+ require_relative "../scaffold_shared_views"
13
14
  require_relative "../scaffold_transport_stylesheet"
14
15
  require_relative "../scaffold_schema"
15
16
  require_relative "../css_variant"
@@ -36,6 +37,7 @@ module Hibiki
36
37
  include ScaffoldParentInjection
37
38
  include ScaffoldParentNotices
38
39
  include ScaffoldPostInstall
40
+ include ScaffoldSharedViews
39
41
  include ScaffoldTransportStylesheet
40
42
 
41
43
  TEMPLATE_ROOT = File.expand_path("templates", __dir__)
@@ -100,7 +102,6 @@ module Hibiki
100
102
 
101
103
  def create_models
102
104
  template "query.rb.tt", query_path
103
- template "row.rb.tt", row_path
104
105
  end
105
106
 
106
107
  def create_form
@@ -112,27 +113,26 @@ module Hibiki
112
113
  end
113
114
 
114
115
  def create_views
115
- return create_phlex_views if phlex?
116
-
117
- %w[index show new edit _form _list _controls _field_error].each do |view|
118
- template "views/#{view}.html.erb.tt", view_path("#{view}.html.erb")
116
+ if phlex?
117
+ create_phlex_views
118
+ else
119
+ %w[index show new edit _form _list _controls].each do |view|
120
+ template "views/#{view}.html.erb.tt", view_path("#{view}.html.erb")
121
+ end
122
+
123
+ template "views/_row.html.erb.tt", view_path("_#{row_partial}.html.erb")
124
+ template "views/_row_form.html.erb.tt", view_path("_#{row_form_partial}.html.erb")
119
125
  end
120
126
 
121
- template "views/_row.html.erb.tt", view_path("_#{row_partial}.html.erb")
122
- template "views/_row_form.html.erb.tt", view_path("_#{row_form_partial}.html.erb")
123
-
124
- # Under --infinite-scroll the sentinel is inlined in _list and there
125
- # is no page control at all — not an empty one.
126
- template "views/_pagination.html.erb.tt", view_path("_pagination.html.erb") unless infinite?
127
+ # The page control and the field-error line, once per app — see
128
+ # ScaffoldSharedViews.
129
+ create_shared_views
127
130
  end
128
131
 
129
132
  # The model being scaffolded — one of two files this generator modifies
130
- # that the app already owned.
131
- #
132
- # It is not optional, and not only about the ping: the row partial
133
- # prints an association's LABEL, and the show page hands that partial a
134
- # live record — so without the delegate the show page raises on arrival.
135
- # Once we are editing the model anyway, the ping belongs here too.
133
+ # that the app already owned. It gets the after_commit ping, which is
134
+ # what makes another tab, another user, the plain controller and a
135
+ # console write all reach an open list.
136
136
  #
137
137
  # The alternative — printing it and hoping — fails SILENTLY: everything
138
138
  # works in one tab, and cross-tab plus out-of-band writes simply never
@@ -181,11 +181,9 @@ module Hibiki
181
181
  #
182
182
  # Private, because a public method on a Thor generator is a command.
183
183
  def create_phlex_views
184
- %w[index show new edit form list row row_form controls field_error].each do |view|
184
+ %w[index show new edit form list row row_form controls].each do |view|
185
185
  template "views/#{view}.rb.tt", view_path("#{view}.rb")
186
186
  end
187
-
188
- template "views/pagination.rb.tt", view_path("pagination.rb") unless infinite?
189
187
  end
190
188
 
191
189
  # Ordered so an app's own lib/templates/... override wins first, then
@@ -1,62 +1,40 @@
1
1
  <%#- The ONE component with a per-variant template. DaisyUI expresses a page
2
2
  item as two tokens (`join-item btn`), so each of the six variants below can
3
- repeat them inline — which is exactly what plain Tailwind cannot do. -%>
4
- <%%# locals: (page: 1, page_count: 1) -%>
5
- <%%# The numbered page control. It lives INSIDE the replaced fragment, like
6
- every other control in there: page_count is a server number, so a
7
- single-page list simply stops rendering this element. Placed outside the
8
- fragment it would never be re-rendered and could only ever change its TEXT
9
- (reactive values are textContent-only), leaving a control whose links point
10
- at pages that no longer exist.
3
+ repeat them inline — which is exactly what plain Tailwind cannot do.
11
4
 
12
- SCROLL. The links are anchors to the list, not buttons, and that IS the
13
- scroll answer — the client only preventDefaults `submit`, so the click both
14
- performs the action and does the browser's own fragment jump, which puts the
15
- top of the list back in view exactly as a full page load would. No client
16
- capability needed. data-turbo="false" keeps Turbo Drive out of it: a
17
- same-page hash with Turbo off is a plain scroll, not a navigation.
18
-
19
- No generation token — a page number is idempotent, so a double fire is one
20
- write and one render.
21
-
22
- `on` returns a { data: } hash, so an element needing its OWN data attributes
23
- has to MERGE rather than splat: a bare ** after `data: { turbo: false }`
24
- would silently replace it and the anchor would stop scrolling. %>
25
- <%%# PENDING. Nothing here asks for it: the client stamps data-hibiki-busy on
26
- the control that fired, so the clicked page link dims itself while the trip
27
- is out, and the swap that answers it destroys the attribute along with the
28
- link. The bar above the list is the other half — the anchor jump has
29
- already put it in view by then. %>
5
+ Emitted ONCE per app into app/views/shared/, so nothing here may name the
6
+ resource: everything resource-shaped (the wrapper id, the anchor target,
7
+ the page-number window) arrives as a local from the list partial. The
8
+ `--css=` marker below is how a later scaffold run detects a variant
9
+ mismatch — keep it. -%>
10
+ <%%# locals: (page: 1, page_count: 1, pages: [], anchor:, id:) -%>
11
+ <%%# Shared page control (--css=<%= css_variant %>), rendered above and below
12
+ every scaffolded list. The links are anchors to the list, so a click also
13
+ does the browser's own fragment jump; data-turbo="false" keeps Turbo Drive
14
+ out. `on` returns a { data: } hash — merge, don't splat, or the anchor
15
+ stops scrolling. %>
30
16
  <%% page_data = ->(n) { { turbo: false }.merge(on(:go_to_page, with: { page: n })[:data]) } %>
31
- <%%# The id-carrying wrapper stays put and stays BARE. It always renders, so
32
- idiomorph always has a node to pair with, and it holds no layout classes of
33
- its own — with no page control it is empty, and padding here would leave
34
- dead vertical space on a list that has none. %>
35
- <div id="<%= pagination_dom_id %>">
17
+ <%%# The wrapper always renders (so idiomorph has a node to pair with) and
18
+ stays bare — layout classes here would leave dead space when empty. %>
19
+ <div id="<%%= id %>">
36
20
  <%% if page_count > 1 %>
37
21
  <nav<%= css_attr(:pagination_nav) %> aria-label="Pagination">
38
- <%%# Prev/next stay rendered and go inert at the ends rather than
39
- disappearing, so the control keeps its width across a page change —
40
- one that reflows under the pointer is how you mis-click a list that
41
- just repainted. A span, not a disabled link: there is no page to
42
- name, so there is no action to stamp. %>
22
+ <%%# Prev/next go inert at the ends rather than disappearing, so the
23
+ control keeps its width across a page change. %>
43
24
  <%% if page == 1 %>
44
25
  <span class="join-item btn btn-disabled" aria-disabled="true">«</span>
45
26
  <%% else %>
46
- <%%= link_to "«", "#<%= list_dom_id %>", class: "join-item btn", rel: "prev",
27
+ <%%= link_to "«", anchor, class: "join-item btn", rel: "prev",
47
28
  "aria-label": "Previous page", data: page_data.(page - 1) %>
48
29
  <%% end %>
49
30
 
50
- <%% <%= query_class_name %>.page_numbers(page: page, page_count: page_count).each do |n| %>
31
+ <%% pages.each do |n| %>
51
32
  <%% if n.nil? %>
52
33
  <span class="join-item btn btn-disabled" aria-hidden="true">…</span>
53
34
  <%% elsif n == page %>
54
- <%%# aria-current is the only thing that tells a screen reader which
55
- page it is on: the visual cue is a class, and the link text is
56
- just a number either way. %>
57
35
  <span class="join-item btn btn-active" aria-current="page"><%%= n %></span>
58
36
  <%% else %>
59
- <%%= link_to n, "#<%= list_dom_id %>", class: "join-item btn",
37
+ <%%= link_to n, anchor, class: "join-item btn",
60
38
  "aria-label": "Page #{n}", data: page_data.(n) %>
61
39
  <%% end %>
62
40
  <%% end %>
@@ -64,7 +42,7 @@
64
42
  <%% if page == page_count %>
65
43
  <span class="join-item btn btn-disabled" aria-disabled="true">»</span>
66
44
  <%% else %>
67
- <%%= link_to "»", "#<%= list_dom_id %>", class: "join-item btn", rel: "next",
45
+ <%%= link_to "»", anchor, class: "join-item btn", rel: "next",
68
46
  "aria-label": "Next page", data: page_data.(page + 1) %>
69
47
  <%% end %>
70
48
  </nav>