Agent skill
plutonium-views
Customizing Plutonium views - pages, forms, displays, tables, and layouts using Phlex
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/plutonium-views
SKILL.md
Plutonium Views
Plutonium uses Phlex for all view components. This provides a Ruby-first approach to building HTML with full IDE support and type safety.
Architecture Overview
Definition
├── IndexPage → renders Table
├── ShowPage → renders Display
├── NewPage → renders Form
├── EditPage → renders Form
└── InteractiveActionPage → renders Form
Each definition has nested classes you can override:
class PostDefinition < ResourceDefinition
class IndexPage < IndexPage; end
class ShowPage < ShowPage; end
class NewPage < NewPage; end
class EditPage < EditPage; end
class Form < Form; end
class Table < Table; end
class Display < Display; end
end
Page Customization
Page Configuration
Set page titles and descriptions in definitions:
class PostDefinition < ResourceDefinition
# Static titles
index_page_title "Blog Posts"
index_page_description "Manage all published articles"
show_page_title "Article Details"
new_page_title "Write New Article"
edit_page_title "Edit Article"
# Control breadcrumbs
breadcrumbs true # Global default
index_page_breadcrumbs false # Per-page override
show_page_breadcrumbs true
end
Custom Page Class
Override page rendering by subclassing:
class PostDefinition < ResourceDefinition
class ShowPage < ShowPage
private
# Custom title logic
def page_title
"#{object.title} - #{object.author.name}"
end
# Add content before the main area
def render_before_content
div(class: "alert alert-info") {
"This post has #{object.comments.count} comments"
}
end
# Add content after
def render_after_content
render RelatedPostsComponent.new(post: object)
end
# Override the toolbar
def render_toolbar
div(class: "flex gap-2") {
button(class: "btn") { "Preview" }
button(class: "btn btn-primary") { "Publish" }
}
end
end
end
Page Hooks
All pages inherit these customization hooks:
| Hook | Purpose |
|---|---|
render_before_header |
Before entire header section |
render_after_header |
After entire header section |
render_before_breadcrumbs |
Before breadcrumbs |
render_after_breadcrumbs |
After breadcrumbs |
render_before_page_header |
Before title/actions |
render_after_page_header |
After title/actions |
render_before_toolbar |
Before toolbar |
render_after_toolbar |
After toolbar |
render_before_content |
Before main content |
render_after_content |
After main content |
render_before_footer |
Before footer |
render_after_footer |
After footer |
Custom View Files
For complete control, create custom ERB view files that replace the default entirely.
File locations:
# Main app (for a PostsController)
app/views/posts/index.html.erb
app/views/posts/show.html.erb
app/views/posts/new.html.erb
app/views/posts/edit.html.erb
# Portal-specific
packages/admin_portal/app/views/admin_portal/posts/show.html.erb
Default view structure:
The default views simply render the page class:
<%# app/views/resource/show.html.erb %>
<%= render current_definition.show_page_class.new %>
Custom view example:
<%# app/views/posts/show.html.erb %>
<div class="max-w-4xl mx-auto">
<article class="prose lg:prose-xl">
<h1><%= resource_record!.title %></h1>
<div class="meta text-gray-500">
By <%= resource_record!.author.name %> on <%= resource_record!.created_at.strftime("%B %d, %Y") %>
</div>
<%= raw resource_record!.content %>
</article>
<div class="mt-8">
<%= link_to "Edit", resource_url_for(resource_record!, action: :edit), class: "btn" %>
<%= link_to "Back", resource_url_for(Post), class: "btn" %>
</div>
</div>
Mixing approaches:
Render the default page with additions:
<%# app/views/posts/show.html.erb %>
<div class="announcement-banner">
Special announcement here
</div>
<%= render current_definition.show_page_class.new %>
<div class="related-posts">
<%= render partial: "related_posts", locals: { post: resource_record! } %>
</div>
Form Customization
Custom Form Template
Override how fields are rendered:
class PostDefinition < ResourceDefinition
class Form < Form
def form_template
# Custom layout with sections
div(class: "grid grid-cols-2 gap-6") {
div {
h3(class: "text-lg font-medium") { "Basic Info" }
render_resource_field :title
render_resource_field :slug
}
div {
h3(class: "text-lg font-medium") { "Content" }
render_resource_field :content
}
}
div(class: "mt-6") {
h3(class: "text-lg font-medium") { "Publishing" }
render_resource_field :published_at
render_resource_field :category
}
render_actions
end
end
end
Form Methods
| Method | Purpose |
|---|---|
render_fields |
Render all permitted fields |
render_resource_field(name) |
Render a single field |
render_actions |
Render submit buttons |
record |
The form object (alias: object) |
resource_fields |
List of permitted field names |
resource_definition |
The definition instance |
Display Customization
Custom Display Template
Override the show page detail rendering:
class PostDefinition < ResourceDefinition
class Display < Display
def display_template
# Hero section
div(class: "bg-gradient-to-r from-blue-500 to-purple-600 p-8 rounded-lg text-white mb-6") {
h1(class: "text-3xl font-bold") { object.title }
p(class: "mt-2 opacity-90") { object.excerpt }
}
# Main content
Block do
fields_wrapper do
render_resource_field :author
render_resource_field :published_at
render_resource_field :category
end
end
# Full-width content
Block do
div(class: "prose max-w-none") {
raw object.content
}
end
# Associations (tabs)
render_associations if present_associations?
end
end
end
Display Methods
| Method | Purpose |
|---|---|
render_fields |
Render all permitted fields in a block |
render_resource_field(name) |
Render single field |
render_associations |
Render association tabs |
object |
The record being displayed |
resource_fields |
List of permitted field names |
resource_associations |
List of permitted associations |
Table Customization
Custom Table Rendering
Override list page table:
class PostDefinition < ResourceDefinition
class Table < Table
def view_template
render_search_bar
render_scopes_bar
if collection.empty?
render_empty_card
else
# Custom card grid instead of table
div(class: "grid grid-cols-3 gap-4") {
collection.each do |post|
render PostCardComponent.new(post:)
end
}
end
render_footer
end
end
end
Table Methods
| Method | Purpose |
|---|---|
render_search_bar |
Search input |
render_scopes_bar |
Scope tabs |
render_table |
Default table |
render_empty_card |
Empty state |
render_footer |
Pagination |
collection |
The paginated records |
resource_fields |
Column field names |
Component Kit
Plutonium provides shorthand methods for common components:
class MyPage < Plutonium::UI::Page::Base
def render_content
# These are automatically rendered
PageHeader(title: "Dashboard")
Panel(class: "mt-4") {
p { "Content here" }
}
Block {
TabList(items: tabs)
}
EmptyCard("No items found")
ActionButton(action, url: "/posts/new")
end
end
Available kit methods:
Breadcrumbs()PageHeader(title:, description:, actions:)Panel(**attrs)Block(**attrs)TabList(items:)EmptyCard(message)ActionButton(action, url:)DynaFrameHost()/DynaFrameContent()TableSearchBar()TableScopesBar()TableInfo(pagy)TablePagination(pagy)
Custom Components
Creating a Phlex Component
# app/components/post_card_component.rb
class PostCardComponent < Plutonium::UI::Component::Base
def initialize(post:)
@post = post
end
def view_template
div(class: "bg-white rounded-lg shadow p-4") {
h3(class: "font-bold") { @post.title }
p(class: "text-gray-600 mt-2") { @post.excerpt }
div(class: "mt-4 flex justify-between items-center") {
span(class: "text-sm text-gray-500") { @post.published_at&.strftime("%B %d, %Y") }
a(href: resource_url_for(@post), class: "text-blue-600") { "Read more" }
}
}
end
end
Using in Definitions
Reference components in field definitions:
class PostDefinition < ResourceDefinition
# Custom display component
display :status, as: StatusBadgeComponent
# Custom input component
input :color, as: ColorPickerComponent
# Block with component
display :metrics do |field|
MetricsChartComponent.new(data: field.value)
end
end
Layout Customization
Eject Layout
Copy the layout template to your project:
rails generate pu:eject:layout
This copies layouts/resource.html.erb to your portal.
Custom Layout Class
Override the Phlex layout:
# packages/admin_portal/app/views/layouts/admin_portal/resource_layout.rb
module AdminPortal
class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
private
# Custom body classes
def body_attributes
{class: "antialiased bg-slate-100 dark:bg-slate-900"}
end
# Add custom header content
def render_before_main
super
render AnnouncementBanner.new if Announcement.active.any?
end
# Custom scripts
def render_body_scripts
super
script(src: "/custom-analytics.js")
end
end
end
Layout Hooks
| Hook | Purpose |
|---|---|
render_before_main |
Before main content area |
render_after_main |
After main (modals, etc.) |
render_before_content |
Inside main, before content |
render_after_content |
Inside main, after content |
render_flash |
Flash messages |
render_head |
HTML head section |
render_title |
Page title tag |
render_metatags |
Meta tags |
render_assets |
CSS/JS assets |
render_body_scripts |
Scripts at end of body |
Available Context
Both ERB views and Phlex components have access to the same context.
Resource Methods
| Method | Description |
|---|---|
resource_class |
The model class (e.g., Post) |
resource_record! |
Current record (raises if not found) |
resource_record? |
Current record (nil if not found) |
current_parent |
Parent record for nested routes |
current_scoped_entity |
Entity for multi-tenant portals |
Definition & Policy
| Method | Description |
|---|---|
current_definition |
Definition instance for current resource |
current_policy |
Policy instance for current record |
current_authorized_scope |
Scoped collection user can access |
Authentication
| Method | Description |
|---|---|
current_user |
Authenticated user (if using Rodauth) |
URL Helpers
| Method | Description |
|---|---|
resource_url_for(record) |
URL for a record |
resource_url_for(record, action: :edit) |
Action URL for record |
resource_url_for(Model) |
Index URL for model |
resource_url_for(Model, action: :new) |
New URL for model |
resource_url_for(record, parent: parent) |
Nested resource URL |
Display Helpers
| Method | Description |
|---|---|
display_name_of(record) |
Human-readable name for record |
resource_name(klass) |
Singular model name |
resource_name_plural(klass) |
Plural model name |
In Phlex Components
class MyComponent < Plutonium::UI::Component::Base
def view_template
# All the above methods work directly
current_user
resource_record!
resource_url_for(@post)
# Rails helpers via helpers proxy
helpers.link_to(...)
helpers.image_tag(...)
helpers.number_to_currency(...)
end
end
In ERB Views
<%# All methods available directly %>
<%= resource_record!.title %>
<%= current_user.name %>
<%= link_to "Edit", resource_url_for(resource_record!, action: :edit) %>
<%# Render Phlex components %>
<%= render current_definition.show_page_class.new %>
<%= render MyCustomComponent.new(post: resource_record!) %>
Portal-Specific Views
Each portal can have its own view overrides:
# Base definition
class PostDefinition < ResourceDefinition
class ShowPage < ShowPage
# Default behavior
end
end
# Admin portal override
class AdminPortal::PostDefinition < ::PostDefinition
class ShowPage < ShowPage # Inherits from ::PostDefinition::ShowPage
def render_after_content
super
render AdminOnlySection.new(post: object)
end
end
end
Related Skills
plutonium-forms- Custom form templates and field buildersplutonium-assets- TailwindCSS and component themingplutonium-definition-fields- Field/input/display configurationplutonium-definition-actions- Action buttons and interactionsplutonium-controller- Presentation hooks (present_parent?, etc.)plutonium-portal- Portal-specific customization
Didn't find tool you were looking for?