plutonium 0.50.0 → 0.51.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/SKILL.md +85 -102
- data/.claude/skills/plutonium-app/SKILL.md +572 -0
- data/.claude/skills/plutonium-auth/SKILL.md +163 -300
- data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
- data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
- data/.claude/skills/plutonium-testing/SKILL.md +6 -5
- data/.claude/skills/plutonium-ui/SKILL.md +900 -0
- data/CHANGELOG.md +27 -2
- data/Rakefile +2 -1
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1009 -1214
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +52 -51
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +37 -27
- data/docs/getting-started/index.md +22 -29
- data/docs/getting-started/installation.md +37 -80
- data/docs/getting-started/tutorial/index.md +4 -5
- data/docs/guides/adding-resources.md +66 -377
- data/docs/guides/authentication.md +94 -463
- data/docs/guides/authorization.md +124 -370
- data/docs/guides/creating-packages.md +94 -296
- data/docs/guides/custom-actions.md +121 -441
- data/docs/guides/index.md +22 -42
- data/docs/guides/multi-tenancy.md +116 -187
- data/docs/guides/nested-resources.md +103 -431
- data/docs/guides/search-filtering.md +123 -240
- data/docs/guides/testing.md +5 -4
- data/docs/guides/theming.md +157 -407
- data/docs/guides/troubleshooting.md +5 -3
- data/docs/guides/user-invites.md +106 -425
- data/docs/guides/user-profile.md +76 -243
- data/docs/index.md +1 -1
- data/docs/reference/app/generators.md +517 -0
- data/docs/reference/app/index.md +158 -0
- data/docs/reference/app/packages.md +146 -0
- data/docs/reference/app/portals.md +377 -0
- data/docs/reference/auth/accounts.md +230 -0
- data/docs/reference/auth/index.md +88 -0
- data/docs/reference/auth/profile.md +185 -0
- data/docs/reference/behavior/controllers.md +395 -0
- data/docs/reference/behavior/index.md +22 -0
- data/docs/reference/behavior/interactions.md +341 -0
- data/docs/reference/behavior/policies.md +417 -0
- data/docs/reference/index.md +56 -49
- data/docs/reference/resource/actions.md +423 -0
- data/docs/reference/resource/definition.md +508 -0
- data/docs/reference/resource/index.md +50 -0
- data/docs/reference/resource/model.md +348 -0
- data/docs/reference/resource/query.md +305 -0
- data/docs/reference/tenancy/entity-scoping.md +361 -0
- data/docs/reference/tenancy/index.md +36 -0
- data/docs/reference/tenancy/invites.md +393 -0
- data/docs/reference/tenancy/nested-resources.md +267 -0
- data/docs/reference/testing/index.md +287 -0
- data/docs/reference/ui/assets.md +400 -0
- data/docs/reference/ui/components.md +165 -0
- data/docs/reference/ui/displays.md +104 -0
- data/docs/reference/ui/forms.md +284 -0
- data/docs/reference/ui/index.md +30 -0
- data/docs/reference/ui/layouts.md +106 -0
- data/docs/reference/ui/pages.md +189 -0
- data/docs/reference/ui/tables.md +117 -0
- data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
- data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
- data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -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/update/update_generator.rb +0 -20
- data/lib/generators/pu/invites/install_generator.rb +1 -0
- data/lib/plutonium/definition/base.rb +1 -1
- data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
- data/lib/plutonium/helpers/turbo_helper.rb +11 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
- data/lib/plutonium/resource/controller.rb +1 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
- data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
- data/lib/plutonium/resource/policy.rb +7 -0
- data/lib/plutonium/routing/mapper_extensions.rb +15 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/form/base.rb +6 -2
- data/lib/plutonium/ui/form/components/json.rb +58 -0
- data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
- data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
- data/lib/plutonium/ui/form/resource.rb +0 -4
- data/lib/plutonium/ui/grid/resource.rb +1 -1
- data/lib/plutonium/ui/layout/base.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +0 -7
- data/lib/plutonium/ui/page/index.rb +4 -4
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +8 -0
- data/lib/tasks/release.rake +15 -1
- data/package.json +10 -10
- data/src/css/slim_select.css +4 -0
- data/src/js/controllers/slim_select_controller.js +61 -0
- data/src/js/turbo/turbo_actions.js +33 -0
- data/yarn.lock +553 -543
- metadata +44 -33
- data/.claude/skills/plutonium-assets/SKILL.md +0 -512
- data/.claude/skills/plutonium-controller/SKILL.md +0 -396
- data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
- data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
- data/.claude/skills/plutonium-forms/SKILL.md +0 -465
- data/.claude/skills/plutonium-installation/SKILL.md +0 -331
- data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
- data/.claude/skills/plutonium-invites/SKILL.md +0 -408
- data/.claude/skills/plutonium-model/SKILL.md +0 -440
- data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
- data/.claude/skills/plutonium-package/SKILL.md +0 -198
- data/.claude/skills/plutonium-policy/SKILL.md +0 -456
- data/.claude/skills/plutonium-portal/SKILL.md +0 -410
- data/.claude/skills/plutonium-views/SKILL.md +0 -651
- data/docs/reference/assets/index.md +0 -496
- data/docs/reference/controller/index.md +0 -412
- data/docs/reference/definition/actions.md +0 -462
- data/docs/reference/definition/fields.md +0 -383
- data/docs/reference/definition/index.md +0 -326
- data/docs/reference/definition/query.md +0 -351
- data/docs/reference/generators/index.md +0 -648
- data/docs/reference/interaction/index.md +0 -449
- data/docs/reference/model/features.md +0 -248
- data/docs/reference/model/index.md +0 -218
- data/docs/reference/policy/index.md +0 -456
- data/docs/reference/portal/index.md +0 -379
- data/docs/reference/views/forms.md +0 -411
- data/docs/reference/views/index.md +0 -544
|
@@ -1,651 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plutonium-views
|
|
3
|
-
description: Use BEFORE building a custom page, panel, table, layout, or Phlex component in Plutonium. Also when overriding IndexPage/ShowPage/Form classes in a definition.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Plutonium Views
|
|
7
|
-
|
|
8
|
-
## 🚨 Critical (read first)
|
|
9
|
-
- **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end` — don't replace the entire view layer.
|
|
10
|
-
- **Use the render hooks.** `render_before_content`, `render_after_content`, `render_before_toolbar`, etc. — they exist so you don't have to override `view_template` and re-implement everything.
|
|
11
|
-
- **All pages inherit `DynaFrameContent`** so turbo-frame requests render only the content. Don't fight it — modals and frame nav "just work".
|
|
12
|
-
- **For custom components, use `Plutonium::UI::Component::Base`** so you inherit the component kit (`PageHeader`, `Panel`, `Block`, etc.) and access to resource helpers.
|
|
13
|
-
- **Related skills:** `plutonium-forms` (form customization), `plutonium-assets` (theming + component classes), `plutonium-definition` (field-level rendering), `plutonium-controller` (presentation hooks like `present_parent?`).
|
|
14
|
-
|
|
15
|
-
Plutonium uses [Phlex](https://www.phlex.fun/) for all view components. This provides a Ruby-first approach to building HTML with full IDE support and type safety.
|
|
16
|
-
|
|
17
|
-
## Architecture Overview
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
Definition
|
|
21
|
-
├── IndexPage → renders Table
|
|
22
|
-
├── ShowPage → renders Display
|
|
23
|
-
├── NewPage → renders Form
|
|
24
|
-
├── EditPage → renders Form
|
|
25
|
-
└── InteractiveActionPage → renders Form
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Each definition has nested classes you can override:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
class PostDefinition < ResourceDefinition
|
|
32
|
-
class IndexPage < IndexPage; end
|
|
33
|
-
class ShowPage < ShowPage; end
|
|
34
|
-
class NewPage < NewPage; end
|
|
35
|
-
class EditPage < EditPage; end
|
|
36
|
-
class Form < Form; end
|
|
37
|
-
class Table < Table; end
|
|
38
|
-
class Display < Display; end
|
|
39
|
-
end
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Page Customization
|
|
43
|
-
|
|
44
|
-
### Page Configuration
|
|
45
|
-
|
|
46
|
-
Set page titles and descriptions in definitions:
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
class PostDefinition < ResourceDefinition
|
|
50
|
-
# Static titles
|
|
51
|
-
index_page_title "Blog Posts"
|
|
52
|
-
index_page_description "Manage all published articles"
|
|
53
|
-
|
|
54
|
-
show_page_title "Article Details"
|
|
55
|
-
new_page_title "Write New Article"
|
|
56
|
-
edit_page_title "Edit Article"
|
|
57
|
-
|
|
58
|
-
# Control breadcrumbs
|
|
59
|
-
breadcrumbs true # Global default
|
|
60
|
-
index_page_breadcrumbs false # Per-page override
|
|
61
|
-
show_page_breadcrumbs true
|
|
62
|
-
end
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Custom Page Class
|
|
66
|
-
|
|
67
|
-
Override page rendering by subclassing:
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
class PostDefinition < ResourceDefinition
|
|
71
|
-
class ShowPage < ShowPage
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
# Custom title logic
|
|
75
|
-
def page_title
|
|
76
|
-
"#{object.title} - #{object.author.name}"
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Add content before the main area
|
|
80
|
-
def render_before_content
|
|
81
|
-
div(class: "alert alert-info") {
|
|
82
|
-
"This post has #{object.comments.count} comments"
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Add content after
|
|
87
|
-
def render_after_content
|
|
88
|
-
render RelatedPostsComponent.new(post: object)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Override the toolbar
|
|
92
|
-
def render_toolbar
|
|
93
|
-
div(class: "flex gap-2") {
|
|
94
|
-
button(class: "btn") { "Preview" }
|
|
95
|
-
button(class: "btn btn-primary") { "Publish" }
|
|
96
|
-
}
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Page Hooks
|
|
103
|
-
|
|
104
|
-
All pages inherit these customization hooks:
|
|
105
|
-
|
|
106
|
-
| Hook | Purpose |
|
|
107
|
-
|------|---------|
|
|
108
|
-
| `render_before_header` | Before entire header section |
|
|
109
|
-
| `render_after_header` | After entire header section |
|
|
110
|
-
| `render_before_breadcrumbs` | Before breadcrumbs |
|
|
111
|
-
| `render_after_breadcrumbs` | After breadcrumbs |
|
|
112
|
-
| `render_before_page_header` | Before title/actions |
|
|
113
|
-
| `render_after_page_header` | After title/actions |
|
|
114
|
-
| `render_before_toolbar` | Before toolbar |
|
|
115
|
-
| `render_after_toolbar` | After toolbar |
|
|
116
|
-
| `render_before_content` | Before main content |
|
|
117
|
-
| `render_after_content` | After main content |
|
|
118
|
-
| `render_before_footer` | Before footer |
|
|
119
|
-
| `render_after_footer` | After footer |
|
|
120
|
-
|
|
121
|
-
### Custom View Files
|
|
122
|
-
|
|
123
|
-
For complete control, create custom ERB view files that replace the default entirely.
|
|
124
|
-
|
|
125
|
-
**File locations:**
|
|
126
|
-
|
|
127
|
-
```
|
|
128
|
-
# Main app (for a PostsController)
|
|
129
|
-
app/views/posts/index.html.erb
|
|
130
|
-
app/views/posts/show.html.erb
|
|
131
|
-
app/views/posts/new.html.erb
|
|
132
|
-
app/views/posts/edit.html.erb
|
|
133
|
-
|
|
134
|
-
# Portal-specific
|
|
135
|
-
packages/admin_portal/app/views/admin_portal/posts/show.html.erb
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**Default view structure:**
|
|
139
|
-
|
|
140
|
-
The default views simply render the page class:
|
|
141
|
-
|
|
142
|
-
```erb
|
|
143
|
-
<%# app/views/resource/show.html.erb %>
|
|
144
|
-
<%= render current_definition.show_page_class.new %>
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
**Custom view example:**
|
|
148
|
-
|
|
149
|
-
```erb
|
|
150
|
-
<%# app/views/posts/show.html.erb %>
|
|
151
|
-
<div class="max-w-4xl mx-auto">
|
|
152
|
-
<article class="prose lg:prose-xl">
|
|
153
|
-
<h1><%= resource_record!.title %></h1>
|
|
154
|
-
<div class="meta text-gray-500">
|
|
155
|
-
By <%= resource_record!.author.name %> on <%= resource_record!.created_at.strftime("%B %d, %Y") %>
|
|
156
|
-
</div>
|
|
157
|
-
<%= raw resource_record!.content %>
|
|
158
|
-
</article>
|
|
159
|
-
|
|
160
|
-
<div class="mt-8">
|
|
161
|
-
<%= link_to "Edit", resource_url_for(resource_record!, action: :edit), class: "btn" %>
|
|
162
|
-
<%= link_to "Back", resource_url_for(Post), class: "btn" %>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**Mixing approaches:**
|
|
168
|
-
|
|
169
|
-
Render the default page with additions:
|
|
170
|
-
|
|
171
|
-
```erb
|
|
172
|
-
<%# app/views/posts/show.html.erb %>
|
|
173
|
-
<div class="announcement-banner">
|
|
174
|
-
Special announcement here
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
<%= render current_definition.show_page_class.new %>
|
|
178
|
-
|
|
179
|
-
<div class="related-posts">
|
|
180
|
-
<%= render partial: "related_posts", locals: { post: resource_record! } %>
|
|
181
|
-
</div>
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
## Form Customization
|
|
185
|
-
|
|
186
|
-
### Custom Form Template
|
|
187
|
-
|
|
188
|
-
Override how fields are rendered:
|
|
189
|
-
|
|
190
|
-
```ruby
|
|
191
|
-
class PostDefinition < ResourceDefinition
|
|
192
|
-
class Form < Form
|
|
193
|
-
def form_template
|
|
194
|
-
# Custom layout with sections
|
|
195
|
-
div(class: "grid grid-cols-2 gap-6") {
|
|
196
|
-
div {
|
|
197
|
-
h3(class: "text-lg font-medium") { "Basic Info" }
|
|
198
|
-
render_resource_field :title
|
|
199
|
-
render_resource_field :slug
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
div {
|
|
203
|
-
h3(class: "text-lg font-medium") { "Content" }
|
|
204
|
-
render_resource_field :content
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
div(class: "mt-6") {
|
|
209
|
-
h3(class: "text-lg font-medium") { "Publishing" }
|
|
210
|
-
render_resource_field :published_at
|
|
211
|
-
render_resource_field :category
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
render_actions
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Form Methods
|
|
221
|
-
|
|
222
|
-
| Method | Purpose |
|
|
223
|
-
|--------|---------|
|
|
224
|
-
| `render_fields` | Render all permitted fields |
|
|
225
|
-
| `render_resource_field(name)` | Render a single field |
|
|
226
|
-
| `render_actions` | Render submit buttons |
|
|
227
|
-
| `record` | The form object (alias: `object`) |
|
|
228
|
-
| `resource_fields` | List of permitted field names |
|
|
229
|
-
| `resource_definition` | The definition instance |
|
|
230
|
-
|
|
231
|
-
## Display Customization
|
|
232
|
-
|
|
233
|
-
### Custom Display Template
|
|
234
|
-
|
|
235
|
-
Override the show page detail rendering:
|
|
236
|
-
|
|
237
|
-
```ruby
|
|
238
|
-
class PostDefinition < ResourceDefinition
|
|
239
|
-
class Display < Display
|
|
240
|
-
def display_template
|
|
241
|
-
# Hero section
|
|
242
|
-
div(class: "bg-gradient-to-r from-blue-500 to-purple-600 p-8 rounded-lg text-white mb-6") {
|
|
243
|
-
h1(class: "text-3xl font-bold") { object.title }
|
|
244
|
-
p(class: "mt-2 opacity-90") { object.excerpt }
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
# Main content
|
|
248
|
-
Block do
|
|
249
|
-
fields_wrapper do
|
|
250
|
-
render_resource_field :author
|
|
251
|
-
render_resource_field :published_at
|
|
252
|
-
render_resource_field :category
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Full-width content
|
|
257
|
-
Block do
|
|
258
|
-
div(class: "prose max-w-none") {
|
|
259
|
-
raw object.content
|
|
260
|
-
}
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
# Associations (tabs)
|
|
264
|
-
render_associations if present_associations?
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### Display Methods
|
|
271
|
-
|
|
272
|
-
| Method | Purpose |
|
|
273
|
-
|--------|---------|
|
|
274
|
-
| `render_fields` | Render all permitted fields in a block |
|
|
275
|
-
| `render_resource_field(name)` | Render single field |
|
|
276
|
-
| `render_associations` | Render association tabs |
|
|
277
|
-
| `object` | The record being displayed |
|
|
278
|
-
| `resource_fields` | List of permitted field names |
|
|
279
|
-
| `resource_associations` | List of permitted associations |
|
|
280
|
-
|
|
281
|
-
## Table Customization
|
|
282
|
-
|
|
283
|
-
### Custom Table Rendering
|
|
284
|
-
|
|
285
|
-
Override list page table:
|
|
286
|
-
|
|
287
|
-
```ruby
|
|
288
|
-
class PostDefinition < ResourceDefinition
|
|
289
|
-
class Table < Table
|
|
290
|
-
def view_template
|
|
291
|
-
render_search_bar
|
|
292
|
-
render_scopes_bar
|
|
293
|
-
|
|
294
|
-
if collection.empty?
|
|
295
|
-
render_empty_card
|
|
296
|
-
else
|
|
297
|
-
# Custom card grid instead of table
|
|
298
|
-
div(class: "grid grid-cols-3 gap-4") {
|
|
299
|
-
collection.each do |post|
|
|
300
|
-
render PostCardComponent.new(post:)
|
|
301
|
-
end
|
|
302
|
-
}
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
render_footer
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### Table Methods
|
|
312
|
-
|
|
313
|
-
| Method | Purpose |
|
|
314
|
-
|--------|---------|
|
|
315
|
-
| `render_search_bar` | Search input |
|
|
316
|
-
| `render_scopes_bar` | Scope tabs |
|
|
317
|
-
| `render_table` | Default table |
|
|
318
|
-
| `render_empty_card` | Empty state |
|
|
319
|
-
| `render_footer` | Pagination |
|
|
320
|
-
| `collection` | The paginated records |
|
|
321
|
-
| `resource_fields` | Column field names |
|
|
322
|
-
|
|
323
|
-
## Component Kit
|
|
324
|
-
|
|
325
|
-
Plutonium provides shorthand methods for common components:
|
|
326
|
-
|
|
327
|
-
```ruby
|
|
328
|
-
class MyPage < Plutonium::UI::Page::Base
|
|
329
|
-
def render_content
|
|
330
|
-
# These are automatically rendered
|
|
331
|
-
PageHeader(title: "Dashboard")
|
|
332
|
-
|
|
333
|
-
Panel(class: "mt-4") {
|
|
334
|
-
p { "Content here" }
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
Block {
|
|
338
|
-
TabList(items: tabs)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
EmptyCard("No items found")
|
|
342
|
-
|
|
343
|
-
ActionButton(action, url: "/posts/new")
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Available kit methods:
|
|
349
|
-
- `Breadcrumbs()`
|
|
350
|
-
- `PageHeader(title:, description:, actions:)`
|
|
351
|
-
- `Panel(**attrs)`
|
|
352
|
-
- `Block(**attrs)`
|
|
353
|
-
- `TabList(items:)`
|
|
354
|
-
- `EmptyCard(message)`
|
|
355
|
-
- `ActionButton(action, url:)`
|
|
356
|
-
- `DynaFrameHost(src:, loading:)` - Lazy-loading turbo frame
|
|
357
|
-
- `DynaFrameContent(content) { |frame| ... }` - Frame-aware content wrapper
|
|
358
|
-
- `TableSearchBar()`
|
|
359
|
-
- `TableScopesBar()`
|
|
360
|
-
- `TableInfo(pagy)`
|
|
361
|
-
- `TablePagination(pagy)`
|
|
362
|
-
|
|
363
|
-
## DynaFrameContent Pattern
|
|
364
|
-
|
|
365
|
-
`DynaFrameContent` enables frame-aware rendering. For turbo-frame requests, only the content is rendered inside the frame. For regular requests, the full page renders with header/footer.
|
|
366
|
-
|
|
367
|
-
```ruby
|
|
368
|
-
# How Page::Base uses DynaFrameContent
|
|
369
|
-
def view_template(&block)
|
|
370
|
-
DynaFrameContent(page_content(block)) do |frame|
|
|
371
|
-
render_header # Skipped for frame requests
|
|
372
|
-
frame.render_content # Always rendered (in turbo-frame for frame requests)
|
|
373
|
-
render_footer # Skipped for frame requests
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
This pattern means:
|
|
379
|
-
- **Regular page load**: Full page with header, content, footer
|
|
380
|
-
- **Turbo-frame request**: Only `<turbo-frame id="...">` with content inside
|
|
381
|
-
|
|
382
|
-
All pages automatically inherit this behavior. No special handling needed for modals or frame navigation.
|
|
383
|
-
|
|
384
|
-
## Custom Components
|
|
385
|
-
|
|
386
|
-
### Creating a Phlex Component
|
|
387
|
-
|
|
388
|
-
```ruby
|
|
389
|
-
# app/components/post_card_component.rb
|
|
390
|
-
class PostCardComponent < Plutonium::UI::Component::Base
|
|
391
|
-
def initialize(post:)
|
|
392
|
-
@post = post
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def view_template
|
|
396
|
-
div(class: "bg-white rounded-lg shadow p-4") {
|
|
397
|
-
h3(class: "font-bold") { @post.title }
|
|
398
|
-
p(class: "text-gray-600 mt-2") { @post.excerpt }
|
|
399
|
-
|
|
400
|
-
div(class: "mt-4 flex justify-between items-center") {
|
|
401
|
-
span(class: "text-sm text-gray-500") { @post.published_at&.strftime("%B %d, %Y") }
|
|
402
|
-
a(href: resource_url_for(@post), class: "text-blue-600") { "Read more" }
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### Using in Definitions
|
|
410
|
-
|
|
411
|
-
Reference components in field definitions:
|
|
412
|
-
|
|
413
|
-
```ruby
|
|
414
|
-
class PostDefinition < ResourceDefinition
|
|
415
|
-
# Custom display component
|
|
416
|
-
display :status, as: StatusBadgeComponent
|
|
417
|
-
|
|
418
|
-
# Custom input component
|
|
419
|
-
input :color, as: ColorPickerComponent
|
|
420
|
-
|
|
421
|
-
# Block with component
|
|
422
|
-
display :metrics do |field|
|
|
423
|
-
MetricsChartComponent.new(data: field.value)
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
## Page Chrome (Shell)
|
|
429
|
-
|
|
430
|
-
`Plutonium.configuration.shell` controls the layout shipped above the
|
|
431
|
-
resource pages. Default is **`:modern`** (topbar + icon rail) — only
|
|
432
|
-
override to **`:classic`** (legacy header + sidebar) if you're upgrading
|
|
433
|
-
from a pre-`:modern` version and want to preserve the old chrome:
|
|
434
|
-
|
|
435
|
-
```ruby
|
|
436
|
-
Plutonium.configure do |config|
|
|
437
|
-
config.shell = :classic
|
|
438
|
-
end
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
To customize the shipped chrome per-portal, eject the templates:
|
|
442
|
-
|
|
443
|
-
```bash
|
|
444
|
-
rails generate pu:eject:shell --dest=admin_portal
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
This copies `_resource_header.html.erb` and `_resource_sidebar.html.erb`
|
|
448
|
-
into the portal's `app/views/plutonium/`. The eject is independent of
|
|
449
|
-
`shell` — you can run it on either.
|
|
450
|
-
|
|
451
|
-
## Modal & Slideover Forms
|
|
452
|
-
|
|
453
|
-
The framework's `:new` / `:edit` actions render inline inside a modal.
|
|
454
|
-
Choose the chrome per-resource via the `modal` DSL on the definition:
|
|
455
|
-
|
|
456
|
-
```ruby
|
|
457
|
-
class PostDefinition < ResourceDefinition
|
|
458
|
-
modal :slideover # default — slide-in panel from the right
|
|
459
|
-
# modal :centered # centered dialog
|
|
460
|
-
# modal false # full standalone page (no modal)
|
|
461
|
-
end
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
Custom interactive actions render in their own dialog with their own
|
|
465
|
-
`modal:` option (`:centered` default, or `:slideover`).
|
|
466
|
-
|
|
467
|
-
### Detecting render context in components
|
|
468
|
-
|
|
469
|
-
Custom pages / forms can branch on render context:
|
|
470
|
-
|
|
471
|
-
| Helper | True when |
|
|
472
|
-
|--------|-----------|
|
|
473
|
-
| `in_frame?` | Request is targeting a turbo-frame |
|
|
474
|
-
| `in_modal?` | Request is rendering inside a modal/slideover |
|
|
475
|
-
|
|
476
|
-
Use them to pin action strips, omit nav chrome, or swap layouts.
|
|
477
|
-
|
|
478
|
-
## Tabs & URL Hash
|
|
479
|
-
|
|
480
|
-
Show pages with associations render the **Details** tab first followed
|
|
481
|
-
by one tab per permitted association. The active tab is reflected in
|
|
482
|
-
the URL hash (`#products`, `#refund-requests`) so the page deep-links and
|
|
483
|
-
the active state survives reloads / back navigation.
|
|
484
|
-
|
|
485
|
-
Tab rows scroll horizontally on narrow viewports — they don't wrap.
|
|
486
|
-
|
|
487
|
-
## Layout Customization
|
|
488
|
-
|
|
489
|
-
### Eject Layout
|
|
490
|
-
|
|
491
|
-
Copy the layout template to your project:
|
|
492
|
-
|
|
493
|
-
```bash
|
|
494
|
-
rails generate pu:eject:layout
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
This copies `layouts/resource.html.erb` to your portal.
|
|
498
|
-
|
|
499
|
-
### Custom Layout Class
|
|
500
|
-
|
|
501
|
-
Override the Phlex layout:
|
|
502
|
-
|
|
503
|
-
```ruby
|
|
504
|
-
# packages/admin_portal/app/views/layouts/admin_portal/resource_layout.rb
|
|
505
|
-
module AdminPortal
|
|
506
|
-
class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
|
|
507
|
-
private
|
|
508
|
-
|
|
509
|
-
# Custom body classes
|
|
510
|
-
def body_attributes
|
|
511
|
-
{class: "antialiased bg-slate-100 dark:bg-slate-900"}
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
# Add custom header content
|
|
515
|
-
def render_before_main
|
|
516
|
-
super
|
|
517
|
-
render AnnouncementBanner.new if Announcement.active.any?
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
# Custom scripts
|
|
521
|
-
def render_body_scripts
|
|
522
|
-
super
|
|
523
|
-
script(src: "/custom-analytics.js")
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
end
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
### Layout Hooks
|
|
530
|
-
|
|
531
|
-
| Hook | Purpose |
|
|
532
|
-
|------|---------|
|
|
533
|
-
| `render_before_main` | Before main content area |
|
|
534
|
-
| `render_after_main` | After main (modals, etc.) |
|
|
535
|
-
| `render_before_content` | Inside main, before content |
|
|
536
|
-
| `render_after_content` | Inside main, after content |
|
|
537
|
-
| `render_flash` | Flash messages |
|
|
538
|
-
| `render_head` | HTML head section |
|
|
539
|
-
| `render_title` | Page title tag |
|
|
540
|
-
| `render_metatags` | Meta tags |
|
|
541
|
-
| `render_assets` | CSS/JS assets |
|
|
542
|
-
| `render_body_scripts` | Scripts at end of body |
|
|
543
|
-
|
|
544
|
-
## Available Context
|
|
545
|
-
|
|
546
|
-
Both ERB views and Phlex components have access to the same context.
|
|
547
|
-
|
|
548
|
-
### Resource Methods
|
|
549
|
-
|
|
550
|
-
| Method | Description |
|
|
551
|
-
|--------|-------------|
|
|
552
|
-
| `resource_class` | The model class (e.g., `Post`) |
|
|
553
|
-
| `resource_record!` | Current record (raises if not found) |
|
|
554
|
-
| `resource_record?` | Current record (nil if not found) |
|
|
555
|
-
| `current_parent` | Parent record for nested routes |
|
|
556
|
-
| `current_scoped_entity` | Entity for multi-tenant portals |
|
|
557
|
-
|
|
558
|
-
### Definition & Policy
|
|
559
|
-
|
|
560
|
-
| Method | Description |
|
|
561
|
-
|--------|-------------|
|
|
562
|
-
| `current_definition` | Definition instance for current resource |
|
|
563
|
-
| `current_policy` | Policy instance for current record |
|
|
564
|
-
| `current_authorized_scope` | Scoped collection user can access |
|
|
565
|
-
|
|
566
|
-
### Authentication
|
|
567
|
-
|
|
568
|
-
| Method | Description |
|
|
569
|
-
|--------|-------------|
|
|
570
|
-
| `current_user` | Authenticated user (if using Rodauth) |
|
|
571
|
-
|
|
572
|
-
### URL Helpers
|
|
573
|
-
|
|
574
|
-
| Method | Description |
|
|
575
|
-
|--------|-------------|
|
|
576
|
-
| `resource_url_for(record)` | URL for a record |
|
|
577
|
-
| `resource_url_for(record, action: :edit)` | Action URL for record |
|
|
578
|
-
| `resource_url_for(Model)` | Index URL for model |
|
|
579
|
-
| `resource_url_for(Model, action: :new)` | New URL for model |
|
|
580
|
-
| `resource_url_for(record, parent: parent)` | Nested resource URL |
|
|
581
|
-
|
|
582
|
-
### Display Helpers
|
|
583
|
-
|
|
584
|
-
| Method | Description |
|
|
585
|
-
|--------|-------------|
|
|
586
|
-
| `display_name_of(record)` | Human-readable name for record |
|
|
587
|
-
| `resource_name(klass)` | Singular model name |
|
|
588
|
-
| `resource_name_plural(klass)` | Plural model name |
|
|
589
|
-
|
|
590
|
-
### In Phlex Components
|
|
591
|
-
|
|
592
|
-
```ruby
|
|
593
|
-
class MyComponent < Plutonium::UI::Component::Base
|
|
594
|
-
def view_template
|
|
595
|
-
# All the above methods work directly
|
|
596
|
-
current_user
|
|
597
|
-
resource_record!
|
|
598
|
-
resource_url_for(@post)
|
|
599
|
-
|
|
600
|
-
# Rails helpers via helpers proxy
|
|
601
|
-
helpers.link_to(...)
|
|
602
|
-
helpers.image_tag(...)
|
|
603
|
-
helpers.number_to_currency(...)
|
|
604
|
-
end
|
|
605
|
-
end
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
### In ERB Views
|
|
609
|
-
|
|
610
|
-
```erb
|
|
611
|
-
<%# All methods available directly %>
|
|
612
|
-
<%= resource_record!.title %>
|
|
613
|
-
<%= current_user.name %>
|
|
614
|
-
<%= link_to "Edit", resource_url_for(resource_record!, action: :edit) %>
|
|
615
|
-
|
|
616
|
-
<%# Render Phlex components %>
|
|
617
|
-
<%= render current_definition.show_page_class.new %>
|
|
618
|
-
<%= render MyCustomComponent.new(post: resource_record!) %>
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
## Portal-Specific Views
|
|
622
|
-
|
|
623
|
-
Each portal can have its own view overrides:
|
|
624
|
-
|
|
625
|
-
```ruby
|
|
626
|
-
# Base definition
|
|
627
|
-
class PostDefinition < ResourceDefinition
|
|
628
|
-
class ShowPage < ShowPage
|
|
629
|
-
# Default behavior
|
|
630
|
-
end
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
# Admin portal override
|
|
634
|
-
class AdminPortal::PostDefinition < ::PostDefinition
|
|
635
|
-
class ShowPage < ShowPage # Inherits from ::PostDefinition::ShowPage
|
|
636
|
-
def render_after_content
|
|
637
|
-
super
|
|
638
|
-
render AdminOnlySection.new(post: object)
|
|
639
|
-
end
|
|
640
|
-
end
|
|
641
|
-
end
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
## Related Skills
|
|
645
|
-
|
|
646
|
-
- `plutonium-forms` - Custom form templates and field builders
|
|
647
|
-
- `plutonium-assets` - TailwindCSS and component theming
|
|
648
|
-
- `plutonium-definition` - Field/input/display configuration
|
|
649
|
-
- `plutonium-definition` - Action buttons and interactions
|
|
650
|
-
- `plutonium-controller` - Presentation hooks (`present_parent?`, etc.)
|
|
651
|
-
- `plutonium-portal` - Portal-specific customization
|