hibiki_rails 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +106 -0
- data/README.md +15 -2
- data/lib/generators/hibiki/rails/css_variant.rb +7 -4
- data/lib/generators/hibiki/rails/scaffold/USAGE +5 -0
- data/lib/generators/hibiki/rails/scaffold/scaffold_generator.rb +2 -0
- data/lib/generators/hibiki/rails/scaffold_controller/USAGE +5 -0
- data/lib/generators/hibiki/rails/scaffold_controller/scaffold_controller_generator.rb +64 -6
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/daisyui/views/pagination.rb.tt +89 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/none/views/pagination.rb.tt +88 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/controls.rb.tt +132 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/edit.rb.tt +24 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/field_error.rb.tt +20 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/form.rb.tt +71 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/index.rb.tt +59 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/list.rb.tt +76 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/new.rb.tt +22 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row.rb.tt +94 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/row_form.rb.tt +102 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/shared/views/show.rb.tt +44 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/tailwind/views/pagination.rb.tt +99 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/channel.rb.tt +1 -2
- data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/controller.rb.tt +12 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/hibiki_busy.css.tt +106 -0
- data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/member_channel.rb.tt +1 -2
- data/lib/generators/hibiki/rails/scaffold_controller/templates/shared/views/index.html.erb.tt +1 -6
- data/lib/generators/hibiki/rails/scaffold_phlex_helpers.rb +276 -0
- data/lib/generators/hibiki/rails/scaffold_post_install.rb +16 -1
- data/lib/generators/hibiki/rails/scaffold_schema.rb +11 -8
- data/lib/generators/hibiki/rails/scaffold_transport_stylesheet.rb +141 -0
- data/lib/generators/hibiki/rails/scaffold_view_helpers.rb +88 -23
- data/lib/hibiki/rails/channel.rb +8 -4
- data/lib/hibiki/rails/version.rb +1 -1
- metadata +21 -6
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c47d1cec132b52f2a741ca77596ca90e524b348f385b0db5d2d2992ae0dba0c
|
|
4
|
+
data.tar.gz: 44505ec6d36cb48589f8d924819d3d6ee02b2fb0092b67e0bd0c9b8af316da1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85985021ba53ce7e11eb4b6713905e8abee6c9e0c6babd36a1fd8debb06887994827cce847ae82a20df822d53ff14398be3f4515f416354ad1058f7562151037
|
|
7
|
+
data.tar.gz: 031d605405895afed00c8119eabf62e7e545213a059f2d7e424f78fc08623939e550231168d829131f494d7bec3e9fa352f151cf41514700d1c8a90e3a68f6a0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,112 @@ 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.0 — 2026-08-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
**`--phlex` on both scaffold generators.** `bin/rails g hibiki:rails:scaffold
|
|
12
|
+
Book title:string --phlex` emits Phlex components under `app/views/books/*.rb`,
|
|
13
|
+
namespaced `Views::`, instead of ERB templates. Purely additive: without the
|
|
14
|
+
flag nothing about the generated output changes, byte for byte.
|
|
15
|
+
|
|
16
|
+
Only the view layer moves. The channels, the query object, the ReactiveForm,
|
|
17
|
+
the model injections, every action and the whole `data-hibiki-*` protocol are
|
|
18
|
+
the same either way — the controller gains an explicit `render Views::…` at
|
|
19
|
+
each of its six render sites, and the two `broadcast_morph` calls swap
|
|
20
|
+
`partial:`/`locals:` for `renderable:`, and that is the entire difference
|
|
21
|
+
outside the templates.
|
|
22
|
+
|
|
23
|
+
Needs `phlex-rails` and `bin/rails g phlex:install`. The generator warns when
|
|
24
|
+
either is missing and writes the files anyway, so the scaffold can come first.
|
|
25
|
+
|
|
26
|
+
Four things worth knowing, because they are not what an ERB reader expects.
|
|
27
|
+
Phlex renders `String`, `Symbol`, `Integer` and `Float` and raises on anything
|
|
28
|
+
else, so date, time and decimal columns are emitted with an explicit `to_s`.
|
|
29
|
+
Phlex omits a `false`-valued attribute entirely, so the page control's
|
|
30
|
+
`data-turbo` is the string `"false"`. Phlex emits no whitespace between
|
|
31
|
+
siblings, so the components space their inline neighbours explicitly. And
|
|
32
|
+
`options_for_select` outputs directly and raises if its return value is passed
|
|
33
|
+
on, so both select sites take their options from a block.
|
|
34
|
+
|
|
35
|
+
The page control is still the one component with a per-`--css` fork, in both
|
|
36
|
+
trees. Under Phlex the plain-Tailwind fork's *reason* dissolves — a hoisted
|
|
37
|
+
template local becomes an ordinary constant — but it stays forked so the two
|
|
38
|
+
trees match file for file and a future `--css` decision stays a diff rather
|
|
39
|
+
than a judgement call.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
**The loading and connection recipes are an asset, not a partial.** They were a
|
|
44
|
+
103-line inline `<style>` emitted as `app/views/<resource>/_busy.html.erb` and
|
|
45
|
+
rendered once per page; they are now
|
|
46
|
+
`app/assets/stylesheets/hibiki_busy.css`, written once per app.
|
|
47
|
+
|
|
48
|
+
Per resource was always wrong: every rule keys on an attribute the client
|
|
49
|
+
stamps and none of them mentions a model, so a two-resource app carried two
|
|
50
|
+
byte-identical copies. It also put CSS somewhere a Content-Security-Policy that
|
|
51
|
+
forbids inline styles would reject.
|
|
52
|
+
|
|
53
|
+
The generator wires it for you: a cssbundling or tailwindcss-rails entry
|
|
54
|
+
stylesheet gets an `@import`, a layout already using
|
|
55
|
+
`stylesheet_link_tag :app` or `:all` needs nothing, and anything else gets a
|
|
56
|
+
`stylesheet_link_tag` injected into the layout. Only when none of those applies
|
|
57
|
+
does it print the line to add. Every branch is idempotent.
|
|
58
|
+
|
|
59
|
+
**If you re-run the generator on an app scaffolded before this**, the old
|
|
60
|
+
`_busy.html.erb` stays on disk — a generator never deletes — and nothing
|
|
61
|
+
renders it any more. The post-install output names it; delete it.
|
|
62
|
+
|
|
63
|
+
The rules are deliberately unlayered, and the file says so: two of them set
|
|
64
|
+
`display` on elements that also carry Tailwind utilities, and unlayered
|
|
65
|
+
declarations beat `@layer utilities` whatever the link order.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
**The npm package no longer drags in a second copy of `@rails/actioncable`.**
|
|
70
|
+
It moves from `dependencies` to `peerDependencies` at `>= 7.0`, matching
|
|
71
|
+
turbo-rails' own range.
|
|
72
|
+
|
|
73
|
+
Why there were two: `@rails/actioncable`'s npm `latest` dist-tag is 7.2.302 even
|
|
74
|
+
though 8.x is published, and resolvers prefer `latest` when it satisfies the
|
|
75
|
+
range. So turbo-rails' `>=7.0` took 7.2.302 while this package's `>= 8.0` was
|
|
76
|
+
forced up to 8.1.301, and both ended up in the bundle — about 16 KB of duplicate
|
|
77
|
+
client, and two separate module instances that could never share a consumer.
|
|
78
|
+
A fresh install happened to hoist a single copy; the duplicate appeared when
|
|
79
|
+
adding hibiki-rails to an app whose lockfile already pinned 7.2.302, which is
|
|
80
|
+
every existing app.
|
|
81
|
+
|
|
82
|
+
If your bundler warns about an unmet peer, install `@rails/actioncable`
|
|
83
|
+
explicitly — but a stock Rails app already has it via turbo-rails, and both bun
|
|
84
|
+
and npm 7+ auto-install a missing peer. The client uses only `createConsumer`,
|
|
85
|
+
`subscriptions.create` and `subscription.perform`, all stable since Action
|
|
86
|
+
Cable 6, so the lower floor changes nothing at runtime.
|
|
87
|
+
|
|
88
|
+
### Changed — BREAKING
|
|
89
|
+
|
|
90
|
+
**The Rails floor is now 8.0.** `actioncable` and `railties` move from
|
|
91
|
+
`>= 7.1` to `>= 8.0`, and the 7.1 / 7.2 CI legs are gone. Ruby stays at `>= 3.4`.
|
|
92
|
+
|
|
93
|
+
This is a correction as much as a policy change: **generated controllers never
|
|
94
|
+
ran on Rails 7.** `hibiki:rails:scaffold` emits `params.expect` at two sites,
|
|
95
|
+
inherited from Rails 8's own scaffold, and `ActionController::Parameters#expect`
|
|
96
|
+
does not exist before 8.0 — so a generated controller raised `NoMethodError` on
|
|
97
|
+
the first request to `show`, `edit`, `update`, `create` or `destroy` on 7.1 and
|
|
98
|
+
7.2. The generator suite never caught it because those specs assert on emitted
|
|
99
|
+
source text and never boot the result.
|
|
100
|
+
|
|
101
|
+
The alternative — branching the template on `Rails::VERSION` — was rejected: it
|
|
102
|
+
would also need a way to *execute* generated output on the old legs, which is
|
|
103
|
+
more work than dropping two CI legs for a version combination (Rails 7.x on
|
|
104
|
+
Ruby >= 3.4) that barely exists.
|
|
105
|
+
|
|
106
|
+
**If you are on Rails 7.1 or 7.2, stay on 0.4.0.** It remains available and is
|
|
107
|
+
unaffected; nothing in 0.5.0 is a security fix for it. Note that the *runtime*
|
|
108
|
+
half of the gem — channels, the graph, the broadcast helpers, the client — has
|
|
109
|
+
no known 8.0-only dependency; it is the generators whose output does. The floor
|
|
110
|
+
applies to the whole gem anyway, because shipping a gem whose headline generator
|
|
111
|
+
cannot run on its own declared floor is what got us here.
|
|
112
|
+
|
|
7
113
|
## 0.4.0 — 2026-08-03
|
|
8
114
|
|
|
9
115
|
### 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 >=
|
|
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
|
|
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
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
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",
|
|
@@ -25,8 +25,13 @@ 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
32
|
Restart the server afterwards: Rails computes autoload paths from the
|
|
32
33
|
app/* glob at boot, and app/forms is new.
|
|
34
|
+
|
|
35
|
+
Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
|
|
36
|
+
ERB templates. The view layer is the only thing it changes; it needs the
|
|
37
|
+
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
|
|
@@ -28,7 +28,12 @@ Example:
|
|
|
28
28
|
app/forms/book_form.rb
|
|
29
29
|
app/controllers/books_controller.rb
|
|
30
30
|
app/views/books/*
|
|
31
|
+
app/assets/stylesheets/hibiki_busy.css
|
|
31
32
|
And modify:
|
|
32
33
|
app/models/book.rb
|
|
33
34
|
app/models/author.rb # each model a belongs_to points at
|
|
34
35
|
config/routes.rb
|
|
36
|
+
|
|
37
|
+
Pass --phlex to emit Phlex components under app/views/books/*.rb instead of
|
|
38
|
+
ERB templates. The view layer is the only thing it changes; it needs the
|
|
39
|
+
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
|
-
|
|
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
|
|
154
|
-
#
|
|
155
|
-
# and a shared-local extraction under
|
|
156
|
-
# class-token map can express — so
|
|
157
|
-
#
|
|
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
|
data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/daisyui/views/pagination.rb.tt
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<%#- ESCAPING: none — plain Ruby out, every <% is generation time. -%>
|
|
2
|
+
# frozen_string_literal: true
|
|
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.
|
|
31
|
+
class <%= view_class_name(:pagination) %> < Views::Base
|
|
32
|
+
<%= phlex_includes(:link_to, hibiki: true) %>
|
|
33
|
+
|
|
34
|
+
<%= initializer_source([%w[page 1], %w[page_count 1]]) %>
|
|
35
|
+
|
|
36
|
+
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.
|
|
41
|
+
div(id: "<%= pagination_dom_id %>") do
|
|
42
|
+
nav<%= arg_list(*css_args(:pagination_nav), %(aria_label: "Pagination")) %> { items } if @page_count > 1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
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.
|
|
54
|
+
@page == 1 ? inert("«") : step("«", @page - 1, rel: "prev", label: "Previous page")
|
|
55
|
+
whitespace
|
|
56
|
+
|
|
57
|
+
<%= query_class_name %>.page_numbers(page: @page, page_count: @page_count).each do |n|
|
|
58
|
+
if n.nil?
|
|
59
|
+
span(class: "join-item btn btn-disabled", aria_hidden: "true") { "…" }
|
|
60
|
+
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
|
+
span(class: "join-item btn btn-active", aria_current: "page") { n.to_s }
|
|
65
|
+
else
|
|
66
|
+
step(n.to_s, n, label: "Page #{n}")
|
|
67
|
+
end
|
|
68
|
+
|
|
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.
|
|
71
|
+
whitespace
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@page == @page_count ? inert("»") : step("»", @page + 1, rel: "next", label: "Next page")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inert(glyph)
|
|
78
|
+
span(class: "join-item btn btn-disabled", aria_disabled: "true") { glyph }
|
|
79
|
+
end
|
|
80
|
+
|
|
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.
|
|
85
|
+
def step(text, n, label:, rel: nil)
|
|
86
|
+
link_to text, "#<%= list_dom_id %>", class: "join-item btn", rel:, "aria-label": label,
|
|
87
|
+
data: { turbo: "false" }.merge(on(:go_to_page, with: { page: n })[:data])
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/generators/hibiki/rails/scaffold_controller/templates/phlex/none/views/pagination.rb.tt
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<%#- ESCAPING: none — plain Ruby out, every <% is generation time. -%>
|
|
2
|
+
# frozen_string_literal: true
|
|
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.
|
|
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.
|
|
38
|
+
class <%= view_class_name(:pagination) %> < Views::Base
|
|
39
|
+
<%= phlex_includes(:link_to, hibiki: true) %>
|
|
40
|
+
|
|
41
|
+
<%= initializer_source([%w[page 1], %w[page_count 1]]) %>
|
|
42
|
+
|
|
43
|
+
def view_template
|
|
44
|
+
# The id-carrying wrapper always renders, so idiomorph always has a node to
|
|
45
|
+
# pair with.
|
|
46
|
+
div(id: "<%= pagination_dom_id %>") do
|
|
47
|
+
nav(aria_label: "Pagination") { items } if @page_count > 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
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.
|
|
58
|
+
@page == 1 ? inert("«") : step("«", @page - 1, rel: "prev", label: "Previous page")
|
|
59
|
+
whitespace
|
|
60
|
+
|
|
61
|
+
<%= query_class_name %>.page_numbers(page: @page, page_count: @page_count).each do |n|
|
|
62
|
+
if n.nil?
|
|
63
|
+
span(aria_hidden: "true") { "…" }
|
|
64
|
+
elsif n == @page
|
|
65
|
+
strong(aria_current: "page") { n.to_s }
|
|
66
|
+
else
|
|
67
|
+
step(n.to_s, n, label: "Page #{n}")
|
|
68
|
+
end
|
|
69
|
+
|
|
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.
|
|
72
|
+
whitespace
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@page == @page_count ? inert("»") : step("»", @page + 1, rel: "next", label: "Next page")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def inert(glyph)
|
|
79
|
+
span(aria_disabled: "true") { glyph }
|
|
80
|
+
end
|
|
81
|
+
|
|
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.
|
|
84
|
+
def step(text, n, label:, rel: nil)
|
|
85
|
+
link_to text, "#<%= list_dom_id %>", rel:, "aria-label": label,
|
|
86
|
+
data: { turbo: "false" }.merge(on(:go_to_page, with: { page: n })[:data])
|
|
87
|
+
end
|
|
88
|
+
end
|