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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-definition/SKILL.md +87 -2
  3. data/.claude/skills/plutonium-installation/SKILL.md +6 -0
  4. data/.claude/skills/plutonium-views/SKILL.md +59 -0
  5. data/CHANGELOG.md +12 -0
  6. data/app/assets/plutonium.css +2 -2
  7. data/app/assets/plutonium.js +369 -25
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +45 -45
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/app/views/plutonium/_resource_header.html.erb +4 -4
  12. data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
  13. data/app/views/resource/_resource_grid.html.erb +1 -0
  14. data/config/brakeman.ignore +25 -2
  15. data/docs/reference/definition/actions.md +14 -1
  16. data/docs/reference/definition/index.md +58 -0
  17. data/docs/reference/views/index.md +43 -0
  18. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
  19. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
  20. data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
  21. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  22. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
  23. data/lib/generators/pu/core/update/update_generator.rb +20 -0
  24. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
  25. data/lib/plutonium/action/base.rb +44 -1
  26. data/lib/plutonium/action/interactive.rb +1 -1
  27. data/lib/plutonium/configuration.rb +4 -0
  28. data/lib/plutonium/definition/actions.rb +3 -0
  29. data/lib/plutonium/definition/base.rb +8 -0
  30. data/lib/plutonium/definition/metadata.rb +40 -0
  31. data/lib/plutonium/definition/views.rb +94 -0
  32. data/lib/plutonium/helpers/turbo_helper.rb +1 -1
  33. data/lib/plutonium/interaction/response/redirect.rb +1 -1
  34. data/lib/plutonium/query/base.rb +8 -0
  35. data/lib/plutonium/query/filters/association.rb +30 -8
  36. data/lib/plutonium/query/filters/boolean.rb +5 -0
  37. data/lib/plutonium/resource/controllers/presentable.rb +11 -2
  38. data/lib/plutonium/resource/definition.rb +42 -0
  39. data/lib/plutonium/resource/query_object.rb +64 -6
  40. data/lib/plutonium/testing/resource_definition.rb +2 -2
  41. data/lib/plutonium/ui/action_button.rb +4 -2
  42. data/lib/plutonium/ui/component/kit.rb +12 -0
  43. data/lib/plutonium/ui/display/base.rb +3 -1
  44. data/lib/plutonium/ui/display/resource.rb +109 -25
  45. data/lib/plutonium/ui/display/theme.rb +2 -1
  46. data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
  47. data/lib/plutonium/ui/empty_card.rb +1 -1
  48. data/lib/plutonium/ui/form/base.rb +29 -1
  49. data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
  50. data/lib/plutonium/ui/form/components/resource_select.rb +79 -1
  51. data/lib/plutonium/ui/form/components/secure_association.rb +7 -2
  52. data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
  53. data/lib/plutonium/ui/form/resource.rb +48 -9
  54. data/lib/plutonium/ui/form/theme.rb +1 -1
  55. data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
  56. data/lib/plutonium/ui/grid/card.rb +235 -0
  57. data/lib/plutonium/ui/grid/resource.rb +149 -0
  58. data/lib/plutonium/ui/layout/base.rb +37 -1
  59. data/lib/plutonium/ui/layout/header.rb +1 -2
  60. data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
  61. data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
  62. data/lib/plutonium/ui/layout/sidebar.rb +12 -24
  63. data/lib/plutonium/ui/layout/topbar.rb +100 -0
  64. data/lib/plutonium/ui/modal/base.rb +109 -0
  65. data/lib/plutonium/ui/modal/centered.rb +21 -0
  66. data/lib/plutonium/ui/modal/slideover.rb +26 -0
  67. data/lib/plutonium/ui/page/base.rb +25 -6
  68. data/lib/plutonium/ui/page/edit.rb +13 -1
  69. data/lib/plutonium/ui/page/index.rb +40 -1
  70. data/lib/plutonium/ui/page/interactive_action.rb +8 -39
  71. data/lib/plutonium/ui/page/new.rb +13 -1
  72. data/lib/plutonium/ui/page/show.rb +8 -1
  73. data/lib/plutonium/ui/page_header.rb +8 -13
  74. data/lib/plutonium/ui/panel.rb +10 -19
  75. data/lib/plutonium/ui/sidebar_menu.rb +2 -25
  76. data/lib/plutonium/ui/tab_list.rb +29 -7
  77. data/lib/plutonium/ui/table/base.rb +106 -0
  78. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
  79. data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
  80. data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
  81. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
  82. data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
  83. data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
  84. data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
  85. data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
  86. data/lib/plutonium/ui/table/resource.rb +158 -89
  87. data/lib/plutonium/ui/table/theme.rb +14 -5
  88. data/lib/plutonium/version.rb +1 -1
  89. data/lib/plutonium.rb +6 -0
  90. data/package.json +1 -1
  91. data/src/css/components.css +304 -131
  92. data/src/css/tokens.css +101 -85
  93. data/src/js/controllers/autosubmit_controller.js +24 -0
  94. data/src/js/controllers/bulk_actions_controller.js +15 -16
  95. data/src/js/controllers/capture_url_controller.js +14 -0
  96. data/src/js/controllers/filter_panel_controller.js +77 -19
  97. data/src/js/controllers/frame_navigator_controller.js +34 -6
  98. data/src/js/controllers/icon_rail_controller.js +22 -0
  99. data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
  100. data/src/js/controllers/register_controllers.js +16 -0
  101. data/src/js/controllers/resource_tab_list_controller.js +56 -3
  102. data/src/js/controllers/row_click_controller.js +21 -0
  103. data/src/js/controllers/table_column_menu_controller.js +43 -0
  104. data/src/js/controllers/table_header_controller.js +16 -0
  105. data/src/js/controllers/view_switcher_controller.js +29 -0
  106. metadata +31 -3
@@ -1,9 +1,9 @@
1
- <%= render Plutonium::UI::Layout::Header.new do |header| %>
2
- <% header.with_brand_logo do %>
3
- <%= resource_logo_tag(classname: "h-10 rounded-md") %>
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
- <% header.with_action do %>
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::Sidebar.new do %>
2
- <%=
3
- render Plutonium::UI::SidebarMenu.new(
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 %>
@@ -120,13 +120,13 @@
120
120
  {
121
121
  "warning_type": "Dangerous Eval",
122
122
  "warning_code": 13,
123
- "fingerprint": "a14cf69ee743ba0aa405725f018c186d6fdfa3f3f57a381613de1b2048621170",
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(\" extend ActiveSupport::Concern\\n\\n included do\\n helper_method :current_user\\n helper_method :logout_url\\n end\\n\\n private\\n\\n def rodauth(name = :#{name})\\n instance = super(name)\\n instance.url_options = default_url_options.presence\\n instance\\n end\\n\\n def current_user\\n rodauth.rails_account\\n end\\n\\n def logout_url\\n rodauth.logout_path\\n end\\n\\n define_singleton_method(:to_s) { \\\"Plutonium::Auth::Rodauth(:#{name})\\\" }\\n define_singleton_method(:inspect) { \\\"Plutonium::Auth::Rodauth(:#{name})\\\" }\\n\", \"lib/plutonium/auth/rodauth.rb\", 7)",
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} # Route configuration
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