plutonium 0.49.1 → 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-views/SKILL.md +59 -0
- data/CHANGELOG.md +12 -0
- data/app/assets/plutonium.css +2 -2
- data/app/assets/plutonium.js +369 -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/_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/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-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_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/lite/rails_pulse/rails_pulse_generator.rb +54 -5
- 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/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/presentable.rb +11 -2
- data/lib/plutonium/resource/definition.rb +42 -0
- 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/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/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 +31 -3
|
@@ -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"
|
|
@@ -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
|