plutonium 0.60.1 → 0.60.3
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-resource/SKILL.md +24 -0
- data/.claude/skills/plutonium-ui/SKILL.md +31 -2
- data/CHANGELOG.md +15 -0
- data/app/assets/plutonium.css +1 -1
- data/docs/reference/resource/definition.md +7 -2
- data/docs/reference/ui/layouts.md +34 -5
- data/lib/plutonium/core/controller.rb +16 -2
- data/lib/plutonium/portal/engine.rb +11 -0
- data/lib/plutonium/ui/form/components/section.rb +23 -8
- data/lib/plutonium/ui/form/resource.rb +10 -0
- data/lib/plutonium/ui/layout/base.rb +6 -1
- data/lib/plutonium/ui/layout/resource_layout.rb +8 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +1 -1
|
@@ -511,7 +511,12 @@ section :advanced, :seo_title, :notes,
|
|
|
511
511
|
label: -> { object.new_record? ? "Set up" : "Advanced" }
|
|
512
512
|
```
|
|
513
513
|
|
|
514
|
-
|
|
514
|
+
A section that resolves to **zero fields** — every declared field filtered out by the permitted set, or no field assigned — renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset). The check is purely "are there fields to render"; it does **not** evaluate per-field `condition:` procs (those run later, at field render). So if you want a whole section to appear only under some state, gate it with the section's own `condition:` rather than relying on every field inside it being hidden:
|
|
515
|
+
|
|
516
|
+
```ruby
|
|
517
|
+
section :shipping, :address, :city, :postcode,
|
|
518
|
+
condition: -> { object.requires_shipping? } # hide the section as a unit
|
|
519
|
+
```
|
|
515
520
|
|
|
516
521
|
### `ungrouped(**opts)`
|
|
517
522
|
|
|
@@ -564,7 +569,7 @@ end
|
|
|
564
569
|
|
|
565
570
|
### Fields not in the permitted set are skipped
|
|
566
571
|
|
|
567
|
-
A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there — a typo, or a field excluded by policy, per-action `permitted_attributes`, entity scoping, or nesting — is **silently dropped**, never an error. This lets a single `form_layout` reference conditionally-permitted fields without crashing the form in the contexts where they're filtered out.
|
|
572
|
+
A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there — a typo, or a field excluded by policy, per-action `permitted_attributes`, entity scoping, or nesting — is **silently dropped**, never an error. This lets a single `form_layout` reference conditionally-permitted fields without crashing the form in the contexts where they're filtered out. And when every field a section lists is dropped this way, the section's chrome is dropped with it (see the zero-fields note above) — so the same layout can serve a richly-permitted `edit` and a minimal `new` without leaving empty headings behind.
|
|
568
573
|
|
|
569
574
|
### On interactions
|
|
570
575
|
|
|
@@ -18,15 +18,44 @@ If you're starting fresh, use `:modern`. `:classic` exists so apps upgrading fro
|
|
|
18
18
|
|
|
19
19
|
## Shell variants & the icon rail
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The shell variant selects the page chrome:
|
|
22
22
|
|
|
23
23
|
- `:modern` (default) — Topbar plus the desktop icon rail.
|
|
24
|
-
- `:plain` — Topbar but **no** icon rail. The Topbar is kept; only the rail is removed, so the
|
|
24
|
+
- `:plain` — Topbar but **no** icon rail. The Topbar is kept; only the rail is removed, so the surface is rail-less.
|
|
25
25
|
- `:classic` — legacy Header/Sidebar (upgrade paths only).
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### Resolving the shell (global → engine → controller)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The shell resolves across three layers, each overriding the one above it:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# 1. Global default (config/initializers/plutonium.rb)
|
|
33
|
+
Plutonium.configure { |config| config.shell = :modern }
|
|
34
|
+
|
|
35
|
+
# 2. Per-engine — set it on a portal engine (lib/engine.rb)
|
|
36
|
+
module CustomerPortal
|
|
37
|
+
class Engine < Rails::Engine
|
|
38
|
+
include Plutonium::Portal::Engine
|
|
39
|
+
|
|
40
|
+
config.after_initialize do
|
|
41
|
+
shell :plain # this whole portal is rail-less
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 3. Per-controller — overrides the engine/global default for one controller (and subclasses)
|
|
47
|
+
class DashboardController < ResourceController
|
|
48
|
+
shell :modern # opt this controller back into the rail
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`shell` takes a plain symbol, so it's safe in the class body too — but the generated engine already has a `config.after_initialize` block (where `scope_to_entity` lives), so keeping it there is the consistent home.
|
|
53
|
+
|
|
54
|
+
An unset engine/controller value (`nil`) falls through to the next layer. Read the resolved value with the `shell` helper (`controller.shell`).
|
|
55
|
+
|
|
56
|
+
### `rail` — a controller-level rail toggle
|
|
57
|
+
|
|
58
|
+
Alongside `shell`, any resource controller exposes a class-level `rail` DSL that flips just the icon rail without changing the shell. It's a `class_attribute`, so it's inherited — a portal opts its entire surface in or out by calling `rail false` (or `rail true`) once in its controller concern:
|
|
30
59
|
|
|
31
60
|
```ruby
|
|
32
61
|
module CustomerPortal
|
|
@@ -39,7 +68,7 @@ module CustomerPortal
|
|
|
39
68
|
end
|
|
40
69
|
```
|
|
41
70
|
|
|
42
|
-
`rail nil` (the default) inherits the shell
|
|
71
|
+
`rail nil` (the default) inherits the resolved shell — the rail shows when the resolved `shell == :modern`. Read the resolved value with the `rail?` predicate.
|
|
43
72
|
|
|
44
73
|
### Stable CSS hooks
|
|
45
74
|
|
|
@@ -47,7 +47,8 @@ module Plutonium
|
|
|
47
47
|
helper_method :registered_resources
|
|
48
48
|
|
|
49
49
|
class_attribute :_rail_enabled, instance_writer: false, default: nil
|
|
50
|
-
|
|
50
|
+
class_attribute :_shell, instance_writer: false, default: nil
|
|
51
|
+
helper_method :rail?, :shell
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
class_methods do
|
|
@@ -56,6 +57,12 @@ module Plutonium
|
|
|
56
57
|
def rail(enabled)
|
|
57
58
|
self._rail_enabled = enabled
|
|
58
59
|
end
|
|
60
|
+
|
|
61
|
+
# Set the shell variant for this controller and its subclasses,
|
|
62
|
+
# overriding the engine/global default. nil (default) inherits.
|
|
63
|
+
def shell(value)
|
|
64
|
+
self._shell = value
|
|
65
|
+
end
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
# Whether the modern icon rail is active for this request. Resolves the
|
|
@@ -63,7 +70,14 @@ module Plutonium
|
|
|
63
70
|
# Public: the resource layout calls `controller.rail?`.
|
|
64
71
|
def rail?
|
|
65
72
|
return _rail_enabled unless _rail_enabled.nil?
|
|
66
|
-
|
|
73
|
+
shell == :modern
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def shell
|
|
77
|
+
return _shell unless _shell.nil?
|
|
78
|
+
|
|
79
|
+
engine = current_engine
|
|
80
|
+
(engine == Rails.application.class) ? Plutonium.configuration.shell : engine.shell
|
|
67
81
|
end
|
|
68
82
|
|
|
69
83
|
private
|
|
@@ -10,6 +10,17 @@ module Plutonium
|
|
|
10
10
|
included do
|
|
11
11
|
isolate_namespace to_s.deconstantize.constantize
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
class_methods do
|
|
15
|
+
# Shell variant for this portal's controllers (:modern / :plain /
|
|
16
|
+
# :classic). When unset it cascades live to the global
|
|
17
|
+
# Plutonium.configuration.shell; a controller's own `shell` overrides
|
|
18
|
+
# this in turn. Set with `shell :plain`, read with `shell`.
|
|
19
|
+
def shell(value = nil)
|
|
20
|
+
@shell = value unless value.nil?
|
|
21
|
+
@shell.nil? ? Plutonium.configuration.shell : @shell
|
|
22
|
+
end
|
|
23
|
+
end
|
|
13
24
|
end
|
|
14
25
|
end
|
|
15
26
|
end
|
|
@@ -13,16 +13,26 @@ module Plutonium
|
|
|
13
13
|
@grid_class = grid_class
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
SECTION_CLASS = "space-y-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
SECTION_CLASS = "space-y-5 border-t border-[var(--pu-border)] pt-7 first:border-t-0 first:pt-0"
|
|
17
|
+
# A short primary accent rule to the left of the heading — anchors the
|
|
18
|
+
# section and adds a touch of brand. Shared by the grouped <div> header
|
|
19
|
+
# and the collapsible <summary> so both read the same.
|
|
20
|
+
ACCENT_CLASS = "border-l-[3px] border-primary-500 pl-3.5"
|
|
21
|
+
HEADING_CLASS = "text-base font-semibold tracking-tight text-[var(--pu-text)]"
|
|
22
|
+
SUMMARY_CLASS = "#{HEADING_CLASS} #{ACCENT_CLASS} cursor-pointer select-none"
|
|
23
|
+
# font-normal resets the semibold inherited from a <summary> parent.
|
|
24
|
+
DESCRIPTION_CLASS = "mt-1 text-sm font-normal text-[var(--pu-text-muted)]"
|
|
20
25
|
|
|
21
26
|
def view_template(&fields_block)
|
|
22
27
|
if @section.collapsible?
|
|
23
28
|
details(open: !@section.collapsed?, class: SECTION_CLASS) do
|
|
24
|
-
summary
|
|
25
|
-
|
|
29
|
+
# <summary> must be the first child of <details> and can't be
|
|
30
|
+
# wrapped, so the title text and its description both live inside
|
|
31
|
+
# it — keeping the description hugging the title under one accent.
|
|
32
|
+
summary(class: SUMMARY_CLASS) do
|
|
33
|
+
plain heading_text
|
|
34
|
+
describe
|
|
35
|
+
end
|
|
26
36
|
grid(&fields_block)
|
|
27
37
|
end
|
|
28
38
|
else
|
|
@@ -35,10 +45,15 @@ module Plutonium
|
|
|
35
45
|
|
|
36
46
|
private
|
|
37
47
|
|
|
48
|
+
# Title + description grouped under one accented header so the
|
|
49
|
+
# description hugs the heading (mt-1) instead of inheriting the
|
|
50
|
+
# section's larger vertical rhythm.
|
|
38
51
|
def header_block
|
|
39
52
|
return if @section.ungrouped? && @section.options[:label].nil?
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
div(class: ACCENT_CLASS) do
|
|
54
|
+
h3(class: HEADING_CLASS) { heading_text }
|
|
55
|
+
describe
|
|
56
|
+
end
|
|
42
57
|
end
|
|
43
58
|
|
|
44
59
|
def heading_text = @section.label
|
|
@@ -122,6 +122,16 @@ module Plutonium
|
|
|
122
122
|
condition = section.condition
|
|
123
123
|
next if condition && !instance_exec(&condition)
|
|
124
124
|
|
|
125
|
+
# Drop sections left with no fields — e.g. every declared field was
|
|
126
|
+
# filtered out by the permitted set (policy, per-action, scoping,
|
|
127
|
+
# nesting). Rendering the chrome (heading + empty grid) for these
|
|
128
|
+
# litters the form with empty headings (notably on `+ New`, where
|
|
129
|
+
# fewer attributes are permitted than declared). Field-level
|
|
130
|
+
# `condition:` is evaluated later, at render — a section whose fields
|
|
131
|
+
# are all condition-hidden is the author's call to gate via the
|
|
132
|
+
# section's own `condition:`.
|
|
133
|
+
next if resolved.fields.empty?
|
|
134
|
+
|
|
125
135
|
# `columns` stays a validated literal; everything else may be a proc.
|
|
126
136
|
options = section.options.to_h do |key, value|
|
|
127
137
|
[key, (key != :condition && value.is_a?(Proc)) ? instance_exec(&value) : value]
|
|
@@ -52,7 +52,7 @@ module Plutonium
|
|
|
52
52
|
# signal) for non-classic shells, since Turbo Drive does not update
|
|
53
53
|
# <html> attributes across cross-URL visits.
|
|
54
54
|
def render_pre_paint_scripts
|
|
55
|
-
manage_no_rail =
|
|
55
|
+
manage_no_rail = pre_paint_shell != :classic
|
|
56
56
|
script do
|
|
57
57
|
raw(safe(<<~JS))
|
|
58
58
|
(function () {
|
|
@@ -80,6 +80,11 @@ module Plutonium
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Shell variant used by pre-paint scripts. Base uses the global config
|
|
84
|
+
# (safe for non-resource layouts whose controllers lack #shell);
|
|
85
|
+
# ResourceLayout overrides this to the resolved shell.
|
|
86
|
+
def pre_paint_shell = Plutonium.configuration.shell
|
|
87
|
+
|
|
83
88
|
def render_body(&)
|
|
84
89
|
body(**body_attributes) {
|
|
85
90
|
render_before_main
|
|
@@ -8,6 +8,12 @@ module Plutonium
|
|
|
8
8
|
# the controller's resolution (shell default + per-controller `rail`).
|
|
9
9
|
def rail? = controller.rail?
|
|
10
10
|
|
|
11
|
+
def shell = controller.shell
|
|
12
|
+
|
|
13
|
+
# Override Base's seam so all pre-paint shell logic uses the resolved
|
|
14
|
+
# (controller/engine/global) shell rather than only the global config.
|
|
15
|
+
def pre_paint_shell = shell
|
|
16
|
+
|
|
11
17
|
# Sets pu-rail-pinned immediately on initial page load so the rail
|
|
12
18
|
# renders in its pinned state from the first frame. Turbo navigations
|
|
13
19
|
# are handled by the turbo:before-render listener in Base. Skipped
|
|
@@ -34,13 +40,13 @@ module Plutonium
|
|
|
34
40
|
# Scoped to the modern family — :classic keeps its own offsets.
|
|
35
41
|
def html_attributes
|
|
36
42
|
attrs = super
|
|
37
|
-
return attrs if
|
|
43
|
+
return attrs if shell == :classic
|
|
38
44
|
|
|
39
45
|
rail? ? attrs : mix(attrs, {class: "pu-no-rail"})
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def main_attributes
|
|
43
|
-
classes = if
|
|
49
|
+
classes = if shell == :classic
|
|
44
50
|
"pt-20 lg:ml-64"
|
|
45
51
|
else
|
|
46
52
|
rail? ? "pt-16 pb-6 px-6 lg:pl-20" : "pt-16 pb-6 px-6"
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED