hibiki_rails 0.5.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/lib/generators/hibiki/rails/generator_helpers.rb +1 -2
  4. data/lib/generators/hibiki/rails/install/install_generator.rb +1 -2
  5. data/lib/generators/hibiki/rails/install/templates/hibiki_controller.js.tt +3 -6
  6. data/lib/generators/hibiki/rails/island/templates/channel.rb.tt +1 -3
  7. data/lib/generators/hibiki/rails/island/templates/display.html.erb.tt +1 -2
  8. data/lib/generators/hibiki/rails/island/templates/island.html.erb.tt +2 -6
  9. data/lib/generators/hibiki/rails/phlex/phlex_generator.rb +3 -4
  10. data/lib/generators/hibiki/rails/phlex/templates/channel.rb.tt +1 -4
  11. data/lib/generators/hibiki/rails/phlex/templates/component.rb.tt +3 -8
  12. data/lib/generators/hibiki/rails/phlex/templates/island_component.rb.tt +3 -5
  13. data/lib/generators/hibiki/rails/scaffold/USAGE +4 -5
  14. data/lib/generators/hibiki/rails/scaffold_controller/USAGE +10 -13
  15. data/lib/generators/hibiki/rails/scaffold_controller/templates/daisyui/views/_pagination.html.erb.tt +9 -37
  16. data/lib/generators/hibiki/rails/scaffold_controller/templates/none/views/_pagination.html.erb.tt +9 -24
  17. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/daisyui/views/pagination.rb.tt +10 -45
  18. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/none/views/pagination.rb.tt +11 -43
  19. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/controls.rb.tt +9 -42
  20. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/field_error.rb.tt +0 -5
  21. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/form.rb.tt +2 -10
  22. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/index.rb.tt +7 -21
  23. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt +9 -34
  24. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row.rb.tt +11 -35
  25. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row_form.rb.tt +13 -34
  26. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/show.rb.tt +3 -8
  27. data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/tailwind/views/pagination.rb.tt +10 -49
  28. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/channel.rb.tt +32 -129
  29. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/controller.rb.tt +2 -4
  30. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/form.rb.tt +5 -16
  31. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/hibiki_busy.css.tt +20 -63
  32. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/member_channel.rb.tt +10 -20
  33. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/query.rb.tt +16 -76
  34. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/row.rb.tt +3 -11
  35. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_controls.html.erb.tt +10 -41
  36. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_form.html.erb.tt +1 -5
  37. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_list.html.erb.tt +7 -33
  38. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row.html.erb.tt +10 -33
  39. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/_row_form.html.erb.tt +11 -34
  40. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/index.html.erb.tt +7 -21
  41. data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/show.html.erb.tt +3 -9
  42. data/lib/generators/hibiki/rails/scaffold_controller/templates/tailwind/views/_pagination.html.erb.tt +9 -25
  43. data/lib/generators/hibiki/rails/scaffold_model_injection.rb +8 -21
  44. data/lib/generators/hibiki/rails/scaffold_parent_injection.rb +4 -21
  45. data/lib/generators/hibiki/rails/scaffold_parent_notices.rb +6 -9
  46. data/lib/generators/hibiki/rails/scaffold_phlex_helpers.rb +5 -7
  47. data/lib/generators/hibiki/rails/scaffold_post_install.rb +11 -14
  48. data/lib/generators/hibiki/rails/scaffold_transport_stylesheet.rb +3 -3
  49. data/lib/generators/hibiki/rails/stimulus/templates/channel.rb.tt +1 -3
  50. data/lib/generators/hibiki/rails/stimulus/templates/display.html.erb.tt +1 -2
  51. data/lib/generators/hibiki/rails/stimulus/templates/island.html.erb.tt +1 -3
  52. data/lib/hibiki/rails/version.rb +1 -1
  53. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c47d1cec132b52f2a741ca77596ca90e524b348f385b0db5d2d2992ae0dba0c
