plutonium-ui
- Repo stars 60
- License MIT
- Author updated Live
- Author repo plutonium-core
- Domain
- Design
- Compatible agents
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- Trust score
- 94 / 100 · audit passed
- Author / version / license
- @radioactive-labs · MIT
- Token usage
- Moderate
- Setup complexity
- Guided setup
- External API key
- Not required
- Operating systems
- macOS · Linux · Windows
- Runtime requirements
- No special requirements
- Permissions
-
- Read-only
- Write / modify
- Shell exec
- Network behavior
- Local-only
- Install commands
- 26 variants
Profile is derived at build time from SKILL.md and install vectors. Subject to drift from author intent.
Heads up: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: plutonium-ui
description: Use BEFORE building or customizing any Plutonium UI — page classes, forms, displays, tables, cus…
category: design
runtime: no special runtime
---
# plutonium-ui output preview
## PART A: Task fit
- Use case: Use BEFORE building or customizing any Plutonium UI — page classes, forms, displays, tables, custom Phlex components, layouts, Stimulus controllers, Tailwind config, design tokens, themes, or component classes. Covers the full view + asset toolchain..
- Inputs: target material, constraints, expected output, and acceptance criteria.
- Evidence boundary: follow “🚨 Critical (read first) / Page titles, descriptions, breadcrumbs / Page hooks (preferred over viewtemplate)” and do not present inference as author intent.
## PART B: Execution result
- **01** The card summarizes the use case; runtime output centers on “Use BEFORE building or customizing any Plutonium UI — page classes, forms, displays, tables, custom Phlex components, layouts, Stimulus controllers, Tailwind config, design tokens, themes, or component classes. Covers the full view + asset toolchain.”.
- **02** When the source has headings, the agent prioritizes “🚨 Critical (read first) / Page titles, descriptions, breadcrumbs / Page hooks (preferred over viewtemplate)” so the result follows the author’s structure.
- **03** Typical output includes task judgment, concrete steps, required commands or file edits, validation, and follow-up options.
- **04** Risk context follows the fingerprint: read files, write/modify files, run shell commands; mostly runs locally; usually needs no extra API key.
## Running Rules
- read files, write/modify files, run shell commands; mostly runs locally; usually needs no extra API key.
- Validate with a small sample before expanding scope.
- Return the result, validation criteria, and next iteration options. The source does not require a stable slash command. After installation, invoke the skill by name and describe the task.
Name target files or source material, expected output, forbidden changes, and whether network or shell access is allowed. Permission fingerprint: read files, write/modify files, run shell commands.
Start with a small task and check whether the result follows “🚨 Critical (read first) / Page titles, descriptions, breadcrumbs / Page hooks (preferred over viewtemplate)”. Inspect diffs, logs, previews, or tests before expanding scope.
Confirm the final output includes a concrete result, evidence, and next action. If it stays generic, tighten inputs, boundaries, and acceptance criteria.
---
name: plutonium-ui
description: Use BEFORE building or customizing any Plutonium UI — page classes, forms, displays, tables, cus…
category: design
source: radioactive-labs/plutonium-core
---
# plutonium-ui
## When to use
- Use BEFORE building or customizing any Plutonium UI — page classes, forms, displays, tables, custom Phlex components…
- Use it when the task has clear inputs, repeatable steps, and validation criteria.
## What to provide
- Target material, scope, expected result, and forbidden changes.
- Whether network, commands, file writes, or external services are allowed.
## Execution rules
- Organize steps around “🚨 Critical (read first) / Page titles, descriptions, breadcrumbs / Page hooks (preferred over viewtemplate)” and keep inference separate from source facts.
- read files, write/modify files, run shell commands; mostly runs locally; usually needs no extra API key.
- Validate with a small sample before expanding the task.
## Output requirements
- Return the deliverable, key evidence, validation method, and next action.
- Mark missing information as unknown; do not invent commands, platforms, or dependencies. The author source anchors workflow facts; repository files anchor sources and commands; Fluxly only adds fit, limitations, and quality judgment.
skill "plutonium-ui" {
input -> user goal + target files + boundaries + acceptance criteria
context -> 🚨 Critical (read first) / Page titles, descriptions, breadcrumbs / Page hooks (preferred over viewtemplate)
rules -> SKILL.md triggers / order / output contract
runtime -> no special runtime | read files, write/modify files, run shell commands | mostly runs locally
guardrails -> usually needs no extra API key + small-sample validation + diff/log review
output -> copyable result + checklist + next iteration
} Plutonium UI — Pages, Forms, Components, Assets
Plutonium uses Phlex for all view components and TailwindCSS 4 + Stimulus for the frontend. This skill covers everything from overriding a single page to writing custom Phlex components, configuring Tailwind, and theming via design tokens.
For field-level rendering (field :foo, as: :markdown, display :status do |f| ... end), see [[plutonium-resource]] › Custom Rendering. For controller render-context hooks (present_parent?, submit_parent?), see [[plutonium-behavior]].
🚨 Critical (read first)
- Override via nested classes in the definition.
class ShowPage < ShowPage; end,class Form < Form; end. Don't replace the entire view layer. - Use render hooks, not
view_template.render_before_content,render_after_content,render_before_toolbar, etc. exist so you don't reimplement the whole page. - All pages inherit
DynaFrameContent— turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work". - Custom components inherit
Plutonium::UI::Component::Base— gives you the component kit (PageHeader,Panel,Block), resource helpers, and thehelpersproxy for Rails helpers. render_actionsis mandatory in customform_template— without it, the form has no submit button.- Always
registerControllers(application)inapp/javascript/controllers/index.js. Without it, Plutonium's Stimulus controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead. - Use
plutoniumTailwindConfig.mergewhen extending Tailwind theme — plain object merge drops Plutonium's defaults. - Prefer
.pu-*classes andvar(--pu-*)tokens over hardcodedgray-X/dark:gray-Ypairs — they switch with dark mode automatically. - Configure inputs in the definition; render them with
render_resource_fieldin the form. Don't reimplement field widgets from scratch.
Part 1 — Pages
Each definition has nested page classes. Override the ones you need to customize:
class PostDefinition < ResourceDefinition
class IndexPage < IndexPage; end
class ShowPage < ShowPage; end
class NewPage < NewPage; end
class EditPage < EditPage; end
class InteractiveActionPage < InteractiveActionPage; end
class Form < Form; end
class Table < Table; end
class Display < Display; end
end
Architecture:
Definition
├── IndexPage → renders Table
├── ShowPage → renders Display
├── NewPage → renders Form
├── EditPage → renders Form
└── InteractiveActionPage → renders Form
Page titles, descriptions, breadcrumbs
class PostDefinition < ResourceDefinition
index_page_title "Blog Posts"
index_page_description "Manage all published articles"
show_page_title "Article Details"
show_page_title -> { "#{current_record!.title} — Details" } # dynamic
breadcrumbs true # global default
index_page_breadcrumbs false # per-page override
end
Page hooks (preferred over view_template)
Every page inherits these:
| Hook | Position |
|---|---|
render_before_header / _after_header |
wraps the entire header section |
render_before_breadcrumbs / _after_breadcrumbs |
around the breadcrumb row |
render_before_page_header / _after_page_header |
around the title + actions block |
render_before_toolbar / _after_toolbar |
around the action toolbar |
render_before_content / _after_content |
around main content |
render_before_footer / _after_footer |
around footer/pagination |
Example:
class ShowPage < ShowPage
private
def page_title
"#{object.title} — #{object.author.name}"
end
def render_before_content
div(class: "alert alert-info") do
plain "This post has #{object.comments.count} comments"
end
end
def render_after_content
render RelatedPostsComponent.new(post: object)
end
def render_toolbar
div(class: "flex gap-2") do
button(class: "pu-btn pu-btn-md pu-btn-secondary") { "Preview" }
button(class: "pu-btn pu-btn-md pu-btn-primary") { "Publish" }
end
end
end
Custom ERB views (full replacement)
For total control, drop the page class entirely with an ERB view at the controller path:
app/views/posts/show.html.erb
packages/admin_portal/app/views/admin_portal/posts/show.html.erb
The default view simply renders the page class:
<%= render current_definition.show_page_class.new %>
Mix: keep the default and add chrome around it:
<div class="announcement-banner">Special announcement</div>
<%= render current_definition.show_page_class.new %>
<div class="related"><%= render partial: "related" %></div>
Detecting render context
| Helper | True when |
|---|---|
in_frame? |
Request targets a turbo-frame |
in_modal? |
Request renders inside a modal/slideover |
Use to pin action strips, omit nav chrome, or swap layouts.
Part 2 — Forms
Forms are built on Phlexi::Form. Hierarchy:
Phlexi::Form::Base
└── Plutonium::UI::Form::Base
├── Plutonium::UI::Form::Resource # CRUD
│ └── Plutonium::UI::Form::Interaction # action forms
└── Plutonium::UI::Form::Query # search/filter
Override the form
class PostDefinition < ResourceDefinition
class Form < Form
def form_template
render_fields # render every permitted field
render_actions # submit buttons — REQUIRED
end
end
end
Form methods
| Method | Purpose |
|---|---|
form_template |
Main override point |
render_fields |
All permitted fields in default layout |
render_resource_field(name) |
One field, using the definition's input config |
render_actions |
Submit + secondary buttons |
fields_wrapper { ... } |
Grid wrapper div (themeable) |
actions_wrapper { ... } |
Button wrapper div (themeable) |
object / record |
The form record |
resource_fields |
Array of permitted field names |
resource_definition |
The definition instance |
Custom layouts
Sectioned
class Form < Form
def form_template
section("Basic") do
render_resource_field :title
render_resource_field :slug
end
section("Publishing") do
render_resource_field :published_at
render_resource_field :category
end
render_actions
end
private
def section(title, &)
div(class: "mb-8") do
h3(class: "text-lg font-semibold mb-4 text-[var(--pu-text)]") { title }
fields_wrapper(&)
end
end
end
Two-column
def form_template
div(class: "grid grid-cols-1 lg:grid-cols-3 gap-6") do
div(class: "lg:col-span-2") do
fields_wrapper do
render_resource_field :title
render_resource_field :content
end
end
div(class: "space-y-4") do
Panel do
h4(class: "font-medium mb-2") { "Settings" }
render_resource_field :status
render_resource_field :visibility
end
end
end
render_actions
end
Field builder (field(:foo).input_tag)
render_resource_field uses the input config from the definition. For ad-hoc rendering, use field(...) directly:
render field(:title).wrapped { |f| f.input_tag } # wrapped: label + hint + errors
render field(:title).input_tag # bare element only
render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
Tag methods
| Tag | Input |
|---|---|
input_tag |
text (auto-detected type) |
string_tag, text_tag, number_tag, email_tag, password_tag, url_tag, tel_tag, hidden_tag |
standard HTML inputs |
checkbox_tag, select_tag, radio_button_tag |
standard |
Plutonium-enhanced tags
| Tag | Component |
|---|---|
easymde_tag / markdown_tag |
EasyMDE markdown editor |
slim_select_tag |
Slim Select |
flatpickr_tag |
Flatpickr date/time picker |
phone_tag / int_tel_input_tag |
intl-tel-input phone field |
uppy_tag / file_tag |
Uppy file upload |
secure_association_tag |
Association with policy-checked options |
belongs_to_tag / has_many_tag / has_one_tag |
Association selects |
key_value_store_tag |
Key/value pairs editor |
render field(:published_at).wrapped { |f| f.flatpickr_tag(min_date: Date.today, enable_time: true) }
render field(:avatar).wrapped { |f| f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes) }
Submit buttons
Default render_actions produces the primary submit, plus an optional "Save and add another" / "Update and continue editing" secondary button.
Control the secondary button via the definition:
class PostDefinition < ResourceDefinition
submit_and_continue false # nil (default — auto), true (always show), false (always hide)
end
Singular resources auto-hide it.
Custom action strip:
def render_actions
actions_wrapper do
a(href: resource_url_for(resource_class), class: "pu-btn pu-btn-md pu-btn-secondary") { "Cancel" }
button(type: :submit, name: "draft", value: "1", class: "pu-btn pu-btn-md") { "Save Draft" }
render submit_button
end
end
Pre-submit, nested inputs, interaction forms
These all live in the definition layer:
- Pre-submit / dynamic forms — see [[plutonium-resource]] › Dynamic Forms.
- Nested inputs (
nested_input :variants) — see [[plutonium-resource]] › Nested Inputs. - Interaction forms — interactions define their own
attribute/inputand inheritPlutonium::UI::Form::Interaction; see [[plutonium-behavior]] › Interactions.
Part 3 — Display & Table
Custom Display
class PostDefinition < ResourceDefinition
class Display < Display
def display_template
div(class: "bg-gradient-to-r from-primary-500 to-secondary-600 p-8 rounded-lg text-white mb-6") do
h1(class: "text-3xl font-bold") { object.title }
p(class: "mt-2 opacity-90") { object.excerpt }
end
Block do
fields_wrapper do
render_resource_field :author
render_resource_field :published_at
end
end
Block do
div(class: "prose max-w-none") { raw object.content }
end
render_associations if present_associations?
end
end
end
| Method | Purpose |
|---|---|
render_fields |
All permitted fields |
render_resource_field(name) |
One field |
render_associations |
Association tabs (driven by permitted_associations — see [[plutonium-behavior]]) |
object |
The record |
resource_fields, resource_associations |
Permitted lists |
Custom Table
class PostDefinition < ResourceDefinition
class Table < Table
def view_template
render_toolbar
render_scopes_pills
if collection.empty?
render_empty_card
else
# Replace the table with a card grid
div(class: "grid grid-cols-3 gap-4") do
collection.each { |post| render PostCardComponent.new(post:) }
end
end
render_footer
end
end
end
| Method | Purpose |
|---|---|
render_toolbar, render_scopes_pills, render_filter_pills, render_bulk_actions_toolbar |
Toolbar pieces |
render_table |
Default table |
render_empty_card |
Empty state |
render_footer |
Pagination |
collection |
Paginated records |
resource_fields |
Column field names |
Part 4 — Component Kit & Custom Components
Built-in shorthand kit
Inside any Plutonium::UI::Component::Base (or any page/form/display):
PageHeader(title: "Dashboard", description: "...", actions: [...])
Panel(class: "mt-4") { p { "Content" } }
Block { TabList(items: tabs) }
EmptyCard("No items found")
ActionButton(action, url: "/posts/new")
DynaFrameHost(src: "/some/path", loading: :lazy)
DynaFrameContent(content) { |frame| frame.render_content }
TableSearchBar()
TableScopesBar()
TableInfo(pagy)
TablePagination(pagy)
Breadcrumbs()
Custom Phlex components
class PostCardComponent < Plutonium::UI::Component::Base
def initialize(post:)
@post = post
end
def view_template
div(class: "bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)] p-4") do
h3(class: "font-bold text-[var(--pu-text)]") { @post.title }
p(class: "text-[var(--pu-text-muted)] mt-2") { @post.excerpt }
a(href: resource_url_for(@post), class: "text-primary-600") { "Read more" }
end
end
end
Use in a definition:
display :card, as: PostCardComponent # custom display component
input :color, as: ColorPickerComponent # custom input component
display :metrics do |field|
MetricsChartComponent.new(data: field.value)
end
DynaFrameContent pattern
Enables frame-aware rendering: regular requests get the full page (header + content + footer); turbo-frame requests get only the content inside the frame.
def view_template(&block)
DynaFrameContent(page_content(block)) do |frame|
render_header # skipped for frame requests
frame.render_content # always rendered
render_footer # skipped for frame requests
end
end
All pages inherit this. Modals and frame navigation work without special handling.
Part 5 — Modals, Slideovers, Tabs
Modal/slideover for :new / :edit + interactive actions
class PostDefinition < ResourceDefinition
modal :slideover # default — slide-in panel from the right
# modal :centered # centered dialog
# modal :centered, size: :lg # centered, wider container
# modal false # full standalone page
end
Drives both framework :new / :edit and every interactive action on the definition. size: accepts :sm, :md (default), :lg, :xl, :auto (hugs content), or :full. Per-action modal: / size: overrides win. See [[plutonium-resource]] › Action Options.
Tabs on the show page
Show pages with permitted_associations (see [[plutonium-behavior]]) render a tablist: Details tab first, then one tab per association. The active tab is reflected in the URL hash (#products, #refund-requests) so the page deep-links and the active state survives reload / back navigation. Tab rows scroll horizontally on narrow viewports — they don't wrap.
Part 6 — Layout (Chrome) & Eject
Shell
Plutonium.configure do |config|
config.shell = :modern # default — topbar + icon rail
# config.shell = :classic # legacy header + sidebar (only when upgrading)
end
Eject the chrome for per-portal customization
rails generate pu:eject:shell --dest=admin_portal
rails generate pu:eject:layout
These copy _resource_header.html.erb, _resource_sidebar.html.erb, and layouts/resource.html.erb into the portal so you can edit them directly.
Custom layout class (Phlex)
module AdminPortal
class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
private
def body_attributes = {class: "antialiased bg-[var(--pu-body)]"}
def render_before_main
super
render AnnouncementBanner.new if Announcement.active.any?
end
def render_body_scripts
super
script(src: "/custom-analytics.js")
end
end
end
| Hook | Position |
|---|---|
render_before_main / _after_main |
around the main content area |
render_before_content / _after_content |
inside main, around content |
render_flash |
flash messages |
render_head, render_title, render_metatags, render_assets |
head section |
render_body_scripts |
end-of-body scripts |
render_fonts |
font links |
Part 7 — Assets, Tailwind, Stimulus
Asset configuration
# config/initializers/plutonium.rb
Plutonium.configure do |config|
config.load_defaults 1.0
config.assets.stylesheet = "application"
config.assets.script = "application"
config.assets.logo = "my_logo.png"
config.assets.favicon = "my_favicon.ico"
end
Generator
rails generate pu:core:assets
This installs npm packages, creates tailwind.config.js extending Plutonium's config, imports Plutonium CSS, registers Stimulus controllers, and points the Plutonium config at your asset files.
Tailwind config (generated)
// tailwind.config.js
const { execSync } = require('child_process');
const plutoniumGemPath = execSync("bundle show plutonium").toString().trim();
const plutoniumTailwindConfig = require(`${plutoniumGemPath}/tailwind.options.js`);
module.exports = {
darkMode: plutoniumTailwindConfig.darkMode, // selector
plugins: [].concat(plutoniumTailwindConfig.plugins),
theme: plutoniumTailwindConfig.merge(
plutoniumTailwindConfig.theme,
{ /* your overrides */ },
),
content: [
`${__dirname}/app/**/*.{erb,haml,html,slim,rb}`,
`${__dirname}/app/javascript/**/*.js`,
`${__dirname}/packages/**/app/**/*.{erb,haml,html,slim,rb}`,
].concat(plutoniumTailwindConfig.content),
};
🚨 Always use plutoniumTailwindConfig.merge(...). A plain spread drops Plutonium's defaults.
Default color palette
| Color | Use |
|---|---|
primary |
Brand primary (turquoise default) |
secondary |
Brand secondary (navy default) |
success |
Success state (green) |
info |
Informational (blue) |
warning |
Warning (amber) |
danger |
Error (red) |
accent |
Highlight (coral pink) |
theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
extend: {
colors: {
primary: { 50: '#eff6ff', 500: '#3b82f6', 900: '#1e3a8a' },
},
},
})
CSS imports
/* app/assets/stylesheets/application.tailwind.css */
@import "gem:plutonium/src/css/plutonium.css";
@import "tailwindcss";
@config '../../../tailwind.config.js';
/* your styles */
Plutonium CSS includes core utilities, EasyMDE, Slim Select, intl-tel-input, Flatpickr.
Stimulus
// app/javascript/controllers/index.js
import { application } from "./application"
import { registerControllers } from "@radioactive-labs/plutonium"
registerControllers(application)
// Your custom controllers...
import CustomController from "./custom_controller"
application.register("custom", CustomController)
Bundled controllers: color-mode, form (pre-submit), nested-resource-form-fields, slim-select, flatpickr, easymde, plus various internal UI controllers.
Custom controller — standard Stimulus:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() { /* ... */ }
}
Typography
Default font: Lato. Override:
class MyLayout < Plutonium::UI::Layout::ResourceLayout
def render_fonts
link(rel: "preconnect", href: "https://fonts.googleapis.com")
link(href: "https://fonts.googleapis.com/css2?family=Inter&display=swap", rel: "stylesheet")
end
end
theme: { fontFamily: { body: ['Inter', 'sans-serif'], sans: ['Inter', 'sans-serif'] } }
Dark mode
selector strategy — toggle by adding/removing dark on <html>. The color-mode Stimulus controller handles it; Plutonium ships a switcher.
Part 8 — Design Tokens & .pu-* Component Classes
Plutonium uses CSS custom properties for surfaces, text, borders, forms, cards, shadows, radii, spacing, and transitions. Tokens auto-switch with dark mode. Source: src/css/tokens.css.
Key tokens
| Token | Purpose |
|---|---|
--pu-body, --pu-surface, --pu-surface-alt, --pu-surface-raised, --pu-surface-overlay |
Backgrounds |
--pu-text, --pu-text-muted, --pu-text-subtle |
Text colors |
--pu-border, --pu-border-muted, --pu-border-strong |
Borders |
--pu-input-bg, --pu-input-border, --pu-input-focus-ring, --pu-input-placeholder |
Form inputs |
--pu-card-bg, --pu-card-border |
Cards |
--pu-shadow-sm/md/lg |
Shadows |
--pu-radius-sm/md/lg/xl/full |
Border radius |
--pu-space-xs/sm/md/lg/xl |
Spacing |
--pu-transition-fast/normal/slow |
Transitions |
🚨 Tokens are CSS variables — use bg-[var(--pu-surface)], not bg-pu-surface.
Customizing tokens
:root {
--pu-surface: #fafafa;
--pu-border: #d1d5db;
}
.dark {
--pu-surface: #111827;
--pu-border: #374151;
}
.pu-* component classes
Ready-to-use styled components in src/css/components.css. Prefer these over hardcoded gray-X/dark:gray-Y pairs.
Buttons
.pu-btn (base)
.pu-btn-md / -sm / -xs (size)
.pu-btn-primary / -secondary / -danger / -success / -warning / -info / -accent
.pu-btn-ghost / -outline
.pu-btn-soft-primary / -soft-danger / ...
<%= form.submit "Save", class: "pu-btn pu-btn-md pu-btn-primary" %>
Inputs, cards, panels, tables, toolbars, empty states
.pu-input / -invalid / -valid .pu-label / -required .pu-hint / .pu-error .pu-checkbox
.pu-card / .pu-card-body
.pu-panel-header / -title / -description
.pu-table-wrapper / .pu-table / -header / -header-cell / -body-row / -body-row-selected / -body-cell / .pu-selection-cell
.pu-toolbar / -text / -actions
.pu-empty-state / -icon / -title / -description
Ruby constants
ComponentClasses::Button.classes(variant: :primary, size: :default, soft: false)
# => "pu-btn pu-btn-md pu-btn-primary"
ComponentClasses::Form::INPUT # "pu-input"
ComponentClasses::Form::LABEL # "pu-label"
ComponentClasses::Table::WRAPPER # "pu-table-wrapper"
ComponentClasses::Card::BASE # "pu-card"
Migration from hardcoded classes
| Old | New |
|---|---|
text-gray-900 dark:text-white |
text-[var(--pu-text)] |
text-gray-500 dark:text-gray-400 |
text-[var(--pu-text-muted)] |
bg-gray-50 dark:bg-gray-700 |
bg-[var(--pu-surface)] |
border-gray-300 dark:border-gray-600 |
border-[var(--pu-border)] |
| Long input class chain | pu-input |
| Long button class chain | pu-btn pu-btn-md pu-btn-primary |
tokens and classes helpers
For conditional class composition in Phlex components:
class MyComponent < Plutonium::UI::Component::Base
def initialize(active:) = @active = active
def view_template
div(class: tokens(
"base-class",
active?: "bg-primary-500 text-white",
inactive?: "bg-gray-200 text-gray-700"
)) { "Content" }
end
private
def active? = @active
def inactive? = !@active
end
# `classes` returns the class as a kwarg-friendly hash
div(**classes("p-4 rounded", active?: "ring-2"))
# => <div class="p-4 rounded ring-2">
# Then/else branches
tokens("base", condition?: {then: "if-true", else: "if-false"})
Part 9 — Phlexi Component Themes
Themes are Ruby classes nested under a Form/Display/Table override. They merge into Plutonium's defaults — never replace wholesale, always super.merge(...).
Form theme
class PostDefinition < ResourceDefinition
class Form < Form
class Theme < Plutonium::UI::Form::Theme
def self.theme
super.merge(
base: "bg-[var(--pu-card-bg)] shadow-md rounded-lg p-6",
fields_wrapper: "grid grid-cols-2 gap-6",
actions_wrapper: "flex justify-end mt-6 space-x-2",
label: "block mb-2 text-base font-bold",
input: "pu-input",
error: "pu-error",
button: "pu-btn pu-btn-md pu-btn-primary"
)
end
end
end
end
Form theme keys
base, fields_wrapper, actions_wrapper, wrapper, inner_wrapper, label, invalid_label, valid_label, neutral_label, input, invalid_input, valid_input, neutral_input, hint, error, button, checkbox, select.
Display theme
class Display < Display
class Theme < Plutonium::UI::Display::Theme
def self.theme
super.merge(
fields_wrapper: "grid grid-cols-3 gap-8",
label: "text-sm font-bold text-[var(--pu-text-muted)] mb-1",
string: "text-lg text-[var(--pu-text)]",
markdown: "prose dark:prose-invert max-w-none"
)
end
end
end
Display theme keys
fields_wrapper, label, description, string, text, link, email, phone, markdown, json.
Table theme
class Table < Table
class Theme < Plutonium::UI::Table::Theme
def self.theme
super.merge(
wrapper: "pu-table-wrapper",
base: "pu-table",
header: "pu-table-header",
header_cell: "pu-table-header-cell",
body_row: "pu-table-body-row",
body_cell: "pu-table-body-cell"
)
end
end
end
Table theme keys
wrapper, base, header, header_cell, body_row, body_cell, sort_icon.
Available context
Inside any page / form / display / Phlex component, the same set of helpers is available — model accessors, definition/policy methods, URL helpers, current_user. For the full list, see [[plutonium-behavior]] › Key methods (controllers expose the same surface; pages inherit it).
In Phlex components, Rails helpers are accessed via the helpers proxy:
class MyComponent < Plutonium::UI::Component::Base
def view_template
helpers.link_to(...)
helpers.number_to_currency(...)
end
end
Portal-specific overrides
Each portal can override page classes independently. The portal definition inherits from the base definition, and its nested classes inherit from the base's nested classes:
class AdminPortal::PostDefinition < ::PostDefinition
class ShowPage < ShowPage # inherits from ::PostDefinition::ShowPage
def render_after_content
super
render AdminOnlySection.new(post: object)
end
end
end
Gotchas
- Don't override
view_templatein pages when a render hook fits — you lose breadcrumbs / header / DynaFrame behavior. - Always register Stimulus controllers. Without
registerControllers(application)the entire UI's interactive layer is dead. - Use
plutoniumTailwindConfig.merge— plain object merge drops Plutonium's defaults. - Dark mode is
selector, notclass. Toggle viadocument.documentElement.classList.toggle('dark'). - Tokens are CSS variables, not Tailwind keys —
bg-[var(--pu-surface)], notbg-pu-surface. render_actionsis mandatory in customform_template— otherwise no submit button.
Related skills
- [[plutonium-resource]] — field/input/display config (
as:,condition:, blocks); modal options for actions. - [[plutonium-behavior]] — controller presentation hooks (
present_parent?), available helpers (resource_record!,current_scoped_entity). - [[plutonium-app]] —
pu:eject:layout,pu:eject:shell, portal package overrides. - [[plutonium-tenancy]] —
permitted_associationsdrives the show-page tablist.
Decide Fit First
Design Intent
How To Use It
Boundaries And Review