Agent skill

plutonium-views

Customizing Plutonium views - pages, forms, displays, tables, and layouts using Phlex

Stars 163
Forks 31

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:

ruby
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:

ruby
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:

ruby
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:

erb
<%# app/views/resource/show.html.erb %>
<%= render current_definition.show_page_class.new %>

Custom view example:

erb
<%# 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:

erb
<%# 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:

ruby
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:

ruby
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:

ruby
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:

ruby
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

ruby
# 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:

ruby
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:

bash
rails generate pu:eject:layout

This copies layouts/resource.html.erb to your portal.

Custom Layout Class

Override the Phlex layout:

ruby
# 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

ruby
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

erb
<%# 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:

ruby
# 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 builders
  • plutonium-assets - TailwindCSS and component theming
  • plutonium-definition-fields - Field/input/display configuration
  • plutonium-definition-actions - Action buttons and interactions
  • plutonium-controller - Presentation hooks (present_parent?, etc.)
  • plutonium-portal - Portal-specific customization

Didn't find tool you were looking for?

Be as detailed as possible for better results