4
- data.tar.gz: 44505ec6d36cb48589f8d924819d3d6ee02b2fb0092b67e0bd0c9b8af316da1b
3
+ metadata.gz: cd126c85e743e5ec9c89e54a1ffaba4cb04e6b6b0ef89868d60023168a0936a7
4
+ data.tar.gz: 00b4dfa1606348df1039cf5b69d1129c170fd418966b3db120964b71eba1e1c6
5
5
  SHA512:
6
- metadata.gz: 85985021ba53ce7e11eb4b6713905e8abee6c9e0c6babd36a1fd8debb06887994827cce847ae82a20df822d53ff14398be3f4515f416354ad1058f7562151037
7
- data.tar.gz: 031d605405895afed00c8119eabf62e7e545213a059f2d7e424f78fc08623939e550231168d829131f494d7bec3e9fa352f151cf41514700d1c8a90e3a68f6a0
6
+ metadata.gz: 3c1e7d75c946f79cc898c20c2c3a8457ba78c152963f8b8155dd70a26308c138b31acc4b7a732e037d6bc28e933fb542a509d2dabee2e03602727d7e5e81b6d6
7
+ data.tar.gz: 6edf4361a4edbda81b933167cb6a7e08030cb50aac694c601a22c2210036eab5c56b101a449d3ea174a5485347e7c9c6e1cc86f9da123cbfe02e0e1ed34f41dd
data/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ 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
+
7
27
  ## 0.5.0 — 2026-08-05
8
28
 
9
29
  ### Added
@@ -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
@@ -29,8 +29,7 @@ Example:
29
29
  And add to config/routes.rb:
30
30
  resources :books
31
31
 
