plutonium 0.49.0 → 0.50.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/.claude/skills/plutonium-definition/SKILL.md +87 -2
- data/.claude/skills/plutonium-installation/SKILL.md +6 -0
- data/.claude/skills/plutonium-invites/SKILL.md +41 -0
- data/.claude/skills/plutonium-views/SKILL.md +59 -0
- data/CHANGELOG.md +27 -0
- data/app/assets/plutonium.css +2 -2
- data/app/assets/plutonium.js +404 -25
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +45 -45
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_flash.html.erb +1 -1
- data/app/views/plutonium/_resource_header.html.erb +4 -4
- data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
- data/app/views/resource/_resource_grid.html.erb +1 -0
- data/config/brakeman.ignore +25 -2
- data/docs/guides/user-invites.md +64 -0
- data/docs/reference/definition/actions.md +14 -1
- data/docs/reference/definition/index.md +58 -0
- data/docs/reference/views/index.md +43 -0
- data/docs/superpowers/plans/2026-05-06-multi-invite-model-support.md +1487 -0
- data/docs/superpowers/plans/2026-05-06-multi-invite-model-support.md.tasks.json +15 -0
- data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
- data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
- data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
- data/lib/generators/pu/core/update/update_generator.rb +20 -0
- data/lib/generators/pu/invites/install_generator.rb +136 -35
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +8 -2
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +7 -1
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +9 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +8 -8
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +13 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +3 -3
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +5 -1
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
- data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +5 -1
- data/lib/plutonium/action/base.rb +44 -1
- data/lib/plutonium/action/interactive.rb +1 -1
- data/lib/plutonium/configuration.rb +4 -0
- data/lib/plutonium/definition/actions.rb +3 -0
- data/lib/plutonium/definition/base.rb +8 -0
- data/lib/plutonium/definition/metadata.rb +40 -0
- data/lib/plutonium/definition/views.rb +94 -0
- data/lib/plutonium/helpers/turbo_helper.rb +1 -1
- data/lib/plutonium/interaction/response/redirect.rb +1 -1
- data/lib/plutonium/invites/concerns/invite_token.rb +11 -3
- data/lib/plutonium/invites/concerns/invite_user.rb +13 -4
- data/lib/plutonium/invites/controller.rb +14 -1
- data/lib/plutonium/invites/pending_invite_check.rb +37 -28
- data/lib/plutonium/query/base.rb +8 -0
- data/lib/plutonium/query/filters/association.rb +30 -8
- data/lib/plutonium/query/filters/boolean.rb +5 -0
- data/lib/plutonium/resource/controllers/interactive_actions.rb +13 -9
- data/lib/plutonium/resource/controllers/presentable.rb +11 -2
- data/lib/plutonium/resource/definition.rb +42 -0
- data/lib/plutonium/resource/policy.rb +23 -8
- data/lib/plutonium/resource/query_object.rb +64 -6
- data/lib/plutonium/testing/resource_definition.rb +2 -2
- data/lib/plutonium/ui/action_button.rb +4 -2
- data/lib/plutonium/ui/component/kit.rb +12 -0
- data/lib/plutonium/ui/display/base.rb +3 -1
- data/lib/plutonium/ui/display/resource.rb +109 -25
- data/lib/plutonium/ui/display/theme.rb +2 -1
- data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
- data/lib/plutonium/ui/empty_card.rb +1 -1
- data/lib/plutonium/ui/form/base.rb +29 -1
- data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +79 -1
- data/lib/plutonium/ui/form/components/secure_association.rb +7 -2
- data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
- data/lib/plutonium/ui/form/resource.rb +48 -9
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
- data/lib/plutonium/ui/grid/card.rb +235 -0
- data/lib/plutonium/ui/grid/resource.rb +149 -0
- data/lib/plutonium/ui/layout/base.rb +37 -1
- data/lib/plutonium/ui/layout/header.rb +1 -2
- data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
- data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
- data/lib/plutonium/ui/layout/sidebar.rb +12 -24
- data/lib/plutonium/ui/layout/topbar.rb +100 -0
- data/lib/plutonium/ui/modal/base.rb +109 -0
- data/lib/plutonium/ui/modal/centered.rb +21 -0
- data/lib/plutonium/ui/modal/slideover.rb +26 -0
- data/lib/plutonium/ui/page/base.rb +25 -6
- data/lib/plutonium/ui/page/edit.rb +13 -1
- data/lib/plutonium/ui/page/index.rb +40 -1
- data/lib/plutonium/ui/page/interactive_action.rb +8 -39
- data/lib/plutonium/ui/page/new.rb +13 -1
- data/lib/plutonium/ui/page/show.rb +8 -1
- data/lib/plutonium/ui/page_header.rb +8 -13
- data/lib/plutonium/ui/panel.rb +10 -19
- data/lib/plutonium/ui/sidebar_menu.rb +2 -25
- data/lib/plutonium/ui/tab_list.rb +29 -7
- data/lib/plutonium/ui/table/base.rb +106 -0
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
- data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
- data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
- data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
- data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
- data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
- data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
- data/lib/plutonium/ui/table/resource.rb +158 -89
- data/lib/plutonium/ui/table/theme.rb +14 -5
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +6 -0
- data/package.json +1 -1
- data/src/css/components.css +304 -131
- data/src/css/tokens.css +101 -85
- data/src/js/controllers/autosubmit_controller.js +24 -0
- data/src/js/controllers/bulk_actions_controller.js +15 -16
- data/src/js/controllers/capture_url_controller.js +14 -0
- data/src/js/controllers/filter_panel_controller.js +77 -19
- data/src/js/controllers/flatpickr_controller.js +23 -0
- data/src/js/controllers/frame_navigator_controller.js +34 -6
- data/src/js/controllers/icon_rail_controller.js +22 -0
- data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
- data/src/js/controllers/register_controllers.js +16 -0
- data/src/js/controllers/resource_tab_list_controller.js +56 -3
- data/src/js/controllers/row_click_controller.js +21 -0
- data/src/js/controllers/sidebar_controller.js +28 -1
- data/src/js/controllers/table_column_menu_controller.js +43 -0
- data/src/js/controllers/table_header_controller.js +16 -0
- data/src/js/controllers/view_switcher_controller.js +29 -0
- metadata +33 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
<%= render "flash_toasts" %>
|
|
1
|
+
<%= render "plutonium/flash_toasts" %>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
<%= render Plutonium::UI::Layout::
|
|
2
|
-
<%
|
|
3
|
-
<%=
|
|
1
|
+
<%= render Plutonium::UI::Layout::Topbar.new do |bar| %>
|
|
2
|
+
<% bar.with_action do %>
|
|
3
|
+
<%= render Plutonium::UI::ColorModeSelector.new %>
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
|
-
<%
|
|
6
|
+
<% bar.with_action do %>
|
|
7
7
|
<%=
|
|
8
8
|
render Plutonium::UI::NavUser.new(
|
|
9
9
|
name: nil,
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
<%= render Plutonium::UI::Layout::
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Phlexi::Menu::Builder.new do |m|
|
|
5
|
-
m.item "Dashboard",
|
|
6
|
-
url: root_path,
|
|
7
|
-
icon: Phlex::TablerIcons::Home
|
|
1
|
+
<%= render Plutonium::UI::Layout::IconRail.new(
|
|
2
|
+
menu: Phlexi::Menu::Builder.new do |m|
|
|
3
|
+
m.item "Dashboard", url: root_path, icon: Phlex::TablerIcons::Home
|
|
8
4
|
|
|
9
5
|
m.item "Resources", icon: Phlex::TablerIcons::GridDots do |n|
|
|
10
6
|
registered_resources.each do |resource|
|
|
@@ -12,6 +8,10 @@
|
|
|
12
8
|
end
|
|
13
9
|
end
|
|
14
10
|
end
|
|
15
|
-
)
|
|
16
|
-
%>
|
|
11
|
+
) do |rail| %>
|
|
12
|
+
<% if respond_to?(:resource_logo_tag) %>
|
|
13
|
+
<% rail.with_brand do %>
|
|
14
|
+
<%= resource_logo_tag(classname: "h-8 w-8 rounded-md") %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
17
|
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= render build_grid_collection %>
|
data/config/brakeman.ignore
CHANGED
|
@@ -120,13 +120,13 @@
|
|
|
120
120
|
{
|
|
121
121
|
"warning_type": "Dangerous Eval",
|
|
122
122
|
"warning_code": 13,
|
|
123
|
-
"fingerprint": "
|
|
123
|
+
"fingerprint": "e5f68f8342a094ffd2124c1650428135e0549da39950158725414bfca0bb8f1e",
|
|
124
124
|
"check_name": "Evaluation",
|
|
125
125
|
"message": "Dynamic string evaluated as code",
|
|
126
126
|
"file": "lib/plutonium/auth/rodauth.rb",
|
|
127
127
|
"line": 6,
|
|
128
128
|
"link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
|
|
129
|
-
"code": "Module.new.module_eval(\"
|
|
129
|
+
"code": "Module.new.module_eval(\"...\", \"lib/plutonium/auth/rodauth.rb\", 7)",
|
|
130
130
|
"render_path": null,
|
|
131
131
|
"location": {
|
|
132
132
|
"type": "method",
|
|
@@ -233,6 +233,29 @@
|
|
|
233
233
|
89
|
|
234
234
|
],
|
|
235
235
|
"note": "False positive: 'key' is the filter attribute name from definition code, not user input. User input (query) is parameterized."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"warning_type": "Redirect",
|
|
239
|
+
"warning_code": 18,
|
|
240
|
+
"fingerprint": "ce92f474802c81eb79c01577415a82ef604bddcb8072b68c5508260b2b0b7469",
|
|
241
|
+
"check_name": "Redirect",
|
|
242
|
+
"message": "Possible unprotected redirect",
|
|
243
|
+
"file": "lib/plutonium/invites/controller.rb",
|
|
244
|
+
"line": 76,
|
|
245
|
+
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
|
246
|
+
"code": "redirect_to(invitation_path_for(params[:token]), :alert => \"Please sign in to accept this invitation\")",
|
|
247
|
+
"render_path": null,
|
|
248
|
+
"location": {
|
|
249
|
+
"type": "method",
|
|
250
|
+
"class": "Plutonium::Invites::Controller",
|
|
251
|
+
"method": "accept"
|
|
252
|
+
},
|
|
253
|
+
"user_input": "params[:token]",
|
|
254
|
+
"confidence": "Weak",
|
|
255
|
+
"cwe_id": [
|
|
256
|
+
601
|
|
257
|
+
],
|
|
258
|
+
"note": "False positive: params[:token] is interpolated into a route helper as a single path segment, not used as the redirect URL itself. The destination is a server-controlled named route."
|
|
236
259
|
}
|
|
237
260
|
],
|
|
238
261
|
"brakeman_version": "7.1.1"
|
data/docs/guides/user-invites.md
CHANGED
|
@@ -48,6 +48,7 @@ rails g pu:invites:install \
|
|
|
48
48
|
|--------|---------|-------------|
|
|
49
49
|
| `--entity-model` | Entity | Entity model for scoping invites |
|
|
50
50
|
| `--user-model` | User | User account model |
|
|
51
|
+
| `--invite-model` | `<EntityModel><UserModel>Invite` | Invite class name. Omit for single-flow apps; set per-invocation to run the generator more than once for distinct flows. |
|
|
51
52
|
| `--membership-model` | EntityUser | Join model for memberships |
|
|
52
53
|
| `--roles` | member,admin | Available invitation roles |
|
|
53
54
|
| `--rodauth` | user | Rodauth configuration name |
|
|
@@ -489,6 +490,69 @@ invite.expired?
|
|
|
489
490
|
invite.cancelled?
|
|
490
491
|
```
|
|
491
492
|
|
|
493
|
+
## Multiple invite flows in one app
|
|
494
|
+
|
|
495
|
+
Some apps invite users to several distinct kinds of entity — for example, customers joining organizations and funders joining projects. Run `pu:invites:install` once per flow; each invocation produces independent migrations, models, policies, definitions, mailers, controllers, view templates, and route helpers.
|
|
496
|
+
|
|
497
|
+
### Default-derivation rule
|
|
498
|
+
|
|
499
|
+
When you omit `--invite-model`, the generator derives the class name as `<EntityModel><UserModel>Invite`. With the defaults (`--entity-model=Organization --user-model=User`) the generated class is `Invites::OrganizationUserInvite` — there is no literal `UserInvite` default. Single-flow apps never need to pass `--invite-model`.
|
|
500
|
+
|
|
501
|
+
Multi-flow apps either:
|
|
502
|
+
|
|
503
|
+
- Run the generator more than once with the **same** entity/user but different `--invite-model` values (rare), or
|
|
504
|
+
- Run with **different** `--entity-model` / `--user-model` pairs (common). Different derivations make `--invite-model` unnecessary; pass it only when you want a custom class name.
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
rails g pu:invites:install \
|
|
508
|
+
--entity-model=FunderOrganization \
|
|
509
|
+
--user-model=SpenderAccount \
|
|
510
|
+
--invite-model=FunderInvite
|
|
511
|
+
|
|
512
|
+
rails g pu:invites:install \
|
|
513
|
+
--entity-model=Project \
|
|
514
|
+
--user-model=Member \
|
|
515
|
+
--invite-model=ProjectInvite
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
After the second invocation you'll have, for example, `Invites::FunderInvite` on table `funder_invites` with controller `Invites::FunderInvitationsController` mounted at `/funder_invitations/:token`, alongside `Invites::ProjectInvite` on `project_invites` mounted at `/project_invitations/:token`. Route helpers are prefixed (`funder_invitation_path`, `project_invitation_path`).
|
|
519
|
+
|
|
520
|
+
### How the welcome flow finds pending invites
|
|
521
|
+
|
|
522
|
+
The shared `Invites::WelcomeController` keeps a running list of invite classes. Each install run injects its class into the array, preserving order:
|
|
523
|
+
|
|
524
|
+
```ruby
|
|
525
|
+
# packages/invites/app/controllers/invites/welcome_controller.rb
|
|
526
|
+
def invite_classes
|
|
527
|
+
[::Invites::FunderInvite, ::Invites::ProjectInvite]
|
|
528
|
+
end
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
After login, `Plutonium::Invites::PendingInviteCheck#pending_invite` iterates `invite_classes` and returns the first valid pending invite for the cookie-stored token (first-match wins). Plug a third-party invite class in by overriding `invite_classes` directly:
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
class WelcomeController < ApplicationController
|
|
535
|
+
include Plutonium::Invites::PendingInviteCheck
|
|
536
|
+
|
|
537
|
+
def invite_classes
|
|
538
|
+
[::Invites::FunderInvite, ::Invites::ProjectInvite, ::Foreign::ApiInvite]
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Per-flow controller overrides
|
|
544
|
+
|
|
545
|
+
The install generator emits an `invitation_path_for` override on each invitations controller so post-login redirects target the correct prefixed route:
|
|
546
|
+
|
|
547
|
+
```ruby
|
|
548
|
+
# packages/invites/app/controllers/invites/funder_invitations_controller.rb
|
|
549
|
+
def invitation_path_for(token)
|
|
550
|
+
funder_invitation_path(token: token)
|
|
551
|
+
end
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
You won't normally edit this — but it's worth knowing the hook exists, because that's how multi-flow controllers stay independent from the shared `Plutonium::Invites::Controller` concern.
|
|
555
|
+
|
|
492
556
|
## Next Steps
|
|
493
557
|
|
|
494
558
|
- [Authentication](/guides/authentication) - Set up Rodauth
|
|
@@ -98,7 +98,20 @@ action :name,
|
|
|
98
98
|
confirmation: "Are you sure?", # Confirmation dialog
|
|
99
99
|
turbo_frame: "_top", # Turbo frame target
|
|
100
100
|
return_to: "/custom/path", # Override return URL
|
|
101
|
-
route_options: {action: :foo}
|
|
101
|
+
route_options: {action: :foo}, # Route configuration
|
|
102
|
+
|
|
103
|
+
# Dialog chrome (for interactive actions with a form)
|
|
104
|
+
modal: :slideover # :centered (default) or :slideover
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `Action#with(...)`
|
|
108
|
+
|
|
109
|
+
Action records are frozen value objects. To derive a variant — typically inside `customize_actions` — call `existing.with(...)` for a new copy with overrides applied:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
def customize_actions
|
|
113
|
+
defined_actions[:edit] = defined_actions[:edit].with(turbo_frame: "_top")
|
|
114
|
+
end
|
|
102
115
|
```
|
|
103
116
|
|
|
104
117
|
## Route Options
|
|
@@ -164,11 +164,69 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
|
164
164
|
# true = always show
|
|
165
165
|
# false = always hide
|
|
166
166
|
submit_and_continue false
|
|
167
|
+
|
|
168
|
+
# How `:new` / `:edit` render. Default is :slideover.
|
|
169
|
+
# :slideover — slide-in panel from the right (default)
|
|
170
|
+
# :centered — centered modal dialog
|
|
171
|
+
# false — full standalone pages (no modal)
|
|
172
|
+
modal :centered
|
|
167
173
|
end
|
|
168
174
|
```
|
|
169
175
|
|
|
170
176
|
Singular resources (e.g., `resource :profile` routes or `has_one` nested) auto-hide the secondary submit button since creating "another" doesn't make sense.
|
|
171
177
|
|
|
178
|
+
The `modal` setting only retargets the framework-provided `:new` / `:edit` actions. Custom interactive actions render in their own dialog whose chrome is set on the action via the per-action `modal:` option (`:centered` default, or `:slideover`) — see [Actions](./actions#action-options).
|
|
179
|
+
|
|
180
|
+
## Show Page Metadata Panel
|
|
181
|
+
|
|
182
|
+
The `metadata` DSL declares a list of fields that render in a right-side aside on the show page as label/value rows, leaving the main card focused on the record's substance.
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
class PostDefinition < Plutonium::Resource::Definition
|
|
186
|
+
metadata :author, :state, :created_at, :updated_at
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **Opt-in.** Without a `metadata` call, the show page renders full-width with no aside.
|
|
191
|
+
- **Policy-aware.** Fields are intersected with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
|
|
192
|
+
- **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
|
|
193
|
+
- **Responsive.** Side-by-side at `lg+`, stacked single-column below.
|
|
194
|
+
|
|
195
|
+
Field formatting (label, `as:`, blocks) is shared with the main card — declare once via `field` / `display` and the metadata panel inherits it.
|
|
196
|
+
|
|
197
|
+
## Index Views (Table & Grid)
|
|
198
|
+
|
|
199
|
+
Resources can opt into a card-based **Grid** view alongside the default **Table** view. The user can switch between them via the toolbar; the choice is persisted per-resource via cookie.
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
class UserDefinition < Plutonium::Resource::Definition
|
|
203
|
+
views :table, :grid # enable both
|
|
204
|
+
default_view :grid # initial view if no cookie
|
|
205
|
+
|
|
206
|
+
grid_fields(
|
|
207
|
+
image: :avatar, # ActiveStorage attachment, Shrine, or URL string
|
|
208
|
+
header: :name, # falls back to record.to_label
|
|
209
|
+
subheader: :email,
|
|
210
|
+
body: :bio,
|
|
211
|
+
meta: [:role, :status], # rendered as small pills
|
|
212
|
+
footer: :last_seen_at # falls back to :created_at
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
grid_layout :media # :compact (default) or :media
|
|
216
|
+
grid_columns 3 # pin to 3 cols on lg+; default is 1/2/3/4 responsive
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Method | Purpose |
|
|
221
|
+
|--------|---------|
|
|
222
|
+
| `views :table, :grid` | Which views are available. Default `[:table]`. |
|
|
223
|
+
| `default_view :grid` | Initial view when no cookie. Falls back to first declared view. |
|
|
224
|
+
| `grid_fields(...)` | Maps card slots to fields. **Implicitly enables `:grid`** if not already declared. |
|
|
225
|
+
| `grid_layout :media` | `:compact` (image left of content) or `:media` (full-width image on top). |
|
|
226
|
+
| `grid_columns 3` | Override responsive column count on `lg+`. Default is 1 / 2 / 3 / 4 at sm/md/lg/xl. |
|
|
227
|
+
|
|
228
|
+
Grid slots — `:image`, `:header`, `:subheader`, `:body`, `:meta`, `:footer` — are all optional. `:meta` accepts an array; the rest are single fields. Slots that point at fields not permitted by the user's policy collapse silently.
|
|
229
|
+
|
|
172
230
|
## Custom Page Classes
|
|
173
231
|
|
|
174
232
|
Override default page components:
|
|
@@ -338,6 +338,49 @@ class PostDefinition < ResourceDefinition
|
|
|
338
338
|
end
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
+
## Page Chrome (Shell)
|
|
342
|
+
|
|
343
|
+
`Plutonium.configuration.shell` controls the layout shipped above the resource pages. The default is **`:modern`** (topbar + icon rail) — leave it alone unless you're upgrading from a pre-`:modern` version and want to keep the legacy header + sidebar:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
Plutonium.configure do |config|
|
|
347
|
+
config.shell = :classic
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
To customize the shipped chrome per-portal, eject the templates:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
rails generate pu:eject:shell --dest=admin_portal
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
This copies `_resource_header.html.erb` and `_resource_sidebar.html.erb` into the portal's `app/views/plutonium/`.
|
|
358
|
+
|
|
359
|
+
## Modal & Slideover Forms
|
|
360
|
+
|
|
361
|
+
Framework-provided `:new` / `:edit` actions render inline inside a modal. Choose the chrome per-resource via [`modal`](/reference/definition/#form-configuration) on the definition (`:slideover` default, `:centered`, or `false`).
|
|
362
|
+
|
|
363
|
+
Custom interactive actions render in their own dialog. Each action carries its own [`modal:` option](/reference/definition/actions#action-options) (`:centered` default, or `:slideover`).
|
|
364
|
+
|
|
365
|
+
### Detecting Render Context
|
|
366
|
+
|
|
367
|
+
In custom Page / Form components:
|
|
368
|
+
|
|
369
|
+
| Helper | True when |
|
|
370
|
+
|--------|-----------|
|
|
371
|
+
| `in_frame?` | Request is targeting a turbo-frame |
|
|
372
|
+
| `in_modal?` | Request is rendering inside a modal/slideover |
|
|
373
|
+
|
|
374
|
+
Use them to pin action strips, omit nav chrome, or swap layouts.
|
|
375
|
+
|
|
376
|
+
## Tabs & URL Hash
|
|
377
|
+
|
|
378
|
+
Show pages with associations render the **Details** tab first followed by one tab per permitted association. The active tab is reflected in the URL hash (`#products`, `#refund-requests`) so the page deep-links and the active state survives reloads / back navigation. Tab rows scroll horizontally on narrow viewports rather than wrapping.
|
|
379
|
+
|
|
380
|
+
## Show Page Metadata Panel
|
|
381
|
+
|
|
382
|
+
The [`metadata` DSL](/reference/definition/#show-page-metadata-panel) on the definition opts a resource into a right-side aside that renders selected fields as label/value rows alongside the main details. The aside collapses to a single-column stack below the `lg` breakpoint and disappears entirely when no listed field is permitted by policy.
|
|
383
|
+
|
|
341
384
|
## Layout Customization
|
|
342
385
|
|
|
343
386
|
### Custom Layout Class
|