plutonium 0.60.0 → 0.60.2
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/.claude/skills/plutonium-auth/SKILL.md +1 -1
- data/.claude/skills/plutonium-ui/SKILL.md +20 -0
- data/CHANGELOG.md +14 -0
- data/app/assets/plutonium.css +1 -1
- data/docs/reference/configuration.md +1 -1
- data/docs/reference/resource/definition.md +2 -2
- data/docs/reference/ui/layouts.md +61 -1
- data/docs/superpowers/plans/2026-06-14-form-sectioning.md +11 -2
- data/docs/superpowers/plans/2026-06-14-railless-portal.md +761 -0
- data/docs/superpowers/plans/2026-06-14-railless-portal.md.tasks.json +51 -0
- data/docs/superpowers/specs/2026-06-14-form-sectioning-design.md +15 -5
- data/docs/superpowers/specs/2026-06-14-railless-portal-design.md +275 -0
- data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
- data/lib/plutonium/auth/rodauth.rb +2 -1
- data/lib/plutonium/configuration.rb +2 -1
- data/lib/plutonium/core/controller.rb +38 -0
- data/lib/plutonium/definition/form_layout.rb +5 -6
- data/lib/plutonium/package/engine.rb +10 -0
- data/lib/plutonium/ui/form/components/sticky_footer.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +10 -0
- data/lib/plutonium/ui/layout/resource_layout.rb +28 -6
- data/lib/plutonium/ui/layout/topbar.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +9 -0
- metadata +5 -2
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
# Railless Portal Support — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add a first-class "no rail" mode so a portal can omit the modern icon rail without inheriting its desktop layout offsets, resolvable globally (`config.shell = :plain`) and per-controller (`rail false`).
|
|
6
|
+
|
|
7
|
+
**Architecture:** A single `rail?` predicate resolved through three tiers — global shell default → controller `rail` DSL (inherited) → layout delegates to the controller. `rail?` gates the sidebar partial, the initial-load `pu-rail-pinned` script, the `main` content offset, and a server-rendered `pu-no-rail` root class. `Topbar`/`StickyFooter` gain stable hook classes and a CSS rule keyed on `html.pu-no-rail` cancels their rail insets.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby, Rails (7 / 8.0 / 8.1 via Appraisal), Phlex components, TailwindCSS 4 (esbuild via `yarn build`), Minitest.
|
|
10
|
+
|
|
11
|
+
**User Verification:** NO — no user verification required.
|
|
12
|
+
|
|
13
|
+
> **Commit policy:** The repo owner's standing rule is *do not stage or commit unless explicitly asked*. The `git commit` steps below are written for completeness; **skip them unless the user has explicitly authorized committing.** Run the verification (test) steps regardless.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Structure
|
|
18
|
+
|
|
19
|
+
| File | Responsibility | Change |
|
|
20
|
+
|------|----------------|--------|
|
|
21
|
+
| `lib/plutonium/core/controller.rb` | Controller mixin; owns `rail` DSL + `rail?` resolution | Modify |
|
|
22
|
+
| `lib/plutonium/ui/layout/resource_layout.rb` | Resource page layout; gates rail-dependent rendering | Modify |
|
|
23
|
+
| `lib/plutonium/ui/layout/topbar.rb` | Fixed topbar; gains `pu-topbar` hook | Modify |
|
|
24
|
+
| `lib/plutonium/ui/form/components/sticky_footer.rb` | Form action footer; gains `pu-sticky-footer` hook | Modify |
|
|
25
|
+
| `src/css/components.css` | Source CSS; `html.pu-no-rail` inset cancel | Modify |
|
|
26
|
+
| `app/assets/plutonium.css` / `.min.js` | Built assets | Regenerated by `yarn build` |
|
|
27
|
+
| `lib/plutonium/auth/rodauth.rb` | Rodauth auth mixin; named `current_<account>` accessor | Modify |
|
|
28
|
+
| `lib/plutonium/configuration.rb` | Config; doc `:plain` shell | Modify (comment) |
|
|
29
|
+
| `lib/generators/pu/core/install/templates/config/initializers/plutonium.rb` | Install initializer; doc `:plain` | Modify (comment) |
|
|
30
|
+
| `test/plutonium/core/controller_rail_test.rb` | Unit: `rail?` resolution + DSL | Create |
|
|
31
|
+
| `test/plutonium/ui/layout/resource_layout_rail_test.rb` | Unit: `main_attributes` / `html_attributes` gating | Create |
|
|
32
|
+
| `test/plutonium/ui/layout/topbar_test.rb` | Unit: `pu-topbar` hook | Modify |
|
|
33
|
+
| `test/plutonium/ui/form/components/sticky_footer_test.rb` | Unit: `pu-sticky-footer` hook | Modify |
|
|
34
|
+
| `test/integration/plain_shell_rendering_test.rb` | Integration: end-to-end `:plain` vs `:modern` | Create |
|
|
35
|
+
| `test/plutonium/auth/rodauth_test.rb` | Unit: named accessor | Modify |
|
|
36
|
+
| `docs/reference/...` + `.claude/skills/...` | Docs for `:plain` / `rail` / named accessor | Modify |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Task 1: Controller `rail` DSL + `rail?` resolution
|
|
41
|
+
|
|
42
|
+
**Goal:** Add an inherited `rail` class DSL and a `rail?` predicate to the controller mixin, defaulting from `config.shell`.
|
|
43
|
+
|
|
44
|
+
**Files:**
|
|
45
|
+
- Modify: `lib/plutonium/core/controller.rb`
|
|
46
|
+
- Test: `test/plutonium/core/controller_rail_test.rb` (create)
|
|
47
|
+
|
|
48
|
+
**Acceptance Criteria:**
|
|
49
|
+
- [ ] `rail?` returns `true` when `config.shell == :modern`, `false` for `:plain` and `:classic`.
|
|
50
|
+
- [ ] `rail true` / `rail false` override the shell default; `nil` (default) inherits it.
|
|
51
|
+
- [ ] `_rail_enabled` is a `class_attribute` (inherits to subclasses; subclass can re-set).
|
|
52
|
+
- [ ] `rail?` is registered as a helper method and is public (the layout calls `controller.rail?`).
|
|
53
|
+
|
|
54
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/core/controller_rail_test.rb` → all pass.
|
|
55
|
+
|
|
56
|
+
**Steps:**
|
|
57
|
+
|
|
58
|
+
- [ ] **Step 1: Write the failing test**
|
|
59
|
+
|
|
60
|
+
Create `test/plutonium/core/controller_rail_test.rb`:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# frozen_string_literal: true
|
|
64
|
+
|
|
65
|
+
require "test_helper"
|
|
66
|
+
|
|
67
|
+
class Plutonium::Core::ControllerRailTest < ActiveSupport::TestCase
|
|
68
|
+
def build_controller_class
|
|
69
|
+
Class.new(ActionController::Base) { include Plutonium::Core::Controller }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def with_shell(value)
|
|
73
|
+
original = Plutonium.configuration.shell
|
|
74
|
+
Plutonium.configuration.shell = value
|
|
75
|
+
yield
|
|
76
|
+
ensure
|
|
77
|
+
Plutonium.configuration.shell = original
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
test "rail? defaults to true for the modern shell" do
|
|
81
|
+
with_shell(:modern) do
|
|
82
|
+
assert build_controller_class.new.rail?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test "rail? defaults to false for the plain shell" do
|
|
87
|
+
with_shell(:plain) do
|
|
88
|
+
refute build_controller_class.new.rail?
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
test "rail? defaults to false for the classic shell" do
|
|
93
|
+
with_shell(:classic) do
|
|
94
|
+
refute build_controller_class.new.rail?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
test "rail false overrides the modern default" do
|
|
99
|
+
klass = build_controller_class
|
|
100
|
+
klass.rail false
|
|
101
|
+
with_shell(:modern) do
|
|
102
|
+
refute klass.new.rail?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
test "rail true overrides the plain default" do
|
|
107
|
+
klass = build_controller_class
|
|
108
|
+
klass.rail true
|
|
109
|
+
with_shell(:plain) do
|
|
110
|
+
assert klass.new.rail?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
test "rail setting inherits to subclasses" do
|
|
115
|
+
parent = build_controller_class
|
|
116
|
+
parent.rail false
|
|
117
|
+
child = Class.new(parent)
|
|
118
|
+
with_shell(:modern) do
|
|
119
|
+
refute child.new.rail?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
test "rail? is registered as a helper method" do
|
|
124
|
+
assert_includes build_controller_class._helper_methods, :rail?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
test "rail? is callable as a public method" do
|
|
128
|
+
assert_respond_to build_controller_class.new, :rail?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
134
|
+
|
|
135
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/core/controller_rail_test.rb`
|
|
136
|
+
Expected: FAIL — `NoMethodError: undefined method 'rail'` / `rail?`.
|
|
137
|
+
|
|
138
|
+
- [ ] **Step 3: Add the DSL and predicate**
|
|
139
|
+
|
|
140
|
+
In `lib/plutonium/core/controller.rb`, inside the existing `included do` block (after `helper_method :registered_resources`, before the block closes at line 48), add:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class_attribute :_rail_enabled, instance_writer: false, default: nil
|
|
144
|
+
helper_method :rail?
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Add a `class_methods do` block (place it just before the `private` keyword on line 50):
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class_methods do
|
|
151
|
+
# Enable or disable the modern icon rail for this controller and its
|
|
152
|
+
# subclasses. nil (the default) inherits the global shell default.
|
|
153
|
+
def rail(enabled)
|
|
154
|
+
self._rail_enabled = enabled
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Whether the modern icon rail is active for this request. Resolves the
|
|
159
|
+
# per-controller `rail` setting, falling back to the shell default.
|
|
160
|
+
# Public: the resource layout calls `controller.rail?`.
|
|
161
|
+
def rail?
|
|
162
|
+
return _rail_enabled unless _rail_enabled.nil?
|
|
163
|
+
Plutonium.configuration.shell == :modern
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> Note: `rail?` is defined *above* the `private` keyword so it stays public.
|
|
168
|
+
|
|
169
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
170
|
+
|
|
171
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/core/controller_rail_test.rb`
|
|
172
|
+
Expected: PASS (8 tests).
|
|
173
|
+
|
|
174
|
+
- [ ] **Step 5: Commit** *(only if commits are authorized — see Commit policy)*
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git add lib/plutonium/core/controller.rb test/plutonium/core/controller_rail_test.rb
|
|
178
|
+
git commit -m "feat(layout): add controller rail DSL and rail? predicate"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Task 2: Gate `ResourceLayout` rendering on `rail?`
|
|
184
|
+
|
|
185
|
+
**Goal:** Make `ResourceLayout` delegate `rail?` to the controller and skip every rail-dependent output (sidebar partial, initial pin script, `main` offset) when rail-less, emitting `pu-no-rail` on `<html>`.
|
|
186
|
+
|
|
187
|
+
**Files:**
|
|
188
|
+
- Modify: `lib/plutonium/ui/layout/resource_layout.rb`
|
|
189
|
+
- Test: `test/plutonium/ui/layout/resource_layout_rail_test.rb` (create)
|
|
190
|
+
|
|
191
|
+
**Acceptance Criteria:**
|
|
192
|
+
- [ ] `rail?` delegates to `controller.rail?`.
|
|
193
|
+
- [ ] `render_before_main` renders `resource_sidebar` only when `rail?`.
|
|
194
|
+
- [ ] `render_pre_paint_scripts` adds the `pu-rail-pinned` initial script only when `rail?`.
|
|
195
|
+
- [ ] `main_attributes` includes `lg:pl-20` only for modern+rail; `:plain` and modern-without-rail get no rail offset; `:classic` keeps `pt-20 lg:ml-64`.
|
|
196
|
+
- [ ] `html_attributes` adds class `pu-no-rail` when `!rail?` and shell is not `:classic`; otherwise unchanged.
|
|
197
|
+
|
|
198
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/resource_layout_rail_test.rb` → all pass.
|
|
199
|
+
|
|
200
|
+
**Steps:**
|
|
201
|
+
|
|
202
|
+
- [ ] **Step 1: Write the failing test**
|
|
203
|
+
|
|
204
|
+
Create `test/plutonium/ui/layout/resource_layout_rail_test.rb`:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# frozen_string_literal: true
|
|
208
|
+
|
|
209
|
+
require "test_helper"
|
|
210
|
+
|
|
211
|
+
class Plutonium::UI::Layout::ResourceLayoutRailTest < ActiveSupport::TestCase
|
|
212
|
+
def build_layout(rail:)
|
|
213
|
+
layout = Plutonium::UI::Layout::ResourceLayout.allocate
|
|
214
|
+
layout.define_singleton_method(:rail?) { rail }
|
|
215
|
+
layout
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def with_shell(value)
|
|
219
|
+
original = Plutonium.configuration.shell
|
|
220
|
+
Plutonium.configuration.shell = value
|
|
221
|
+
yield
|
|
222
|
+
ensure
|
|
223
|
+
Plutonium.configuration.shell = original
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
test "main_attributes includes lg:pl-20 for modern with rail" do
|
|
227
|
+
with_shell(:modern) do
|
|
228
|
+
classes = build_layout(rail: true).send(:main_attributes)[:class]
|
|
229
|
+
assert_includes classes, "lg:pl-20"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
test "main_attributes drops the rail offset for modern without rail" do
|
|
234
|
+
with_shell(:modern) do
|
|
235
|
+
classes = build_layout(rail: false).send(:main_attributes)[:class]
|
|
236
|
+
refute_includes classes, "lg:pl-20"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
test "main_attributes has no rail offset for the plain shell" do
|
|
241
|
+
with_shell(:plain) do
|
|
242
|
+
classes = build_layout(rail: false).send(:main_attributes)[:class]
|
|
243
|
+
refute_includes classes, "lg:pl-20"
|
|
244
|
+
refute_includes classes, "lg:ml-64"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
test "main_attributes keeps the classic sidebar offset" do
|
|
249
|
+
with_shell(:classic) do
|
|
250
|
+
classes = build_layout(rail: false).send(:main_attributes)[:class]
|
|
251
|
+
assert_includes classes, "lg:ml-64"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
test "html_attributes adds pu-no-rail when rail-less (plain)" do
|
|
256
|
+
with_shell(:plain) do
|
|
257
|
+
attrs = build_layout(rail: false).send(:html_attributes)
|
|
258
|
+
assert_includes attrs[:class].to_s, "pu-no-rail"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
test "html_attributes has no pu-no-rail when rail present" do
|
|
263
|
+
with_shell(:modern) do
|
|
264
|
+
attrs = build_layout(rail: true).send(:html_attributes)
|
|
265
|
+
refute_includes attrs[:class].to_s, "pu-no-rail"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
test "html_attributes leaves classic shell untouched" do
|
|
270
|
+
with_shell(:classic) do
|
|
271
|
+
attrs = build_layout(rail: false).send(:html_attributes)
|
|
272
|
+
refute_includes attrs[:class].to_s, "pu-no-rail"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
279
|
+
|
|
280
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/resource_layout_rail_test.rb`
|
|
281
|
+
Expected: FAIL — `main_attributes` lacks the plain branch / no `html_attributes` override (no `pu-no-rail`).
|
|
282
|
+
|
|
283
|
+
- [ ] **Step 3: Update `ResourceLayout`**
|
|
284
|
+
|
|
285
|
+
Replace the body of `lib/plutonium/ui/layout/resource_layout.rb` (the `private` section) with:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
class ResourceLayout < Base
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
# Whether the modern icon rail is active for this request. Delegates to
|
|
292
|
+
# the controller's resolution (shell default + per-controller `rail`).
|
|
293
|
+
def rail? = controller.rail?
|
|
294
|
+
|
|
295
|
+
# Sets pu-rail-pinned immediately on initial page load so the rail
|
|
296
|
+
# renders in its pinned state from the first frame. Turbo navigations
|
|
297
|
+
# are handled by the turbo:before-render listener in Base. Skipped
|
|
298
|
+
# entirely when the layout renders no rail.
|
|
299
|
+
def render_pre_paint_scripts
|
|
300
|
+
super
|
|
301
|
+
return unless rail?
|
|
302
|
+
|
|
303
|
+
script do
|
|
304
|
+
raw(safe(<<~JS))
|
|
305
|
+
(function () {
|
|
306
|
+
try {
|
|
307
|
+
if (localStorage.getItem("pu_rail_pinned") !== "false") {
|
|
308
|
+
document.documentElement.classList.add("pu-rail-pinned");
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {}
|
|
311
|
+
})();
|
|
312
|
+
JS
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Adds `pu-no-rail` to <html> (server-side, no FOUC) so decoupled fixed
|
|
317
|
+
# chrome (Topbar, form StickyFooter) can cancel their rail insets.
|
|
318
|
+
# Scoped to the modern family — :classic keeps its own offsets.
|
|
319
|
+
def html_attributes
|
|
320
|
+
attrs = super
|
|
321
|
+
return attrs if Plutonium.configuration.shell == :classic
|
|
322
|
+
|
|
323
|
+
rail? ? attrs : mix(attrs, {class: "pu-no-rail"})
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def main_attributes
|
|
327
|
+
classes = case Plutonium.configuration.shell
|
|
328
|
+
when :modern
|
|
329
|
+
rail? ? "pt-16 pb-6 px-6 lg:pl-20" : "pt-16 pb-6 px-6"
|
|
330
|
+
when :plain
|
|
331
|
+
"pt-16 pb-6 px-6"
|
|
332
|
+
else # :classic
|
|
333
|
+
"pt-20 lg:ml-64"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
mix(super, {class: classes})
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def page_title
|
|
340
|
+
make_page_title(
|
|
341
|
+
controller.instance_variable_get(:@page_title)
|
|
342
|
+
)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def render_before_main
|
|
346
|
+
super
|
|
347
|
+
|
|
348
|
+
render partial("resource_header")
|
|
349
|
+
render partial("resource_sidebar") if rail?
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
355
|
+
|
|
356
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/resource_layout_rail_test.rb`
|
|
357
|
+
Expected: PASS (7 tests).
|
|
358
|
+
|
|
359
|
+
- [ ] **Step 5: Commit** *(only if authorized)*
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
git add lib/plutonium/ui/layout/resource_layout.rb test/plutonium/ui/layout/resource_layout_rail_test.rb
|
|
363
|
+
git commit -m "feat(layout): gate rail rendering and offsets on rail?"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Task 3: Stable hook classes + `pu-no-rail` CSS + asset build
|
|
369
|
+
|
|
370
|
+
**Goal:** Give `Topbar` and `StickyFooter` stable classes and add a CSS rule that cancels their `lg:left-14` rail inset under `html.pu-no-rail`; rebuild assets.
|
|
371
|
+
|
|
372
|
+
**Files:**
|
|
373
|
+
- Modify: `lib/plutonium/ui/layout/topbar.rb` (line 36 nav class)
|
|
374
|
+
- Modify: `lib/plutonium/ui/form/components/sticky_footer.rb` (line 9 div class)
|
|
375
|
+
- Modify: `src/css/components.css` (after the `html.pu-rail-pinned main` rule, ~line 758)
|
|
376
|
+
- Modify (regenerated): `app/assets/plutonium.css`, `app/assets/plutonium.min.js` via `yarn build`
|
|
377
|
+
- Test: `test/plutonium/ui/layout/topbar_test.rb`, `test/plutonium/ui/form/components/sticky_footer_test.rb`
|
|
378
|
+
|
|
379
|
+
**Acceptance Criteria:**
|
|
380
|
+
- [ ] `Topbar` nav class contains `pu-topbar` (and still `lg:left-14`).
|
|
381
|
+
- [ ] `StickyFooter` div class contains `pu-sticky-footer` (and still `lg:left-14`).
|
|
382
|
+
- [ ] `src/css/components.css` cancels the inset to `left: 0` for both under `html.pu-no-rail` at `min-width: 1024px`.
|
|
383
|
+
- [ ] `app/assets/plutonium.css` rebuilt and contains `pu-no-rail`.
|
|
384
|
+
|
|
385
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/topbar_test.rb test/plutonium/ui/form/components/sticky_footer_test.rb` → pass; `grep -c "pu-no-rail" app/assets/plutonium.css` → ≥ 1.
|
|
386
|
+
|
|
387
|
+
**Steps:**
|
|
388
|
+
|
|
389
|
+
- [ ] **Step 1: Write the failing tests**
|
|
390
|
+
|
|
391
|
+
Append to `test/plutonium/ui/layout/topbar_test.rb` (before the final `end`):
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
test "nav has pu-topbar stable hook class" do
|
|
395
|
+
html = render_html(build_component)
|
|
396
|
+
assert_includes html, "pu-topbar"
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Append to `test/plutonium/ui/form/components/sticky_footer_test.rb` (before the final `end`):
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
def test_outer_div_has_pu_sticky_footer_stable_hook
|
|
404
|
+
outer_class = capture_outer_div_class
|
|
405
|
+
assert_includes outer_class, "pu-sticky-footer"
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
410
|
+
|
|
411
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/topbar_test.rb test/plutonium/ui/form/components/sticky_footer_test.rb`
|
|
412
|
+
Expected: FAIL — `pu-topbar` / `pu-sticky-footer` not present.
|
|
413
|
+
|
|
414
|
+
- [ ] **Step 3: Add the hook classes**
|
|
415
|
+
|
|
416
|
+
In `lib/plutonium/ui/layout/topbar.rb`, change the `nav` class string (line 36) from:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
class: "fixed top-0 right-0 left-0 lg:left-14 z-30 h-12 " \
|
|
420
|
+
```
|
|
421
|
+
to:
|
|
422
|
+
```ruby
|
|
423
|
+
class: "pu-topbar fixed top-0 right-0 left-0 lg:left-14 z-30 h-12 " \
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
In `lib/plutonium/ui/form/components/sticky_footer.rb`, change the `div` class (line 9) from:
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
div(class: "fixed bottom-0 left-0 right-0 lg:left-14 z-20 " \
|
|
430
|
+
```
|
|
431
|
+
to:
|
|
432
|
+
```ruby
|
|
433
|
+
div(class: "pu-sticky-footer fixed bottom-0 left-0 right-0 lg:left-14 z-20 " \
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
- [ ] **Step 4: Add the CSS rule**
|
|
437
|
+
|
|
438
|
+
In `src/css/components.css`, immediately after the existing block:
|
|
439
|
+
|
|
440
|
+
```css
|
|
441
|
+
@media (min-width: 1024px) {
|
|
442
|
+
html.pu-rail-pinned main {
|
|
443
|
+
padding-left: 15.5rem !important;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
add:
|
|
449
|
+
|
|
450
|
+
```css
|
|
451
|
+
/* Rail-less layouts (html.pu-no-rail): cancel the icon-rail inset on fixed
|
|
452
|
+
chrome so the topbar and form sticky footer span the full width. */
|
|
453
|
+
@media (min-width: 1024px) {
|
|
454
|
+
html.pu-no-rail .pu-topbar,
|
|
455
|
+
html.pu-no-rail .pu-sticky-footer {
|
|
456
|
+
left: 0 !important;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
- [ ] **Step 5: Rebuild assets**
|
|
462
|
+
|
|
463
|
+
Run: `yarn build`
|
|
464
|
+
Expected: rebuilds `app/assets/plutonium.css` and `app/assets/plutonium.min.js`.
|
|
465
|
+
|
|
466
|
+
- [ ] **Step 6: Verify tests and built asset**
|
|
467
|
+
|
|
468
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/ui/layout/topbar_test.rb test/plutonium/ui/form/components/sticky_footer_test.rb`
|
|
469
|
+
Expected: PASS.
|
|
470
|
+
Run: `grep -c "pu-no-rail" app/assets/plutonium.css`
|
|
471
|
+
Expected: ≥ 1.
|
|
472
|
+
|
|
473
|
+
- [ ] **Step 7: Commit** *(only if authorized)*
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
git add lib/plutonium/ui/layout/topbar.rb lib/plutonium/ui/form/components/sticky_footer.rb src/css/components.css app/assets/plutonium.css app/assets/plutonium.min.js test/plutonium/ui/layout/topbar_test.rb test/plutonium/ui/form/components/sticky_footer_test.rb
|
|
477
|
+
git commit -m "feat(layout): add pu-topbar/pu-sticky-footer hooks and pu-no-rail inset cancel"
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Task 4: End-to-end integration test (`:plain` vs `:modern`)
|
|
483
|
+
|
|
484
|
+
**Goal:** Prove a rail-less render omits the icon rail and pin script and carries `pu-no-rail`, while modern is unchanged.
|
|
485
|
+
|
|
486
|
+
**Files:**
|
|
487
|
+
- Test: `test/integration/plain_shell_rendering_test.rb` (create)
|
|
488
|
+
|
|
489
|
+
**Acceptance Criteria:**
|
|
490
|
+
- [ ] With `config.shell = :plain`: response has no `icon-rail` controller, no `classList.add("pu-rail-pinned")`, and `<html ... class="...pu-no-rail...">`.
|
|
491
|
+
- [ ] With `config.shell = :modern`: response includes `icon-rail` and the pin script.
|
|
492
|
+
- [ ] Shell config is restored in teardown.
|
|
493
|
+
|
|
494
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/integration/plain_shell_rendering_test.rb` → all pass.
|
|
495
|
+
|
|
496
|
+
**Steps:**
|
|
497
|
+
|
|
498
|
+
- [ ] **Step 1: Write the integration test**
|
|
499
|
+
|
|
500
|
+
Create `test/integration/plain_shell_rendering_test.rb`:
|
|
501
|
+
|
|
502
|
+
```ruby
|
|
503
|
+
# frozen_string_literal: true
|
|
504
|
+
|
|
505
|
+
require "test_helper"
|
|
506
|
+
|
|
507
|
+
class PlainShellRenderingTest < ActionDispatch::IntegrationTest
|
|
508
|
+
include IntegrationTestHelper
|
|
509
|
+
|
|
510
|
+
setup do
|
|
511
|
+
@admin = create_admin!
|
|
512
|
+
login_as_admin(@admin)
|
|
513
|
+
@original_shell = Plutonium.configuration.shell
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
teardown do
|
|
517
|
+
Plutonium.configuration.shell = @original_shell
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
test "plain shell renders rail-less" do
|
|
521
|
+
Plutonium.configuration.shell = :plain
|
|
522
|
+
get "/admin/kitchen_sinks"
|
|
523
|
+
|
|
524
|
+
assert_response :success
|
|
525
|
+
# NOTE: probe rail-gated-specific markers, not bare "icon-rail" /
|
|
526
|
+
# 'classList.add("pu-rail-pinned")' — those also appear in the
|
|
527
|
+
# always-present turbo:before-render listener in Base#render_pre_paint_scripts.
|
|
528
|
+
refute_includes response.body, 'data-controller="sidebar icon-rail"',
|
|
529
|
+
"plain shell should not render the icon rail"
|
|
530
|
+
refute_includes response.body, 'if (localStorage.getItem("pu_rail_pinned") !== "false") {',
|
|
531
|
+
"plain shell should not emit the initial pin script (Base listener's variant is prefixed `hasRail &&`)"
|
|
532
|
+
assert_match(/<html[^>]*class="[^"]*pu-no-rail/, response.body,
|
|
533
|
+
"plain shell should mark <html> with pu-no-rail")
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
test "modern shell still renders the icon rail and pin script" do
|
|
537
|
+
Plutonium.configuration.shell = :modern
|
|
538
|
+
get "/admin/kitchen_sinks"
|
|
539
|
+
|
|
540
|
+
assert_response :success
|
|
541
|
+
assert_includes response.body, 'data-controller="sidebar icon-rail"'
|
|
542
|
+
assert_includes response.body, 'if (localStorage.getItem("pu_rail_pinned") !== "false") {'
|
|
543
|
+
refute_match(/<html[^>]*class="[^"]*pu-no-rail/, response.body)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
- [ ] **Step 2: Run the test**
|
|
549
|
+
|
|
550
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/integration/plain_shell_rendering_test.rb`
|
|
551
|
+
Expected: PASS (2 tests). If FAIL, the gating in Tasks 1–2 is incomplete — fix there, not here.
|
|
552
|
+
|
|
553
|
+
- [ ] **Step 3: Commit** *(only if authorized)*
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
git add test/integration/plain_shell_rendering_test.rb
|
|
557
|
+
git commit -m "test(layout): integration coverage for plain (rail-less) shell"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Task 5: Named `current_<account>` Rodauth accessor
|
|
563
|
+
|
|
564
|
+
**Goal:** `Plutonium::Auth::Rodauth(name)` exposes `current_<name>` (aliased to `current_user`) in addition to `current_user`, both as helper methods.
|
|
565
|
+
|
|
566
|
+
**Files:**
|
|
567
|
+
- Modify: `lib/plutonium/auth/rodauth.rb`
|
|
568
|
+
- Test: `test/plutonium/auth/rodauth_test.rb`
|
|
569
|
+
|
|
570
|
+
**Acceptance Criteria:**
|
|
571
|
+
- [ ] `Rodauth.for(:admin)` registers both `:current_user` and `:current_admin` as helper methods.
|
|
572
|
+
- [ ] `current_admin` returns the same value as `current_user` (`rodauth.rails_account`).
|
|
573
|
+
- [ ] `Rodauth.for(:user)` still works (no duplicate-name breakage).
|
|
574
|
+
|
|
575
|
+
**Verify:** `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/auth/rodauth_test.rb` → all pass.
|
|
576
|
+
|
|
577
|
+
**Steps:**
|
|
578
|
+
|
|
579
|
+
- [ ] **Step 1: Write the failing tests**
|
|
580
|
+
|
|
581
|
+
Append to `test/plutonium/auth/rodauth_test.rb` (before the `private` keyword):
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
test "module includes named current_<account> helper method" do
|
|
585
|
+
mod = Plutonium::Auth::Rodauth.for(:admin)
|
|
586
|
+
controller_class = build_controller_class(mod)
|
|
587
|
+
|
|
588
|
+
assert_includes controller_class._helper_methods, :current_admin
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
test "named accessor returns the same account as current_user" do
|
|
592
|
+
mod = Plutonium::Auth::Rodauth.for(:admin)
|
|
593
|
+
controller_class = build_controller_class(mod)
|
|
594
|
+
controller = controller_class.new
|
|
595
|
+
|
|
596
|
+
assert_equal controller.send(:current_user), controller.send(:current_admin)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
test "for(:user) still defines current_user without error" do
|
|
600
|
+
mod = Plutonium::Auth::Rodauth.for(:user)
|
|
601
|
+
controller_class = build_controller_class(mod)
|
|
602
|
+
|
|
603
|
+
assert_includes controller_class._helper_methods, :current_user
|
|
604
|
+
end
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
608
|
+
|
|
609
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/auth/rodauth_test.rb`
|
|
610
|
+
Expected: FAIL — `current_admin` not a helper method.
|
|
611
|
+
|
|
612
|
+
- [ ] **Step 3: Add the named accessor**
|
|
613
|
+
|
|
614
|
+
In `lib/plutonium/auth/rodauth.rb`, update the heredoc. Change the `included do` helper registration from:
|
|
615
|
+
|
|
616
|
+
```ruby
|
|
617
|
+
included do
|
|
618
|
+
helper_method :current_user
|
|
619
|
+
helper_method :logout_url
|
|
620
|
+
helper_method :profile_url
|
|
621
|
+
end
|
|
622
|
+
```
|
|
623
|
+
to:
|
|
624
|
+
```ruby
|
|
625
|
+
included do
|
|
626
|
+
helper_method :current_user, :current_#{name}
|
|
627
|
+
helper_method :logout_url
|
|
628
|
+
helper_method :profile_url
|
|
629
|
+
end
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
And after the `current_user` definition:
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
def current_user
|
|
636
|
+
rodauth.rails_account
|
|
637
|
+
end
|
|
638
|
+
```
|
|
639
|
+
add:
|
|
640
|
+
```ruby
|
|
641
|
+
alias_method :current_#{name}, :current_user
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
> When `name == :user`, `current_#{name}` is just `current_user` — the alias and the duplicate `helper_method` arg are harmless no-ops.
|
|
645
|
+
|
|
646
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
647
|
+
|
|
648
|
+
Run: `bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/auth/rodauth_test.rb`
|
|
649
|
+
Expected: PASS.
|
|
650
|
+
|
|
651
|
+
- [ ] **Step 5: Commit** *(only if authorized)*
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
git add lib/plutonium/auth/rodauth.rb test/plutonium/auth/rodauth_test.rb
|
|
655
|
+
git commit -m "feat(auth): expose named current_<account> accessor on Rodauth(name)"
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Task 6: Documentation + config comments
|
|
661
|
+
|
|
662
|
+
**Goal:** Document the `:plain` shell, the controller `rail` DSL, and the named Rodauth accessor; add explanatory comments where `shell` is configured.
|
|
663
|
+
|
|
664
|
+
**Files:**
|
|
665
|
+
- Modify: `lib/plutonium/configuration.rb` (the `shell` attr comment, ~line 30)
|
|
666
|
+
- Modify: `lib/generators/pu/core/install/templates/config/initializers/plutonium.rb` (~line 6)
|
|
667
|
+
- Modify: relevant docs in `docs/` and skills in `.claude/skills/` (UI/app shell docs; auth docs)
|
|
668
|
+
|
|
669
|
+
**Acceptance Criteria:**
|
|
670
|
+
- [ ] `configuration.rb` documents `:plain` alongside `:modern`/`:classic`.
|
|
671
|
+
- [ ] Install initializer notes the `:plain` option in a comment.
|
|
672
|
+
- [ ] Docs/skills explain: `config.shell = :plain`, per-controller `rail false`/`true` (incl. the one-line portal-concern example), and the named `current_<account>` accessor.
|
|
673
|
+
- [ ] `grep -ri "rail false" docs .claude/skills` returns at least one hit.
|
|
674
|
+
|
|
675
|
+
**Verify:** `yarn docs:build` → no broken links; `grep -rl "config.shell = :plain" docs .claude/skills` → ≥ 1.
|
|
676
|
+
|
|
677
|
+
**Steps:**
|
|
678
|
+
|
|
679
|
+
- [ ] **Step 1: Update the configuration comment**
|
|
680
|
+
|
|
681
|
+
In `lib/plutonium/configuration.rb`, change the `shell` doc comment (line 30) from:
|
|
682
|
+
|
|
683
|
+
```ruby
|
|
684
|
+
# @return [Symbol] :classic (legacy Header/Sidebar) or :modern (Topbar/IconRail)
|
|
685
|
+
attr_accessor :shell
|
|
686
|
+
```
|
|
687
|
+
to:
|
|
688
|
+
```ruby
|
|
689
|
+
# @return [Symbol] :modern (Topbar/IconRail, default), :plain (Topbar, no
|
|
690
|
+
# icon rail), or :classic (legacy Header/Sidebar).
|
|
691
|
+
attr_accessor :shell
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
- [ ] **Step 2: Update the install initializer comment**
|
|
695
|
+
|
|
696
|
+
In `lib/generators/pu/core/install/templates/config/initializers/plutonium.rb`, add a comment above `config.shell = :modern`:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
# Shell variant: :modern (icon rail), :plain (no rail), or :classic (legacy).
|
|
700
|
+
config.shell = :modern
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
- [ ] **Step 3: Update docs and skills**
|
|
704
|
+
|
|
705
|
+
Add a "Shell variants & the icon rail" subsection to the appropriate UI/app shell doc under `docs/` (follow the existing doc structure) covering:
|
|
706
|
+
- `config.shell = :plain` for an app-wide rail-less shell.
|
|
707
|
+
- Per-controller / per-portal override:
|
|
708
|
+
```ruby
|
|
709
|
+
module CustomerPortal
|
|
710
|
+
module Concerns
|
|
711
|
+
module Controller
|
|
712
|
+
extend ActiveSupport::Concern
|
|
713
|
+
included { rail false } # entire portal rail-less
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
```
|
|
718
|
+
- Stable hooks `pu-topbar` / `pu-sticky-footer` and the `html.pu-no-rail` body class for custom CSS.
|
|
719
|
+
|
|
720
|
+
Update the `plutonium-ui` (and/or `plutonium-app`) skill in `.claude/skills/` with the same `rail`/`:plain` guidance, and the `plutonium-auth` skill with the named `current_<account>` accessor.
|
|
721
|
+
|
|
722
|
+
- [ ] **Step 4: Verify docs build and grep**
|
|
723
|
+
|
|
724
|
+
Run: `yarn docs:build`
|
|
725
|
+
Expected: builds with no broken-link errors.
|
|
726
|
+
Run: `grep -rl "config.shell = :plain" docs .claude/skills`
|
|
727
|
+
Expected: ≥ 1 file.
|
|
728
|
+
|
|
729
|
+
- [ ] **Step 5: Commit** *(only if authorized)*
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
git add lib/plutonium/configuration.rb lib/generators/pu/core/install/templates/config/initializers/plutonium.rb docs .claude/skills
|
|
733
|
+
git commit -m "docs(layout): document :plain shell, rail DSL, and named auth accessor"
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Final verification
|
|
739
|
+
|
|
740
|
+
- [ ] Run the full affected suite on the default Rails version:
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
bundle exec appraisal rails-8.1 ruby -Itest \
|
|
744
|
+
test/plutonium/core/controller_rail_test.rb \
|
|
745
|
+
test/plutonium/ui/layout/resource_layout_rail_test.rb \
|
|
746
|
+
test/plutonium/ui/layout/topbar_test.rb \
|
|
747
|
+
test/plutonium/ui/form/components/sticky_footer_test.rb \
|
|
748
|
+
test/integration/plain_shell_rendering_test.rb \
|
|
749
|
+
test/plutonium/auth/rodauth_test.rb
|
|
750
|
+
```
|
|
751
|
+
Expected: all green.
|
|
752
|
+
|
|
753
|
+
- [ ] Run the whole suite across versions to catch regressions:
|
|
754
|
+
|
|
755
|
+
```bash
|
|
756
|
+
bundle exec appraisal rake test
|
|
757
|
+
```
|
|
758
|
+
Expected: no new failures versus baseline.
|
|
759
|
+
|
|
760
|
+
- [ ] Confirm `app/assets/plutonium.css` was rebuilt (`yarn build`) and contains `pu-no-rail`.
|
|
761
|
+
```
|