32
- Restart the server afterwards: Rails computes autoload paths from the
33
- app/* glob at boot, and app/forms is new.
32
+ Restart the server afterwards if it is running — app/forms is new.
34
33
 
35
34
  Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
36
35
  ERB templates. The view layer is the only thing it changes; it needs the
@@ -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
@@ -2,44 +2,19 @@
2
2
  item as two tokens (`join-item btn`), so each of the six variants below can
3
3
  repeat them inline — which is exactly what plain Tailwind cannot do. -%>
4
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.
11
-
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
+ <%%# The numbered page control, inside the replaced fragment so it re-renders
6
+ with the list. The links are anchors to the list, so a click also does the
7
+ browser's own fragment jump; data-turbo="false" keeps Turbo Drive out.
8
+ `on` returns a { data: } hash — merge, don't splat, or the anchor stops
9
+ scrolling. %>
30
10
  <%% 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. %>
11
+ <%%# The wrapper always renders (so idiomorph has a node to pair with) and
12
+ stays bare — layout classes here would leave dead space when empty. %>
35
13
  <div id="<%= pagination_dom_id %>">
36
14
  <%% if page_count > 1 %>
37
15
  <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. %>
16
+ <%%# Prev/next go inert at the ends rather than disappearing, so the
17
+ control keeps its width across a page change. %>
43
18
  <%% if page == 1 %>
44
19
  <span class="join-item btn btn-disabled" aria-disabled="true">«</span>
45
20
  <%% else %>
@@ -51,9 +26,6 @@
51
26
  <%% if n.nil? %>
52
27
  <span class="join-item btn btn-disabled" aria-hidden="true">…</span>
53
28
  <%% 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
29
  <span class="join-item btn btn-active" aria-current="page"><%%= n %></span>
58
30
  <%% else %>
59
31
  <%%= link_to n, "#<%= list_dom_id %>", class: "join-item btn",
@@ -2,36 +2,21 @@
2
2
  the current page with a CLASS (btn-active, or a background utility). With
3
3
  no stylesheet there is nothing to carry that signal, so the current page
4
4
  has to change ELEMENT — <strong> rather than a styled <span> — and the
5
- inert prev/next markers become plain text. aria-current still does the work
6
- for a screen reader either way; what changes is that sighted users need the
7
- element, because there is no stylesheet to help. A class-token map cannot
5
+ inert prev/next markers become plain text. A class-token map cannot
8
6
  express a change of tag name. -%>
9
7
  <%%# locals: (page: 1, page_count: 1) -%>
10
- <%%# The numbered page control. It lives INSIDE the replaced fragment, like
11
- every other control in there: page_count is a server number, so a
12
- single-page list simply stops rendering this element.
13
-
14
- SCROLL. The links are anchors to the list, not buttons — the client only
15
- preventDefaults `submit`, so the click both performs the action and does the
16
- browser's own fragment jump. data-turbo="false" keeps Turbo Drive out of it.
17
-
18
- `on` returns a { data: } hash, so an element needing its OWN data attributes
19
- has to MERGE rather than splat, or the anchor stops scrolling. %>
20
- <%%# PENDING. Nothing here asks for it: the client stamps data-hibiki-busy on
21
- the control that fired, so the clicked page link dims itself while the trip
22
- is out, and the swap that answers it destroys the attribute along with the
23
- link. The bar above the list is the other half — the anchor jump has
24
- already put it in view by then. %>
8
+ <%%# The numbered page control, inside the replaced fragment so it re-renders
9
+ with the list. The links are anchors to the list, so a click also does the
10
+ browser's own fragment jump; data-turbo="false" keeps Turbo Drive out.
11
+ `on` returns a { data: } hash — merge, don't splat, or the anchor stops
12
+ scrolling. %>
25
13
  <%% page_data = ->(n) { { turbo: false }.merge(on(:go_to_page, with: { page: n })[:data]) } %>
26
- <%%# The id-carrying wrapper always renders, so idiomorph always has a node to
27
- pair with. %>
14
+ <%%# The wrapper always renders, so idiomorph always has a node to pair with. %>
28
15
  <div id="<%= pagination_dom_id %>">
29
16
  <%% if page_count > 1 %>
30
17
  <nav aria-label="Pagination">
31
- <%%# Prev/next stay rendered and go inert at the ends rather than
32
- disappearing, so the control keeps its position across a page change.
33
- A span, not a disabled link: there is no page to name, so there is no
34
- action to stamp. %>
18
+ <%%# Prev/next go inert at the ends rather than disappearing, so the
19
+ control keeps its position across a page change. %>
35
20
  <%% if page == 1 %>
36
21
  <span aria-disabled="true">«</span>
37
22
  <%% else %>
@@ -1,43 +1,17 @@
1
1
  <%#- ESCAPING: none — plain Ruby out, every <% is generation time. -%>
2
2
  # frozen_string_literal: true
3
3
 
4
- # The numbered page control. It lives INSIDE the replaced fragment, like every
5
- # other control in there: page_count is a server number, so a single-page list
6
- # simply stops rendering this element. Placed outside the fragment it would
7
- # never be re-rendered and could only ever change its TEXT (reactive values are
8
- # textContent-only), leaving a control whose links point at pages that no
9
- # longer exist.
10
- #
11
- # SCROLL. The links are anchors to the list, not buttons, and that IS the
12
- # scroll answer — the client only preventDefaults `submit`, so the click both
13
- # performs the action and does the browser's own fragment jump, which puts the
14
- # top of the list back in view exactly as a full page load would. No client
15
- # capability needed. data-turbo="false" keeps Turbo Drive out of it: a
16
- # same-page hash with Turbo off is a plain scroll, not a navigation.
17
- #
18
- # THE STRING "false", not the boolean. Phlex OMITS a false-valued attribute
19
- # entirely, so `turbo: false` would emit no data-turbo at all and Turbo Drive
20
- # would take the click back. The ERB twin's boolean is right there and wrong
21
- # here.
22
- #
23
- # No generation token — a page number is idempotent, so a double fire is one
24
- # write and one render.
25
- #
26
- # PENDING. Nothing here asks for it: the client stamps data-hibiki-busy on the
27
- # control that fired, so the clicked page link dims itself while the trip is
28
- # out, and the swap that answers it destroys the attribute along with the link.
29
- # The bar above the list is the other half — the anchor jump has already put it
30
- # in view by then.
4
+ # The numbered page control, inside the replaced fragment so it re-renders
5
+ # with the list. The links are anchors to the list, so a click also does the
6
+ # browser's own fragment jump; data-turbo="false" keeps Turbo Drive out.
31
7
  class <%= view_class_name(:pagination) %> < Views::Base
32
8
  <%= phlex_includes(:link_to, hibiki: true) %>
33
9
 
34
10
  <%= initializer_source([%w[page 1], %w[page_count 1]]) %>
35
11
 
36
12
  def view_template
37
- # The id-carrying wrapper stays put and stays BARE. It always renders, so
38
- # idiomorph always has a node to pair with, and it holds no layout classes
39
- # of its own — with no page control it is empty, and padding here would
40
- # leave dead vertical space on a list that has none.
13
+ # The wrapper always renders (so idiomorph has a node to pair with) and
14
+ # stays bare — layout classes here would leave dead space when empty.
41
15
  div(id: "<%= pagination_dom_id %>") do
42
16
  nav<%= arg_list(*css_args(:pagination_nav), %(aria_label: "Pagination")) %> { items } if @page_count > 1
43
17
  end
@@ -46,11 +20,8 @@ class <%= view_class_name(:pagination) %> < Views::Base
46
20
  private
47
21
 
48
22
  def items
49
- # Prev/next stay rendered and go inert at the ends rather than
50
- # disappearing, so the control keeps its width across a page change — one
51
- # that reflows under the pointer is how you mis-click a list that just
52
- # repainted. A span, not a disabled link: there is no page to name, so
53
- # there is no action to stamp.
23
+ # Prev/next go inert at the ends rather than disappearing, so the control
24
+ # keeps its width across a page change.
54
25
  @page == 1 ? inert("«") : step("«", @page - 1, rel: "prev", label: "Previous page")
55
26
  whitespace
56
27
 
@@ -58,16 +29,12 @@ class <%= view_class_name(:pagination) %> < Views::Base
58
29
  if n.nil?
59
30
  span(class: "join-item btn btn-disabled", aria_hidden: "true") { "…" }
60
31
  elsif n == @page
61
- # aria-current is the only thing that tells a screen reader which page
62
- # it is on: the visual cue is a class, and the link text is just a
63
- # number either way.
64
32
  span(class: "join-item btn btn-active", aria_current: "page") { n.to_s }
65
33
  else
66
34
  step(n.to_s, n, label: "Page #{n}")
67
35
  end
68
36
 
69
- # Phlex emits nothing between siblings; without this the page numbers run
70
- # together, which under --css=none is all there is to separate them.
37
+ # Phlex emits nothing between siblings.
71
38
  whitespace
72
39
  end
73
40
 
@@ -78,10 +45,8 @@ class <%= view_class_name(:pagination) %> < Views::Base
78
45
  span(class: "join-item btn btn-disabled", aria_disabled: "true") { glyph }
79
46
  end
80
47
 
81
- # `on` returns a { data: } hash, so an element needing its OWN data
82
- # attributes has to MERGE rather than splat: a bare ** after
83
- # `data: { turbo: "false" }` would silently replace it and the anchor would
84
- # stop scrolling.
48
+ # The STRING "false", not the boolean — Phlex omits false-valued attributes.
49
+ # And merge, don't splat: the anchor needs its own data attribute too.
85
50
  def step(text, n, label:, rel: nil)
86
51
  link_to text, "#<%= list_dom_id %>", class: "join-item btn", rel:, "aria-label": label,
87
52
  data: { turbo: "false" }.merge(on(:go_to_page, with: { page: n })[:data])
@@ -1,48 +1,19 @@
1
1
  <%#- ESCAPING: none — plain Ruby out, every <% is generation time. -%>
2
2
  # frozen_string_literal: true
3
3
 
4
- # The numbered page control. It lives INSIDE the replaced fragment, like every
5
- # other control in there: page_count is a server number, so a single-page list
6
- # simply stops rendering this element. Placed outside the fragment it would
7
- # never be re-rendered and could only ever change its TEXT (reactive values are
8
- # textContent-only), leaving a control whose links point at pages that no
9
- # longer exist.
4
+ # The numbered page control, inside the replaced fragment so it re-renders
5
+ # with the list. The links are anchors to the list, so a click also does the
6
+ # browser's own fragment jump; data-turbo="false" keeps Turbo Drive out.
10
7
  #
11
- # SCROLL. The links are anchors to the list, not buttons, and that IS the
12
- # scroll answer — the client only preventDefaults `submit`, so the click both
13
- # performs the action and does the browser's own fragment jump, which puts the
14
- # top of the list back in view exactly as a full page load would. No client
15
- # capability needed. data-turbo="false" keeps Turbo Drive out of it: a
16
- # same-page hash with Turbo off is a plain scroll, not a navigation.
17
- #
18
- # THE STRING "false", not the boolean. Phlex OMITS a false-valued attribute
19
- # entirely, so `turbo: false` would emit no data-turbo at all and Turbo Drive
20
- # would take the click back. The ERB twin's boolean is right there and wrong
21
- # here.
22
- #
23
- # No generation token — a page number is idempotent, so a double fire is one
24
- # write and one render.
25
- #
26
- # PENDING. Nothing here asks for it: the client stamps data-hibiki-busy on the
27
- # control that fired, so the clicked page link dims itself while the trip is
28
- # out, and the swap that answers it destroys the attribute along with the link.
29
- # The bar above the list is the other half — the anchor jump has already put it
30
- # in view by then.
31
- #
32
- # The sharpest of the three forks, and the one Phlex does NOT dissolve. Both
33
- # styled variants mark the current page with a CLASS. With no stylesheet there
34
- # is nothing to carry that signal, so the current page has to change ELEMENT —
35
- # strong rather than a styled span — and the inert prev/next markers become
36
- # plain text. aria-current still does the work for a screen reader either way;
37
- # what changes is that sighted users need the element.
8
+ # Unstyled, so the current page changes ELEMENT (strong) — there is no class
9
+ # to carry the signal.
38
10
  class <%= view_class_name(:pagination) %> < Views::Base
39
11
  <%= phlex_includes(:link_to, hibiki: true) %>
40
12
 
41
13
  <%= initializer_source([%w[page 1], %w[page_count 1]]) %>
42
14
 
43
15
  def view_template
44
- # The id-carrying wrapper always renders, so idiomorph always has a node to
45
- # pair with.
16
+ # The wrapper always renders, so idiomorph always has a node to pair with.
46
17
  div(id: "<%= pagination_dom_id %>") do
47
18
  nav(aria_label: "Pagination") { items } if @page_count > 1
48
19
  end
@@ -51,10 +22,8 @@ class <%= view_class_name(:pagination) %> < Views::Base
51
22
  private
52
23
 
53
24
  def items
54
- # Prev/next stay rendered and go inert at the ends rather than
55
- # disappearing, so the control keeps its position across a page change. A
56
- # span, not a disabled link: there is no page to name, so there is no
57
- # action to stamp.
25
+ # Prev/next go inert at the ends rather than disappearing, so the control
26
+ # keeps its position across a page change.
58
27
  @page == 1 ? inert("«") : step("«", @page - 1, rel: "prev", label: "Previous page")
59
28
  whitespace
60
29
 
@@ -67,8 +36,7 @@ class <%= view_class_name(:pagination) %> < Views::Base
67
36
  step(n.to_s, n, label: "Page #{n}")
68
37
  end
69
38
 
70
- # Phlex emits nothing between siblings; without this the page numbers run
71
- # together, which under --css=none is all there is to separate them.
39
+ # Phlex emits nothing between siblings.
72
40
  whitespace
73
41
  end
74
42
 
@@ -79,8 +47,8 @@ class <%= view_class_name(:pagination) %> < Views::Base
79
47
  span(aria_disabled: "true") { glyph }
80
48
  end
81
49
 
82
- # `on` returns a { data: } hash, so an element needing its OWN data
83
- # attributes has to MERGE rather than splat, or the anchor stops scrolling.
50
+ # The STRING "false", not the boolean — Phlex omits false-valued attributes.
51
+ # And merge, don't splat: the anchor needs its own data attribute too.
84
52
  def step(text, n, label:, rel: nil)
85
53
  link_to text, "#<%= list_dom_id %>", rel:, "aria-label": label,
86
54
  data: { turbo: "false" }.merge(on(:go_to_page, with: { page: n })[:data])