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
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plutonium-ui
|
|
3
|
+
description: 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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium UI — Pages, Forms, Components, Assets
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
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]].
|
|
11
|
+
|
|
12
|
+
## 🚨 Critical (read first)
|
|
13
|
+
|
|
14
|
+
- **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end`. Don't replace the entire view layer.
|
|
15
|
+
- **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.
|
|
16
|
+
- **All pages inherit `DynaFrameContent`** — turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work".
|
|
17
|
+
- **Custom components inherit `Plutonium::UI::Component::Base`** — gives you the component kit (`PageHeader`, `Panel`, `Block`), resource helpers, and the `helpers` proxy for Rails helpers.
|
|
18
|
+
- **`render_actions` is mandatory in custom `form_template`** — without it, the form has no submit button.
|
|
19
|
+
- **Always `registerControllers(application)`** in `app/javascript/controllers/index.js`. Without it, Plutonium's Stimulus controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead.
|
|
20
|
+
- **Use `plutoniumTailwindConfig.merge`** when extending Tailwind theme — plain object merge drops Plutonium's defaults.
|
|
21
|
+
- **Prefer `.pu-*` classes and `var(--pu-*)` tokens** over hardcoded `gray-X/dark:gray-Y` pairs — they switch with dark mode automatically.
|
|
22
|
+
- **Configure inputs in the definition; render them with `render_resource_field` in the form.** Don't reimplement field widgets from scratch.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Part 1 — Pages
|
|
27
|
+
|
|
28
|
+
Each definition has nested page classes. Override the ones you need to customize:
|
|
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 InteractiveActionPage < InteractiveActionPage; end
|
|
37
|
+
class Form < Form; end
|
|
38
|
+
class Table < Table; end
|
|
39
|
+
class Display < Display; end
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Architecture:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Definition
|
|
47
|
+
├── IndexPage → renders Table
|
|
48
|
+
├── ShowPage → renders Display
|
|
49
|
+
├── NewPage → renders Form
|
|
50
|
+
├── EditPage → renders Form
|
|
51
|
+
└── InteractiveActionPage → renders Form
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Page titles, descriptions, breadcrumbs
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class PostDefinition < ResourceDefinition
|
|
58
|
+
index_page_title "Blog Posts"
|
|
59
|
+
index_page_description "Manage all published articles"
|
|
60
|
+
show_page_title "Article Details"
|
|
61
|
+
show_page_title -> { "#{current_record!.title} — Details" } # dynamic
|
|
62
|
+
|
|
63
|
+
breadcrumbs true # global default
|
|
64
|
+
index_page_breadcrumbs false # per-page override
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Page hooks (preferred over `view_template`)
|
|
69
|
+
|
|
70
|
+
Every page inherits these:
|
|
71
|
+
|
|
72
|
+
| Hook | Position |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `render_before_header` / `_after_header` | wraps the entire header section |
|
|
75
|
+
| `render_before_breadcrumbs` / `_after_breadcrumbs` | around the breadcrumb row |
|
|
76
|
+
| `render_before_page_header` / `_after_page_header` | around the title + actions block |
|
|
77
|
+
| `render_before_toolbar` / `_after_toolbar` | around the action toolbar |
|
|
78
|
+
| `render_before_content` / `_after_content` | around main content |
|
|
79
|
+
| `render_before_footer` / `_after_footer` | around footer/pagination |
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class ShowPage < ShowPage
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def page_title
|
|
88
|
+
"#{object.title} — #{object.author.name}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_before_content
|
|
92
|
+
div(class: "alert alert-info") do
|
|
93
|
+
plain "This post has #{object.comments.count} comments"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render_after_content
|
|
98
|
+
render RelatedPostsComponent.new(post: object)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_toolbar
|
|
102
|
+
div(class: "flex gap-2") do
|
|
103
|
+
button(class: "pu-btn pu-btn-md pu-btn-secondary") { "Preview" }
|
|
104
|
+
button(class: "pu-btn pu-btn-md pu-btn-primary") { "Publish" }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Custom ERB views (full replacement)
|
|
111
|
+
|
|
112
|
+
For total control, drop the page class entirely with an ERB view at the controller path:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
app/views/posts/show.html.erb
|
|
116
|
+
packages/admin_portal/app/views/admin_portal/posts/show.html.erb
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The default view simply renders the page class:
|
|
120
|
+
|
|
121
|
+
```erb
|
|
122
|
+
<%= render current_definition.show_page_class.new %>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Mix: keep the default and add chrome around it:
|
|
126
|
+
|
|
127
|
+
```erb
|
|
128
|
+
<div class="announcement-banner">Special announcement</div>
|
|
129
|
+
<%= render current_definition.show_page_class.new %>
|
|
130
|
+
<div class="related"><%= render partial: "related" %></div>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Detecting render context
|
|
134
|
+
|
|
135
|
+
| Helper | True when |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `in_frame?` | Request targets a turbo-frame |
|
|
138
|
+
| `in_modal?` | Request renders inside a modal/slideover |
|
|
139
|
+
|
|
140
|
+
Use to pin action strips, omit nav chrome, or swap layouts.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
# Part 2 — Forms
|
|
145
|
+
|
|
146
|
+
Forms are built on [Phlexi::Form](https://github.com/radioactive-labs/phlexi-form). Hierarchy:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Phlexi::Form::Base
|
|
150
|
+
└── Plutonium::UI::Form::Base
|
|
151
|
+
├── Plutonium::UI::Form::Resource # CRUD
|
|
152
|
+
│ └── Plutonium::UI::Form::Interaction # action forms
|
|
153
|
+
└── Plutonium::UI::Form::Query # search/filter
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Override the form
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class PostDefinition < ResourceDefinition
|
|
160
|
+
class Form < Form
|
|
161
|
+
def form_template
|
|
162
|
+
render_fields # render every permitted field
|
|
163
|
+
render_actions # submit buttons — REQUIRED
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Form methods
|
|
170
|
+
|
|
171
|
+
| Method | Purpose |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `form_template` | Main override point |
|
|
174
|
+
| `render_fields` | All permitted fields in default layout |
|
|
175
|
+
| `render_resource_field(name)` | One field, using the definition's `input` config |
|
|
176
|
+
| `render_actions` | Submit + secondary buttons |
|
|
177
|
+
| `fields_wrapper { ... }` | Grid wrapper div (themeable) |
|
|
178
|
+
| `actions_wrapper { ... }` | Button wrapper div (themeable) |
|
|
179
|
+
| `object` / `record` | The form record |
|
|
180
|
+
| `resource_fields` | Array of permitted field names |
|
|
181
|
+
| `resource_definition` | The definition instance |
|
|
182
|
+
|
|
183
|
+
## Custom layouts
|
|
184
|
+
|
|
185
|
+
### Sectioned
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
class Form < Form
|
|
189
|
+
def form_template
|
|
190
|
+
section("Basic") do
|
|
191
|
+
render_resource_field :title
|
|
192
|
+
render_resource_field :slug
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
section("Publishing") do
|
|
196
|
+
render_resource_field :published_at
|
|
197
|
+
render_resource_field :category
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
render_actions
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def section(title, &)
|
|
206
|
+
div(class: "mb-8") do
|
|
207
|
+
h3(class: "text-lg font-semibold mb-4 text-[var(--pu-text)]") { title }
|
|
208
|
+
fields_wrapper(&)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Two-column
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
def form_template
|
|
218
|
+
div(class: "grid grid-cols-1 lg:grid-cols-3 gap-6") do
|
|
219
|
+
div(class: "lg:col-span-2") do
|
|
220
|
+
fields_wrapper do
|
|
221
|
+
render_resource_field :title
|
|
222
|
+
render_resource_field :content
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
div(class: "space-y-4") do
|
|
227
|
+
Panel do
|
|
228
|
+
h4(class: "font-medium mb-2") { "Settings" }
|
|
229
|
+
render_resource_field :status
|
|
230
|
+
render_resource_field :visibility
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
render_actions
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Field builder (`field(:foo).input_tag`)
|
|
239
|
+
|
|
240
|
+
`render_resource_field` uses the input config from the definition. For ad-hoc rendering, use `field(...)` directly:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
render field(:title).wrapped { |f| f.input_tag } # wrapped: label + hint + errors
|
|
244
|
+
render field(:title).input_tag # bare element only
|
|
245
|
+
render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Tag methods
|
|
249
|
+
|
|
250
|
+
| Tag | Input |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `input_tag` | text (auto-detected type) |
|
|
253
|
+
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
|
|
254
|
+
| `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
|
|
255
|
+
|
|
256
|
+
### Plutonium-enhanced tags
|
|
257
|
+
|
|
258
|
+
| Tag | Component |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `easymde_tag` / `markdown_tag` | EasyMDE markdown editor |
|
|
261
|
+
| `slim_select_tag` | Slim Select |
|
|
262
|
+
| `flatpickr_tag` | Flatpickr date/time picker |
|
|
263
|
+
| `phone_tag` / `int_tel_input_tag` | intl-tel-input phone field |
|
|
264
|
+
| `uppy_tag` / `file_tag` | Uppy file upload |
|
|
265
|
+
| `secure_association_tag` | Association with policy-checked options |
|
|
266
|
+
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
267
|
+
| `key_value_store_tag` | Key/value pairs editor |
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
render field(:published_at).wrapped { |f| f.flatpickr_tag(min_date: Date.today, enable_time: true) }
|
|
271
|
+
render field(:avatar).wrapped { |f| f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes) }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Submit buttons
|
|
275
|
+
|
|
276
|
+
Default `render_actions` produces the primary submit, plus an optional "Save and add another" / "Update and continue editing" secondary button.
|
|
277
|
+
|
|
278
|
+
Control the secondary button via the definition:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class PostDefinition < ResourceDefinition
|
|
282
|
+
submit_and_continue false # nil (default — auto), true (always show), false (always hide)
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Singular resources auto-hide it.
|
|
287
|
+
|
|
288
|
+
Custom action strip:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
def render_actions
|
|
292
|
+
actions_wrapper do
|
|
293
|
+
a(href: resource_url_for(resource_class), class: "pu-btn pu-btn-md pu-btn-secondary") { "Cancel" }
|
|
294
|
+
button(type: :submit, name: "draft", value: "1", class: "pu-btn pu-btn-md") { "Save Draft" }
|
|
295
|
+
render submit_button
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Pre-submit, nested inputs, interaction forms
|
|
301
|
+
|
|
302
|
+
These all live in the definition layer:
|
|
303
|
+
|
|
304
|
+
- **Pre-submit / dynamic forms** — see [[plutonium-resource]] › Dynamic Forms.
|
|
305
|
+
- **Nested inputs** (`nested_input :variants`) — see [[plutonium-resource]] › Nested Inputs.
|
|
306
|
+
- **Interaction forms** — interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [[plutonium-behavior]] › Interactions.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
# Part 3 — Display & Table
|
|
311
|
+
|
|
312
|
+
## Custom Display
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class PostDefinition < ResourceDefinition
|
|
316
|
+
class Display < Display
|
|
317
|
+
def display_template
|
|
318
|
+
div(class: "bg-gradient-to-r from-primary-500 to-secondary-600 p-8 rounded-lg text-white mb-6") do
|
|
319
|
+
h1(class: "text-3xl font-bold") { object.title }
|
|
320
|
+
p(class: "mt-2 opacity-90") { object.excerpt }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
Block do
|
|
324
|
+
fields_wrapper do
|
|
325
|
+
render_resource_field :author
|
|
326
|
+
render_resource_field :published_at
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
Block do
|
|
331
|
+
div(class: "prose max-w-none") { raw object.content }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
render_associations if present_associations?
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
| Method | Purpose |
|
|
341
|
+
|---|---|
|
|
342
|
+
| `render_fields` | All permitted fields |
|
|
343
|
+
| `render_resource_field(name)` | One field |
|
|
344
|
+
| `render_associations` | Association tabs (driven by `permitted_associations` — see [[plutonium-behavior]]) |
|
|
345
|
+
| `object` | The record |
|
|
346
|
+
| `resource_fields`, `resource_associations` | Permitted lists |
|
|
347
|
+
|
|
348
|
+
## Custom Table
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
class PostDefinition < ResourceDefinition
|
|
352
|
+
class Table < Table
|
|
353
|
+
def view_template
|
|
354
|
+
render_search_bar
|
|
355
|
+
render_scopes_bar
|
|
356
|
+
|
|
357
|
+
if collection.empty?
|
|
358
|
+
render_empty_card
|
|
359
|
+
else
|
|
360
|
+
# Replace the table with a card grid
|
|
361
|
+
div(class: "grid grid-cols-3 gap-4") do
|
|
362
|
+
collection.each { |post| render PostCardComponent.new(post:) }
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
render_footer
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
| Method | Purpose |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `render_search_bar`, `render_scopes_bar` | Toolbar pieces |
|
|
375
|
+
| `render_table` | Default table |
|
|
376
|
+
| `render_empty_card` | Empty state |
|
|
377
|
+
| `render_footer` | Pagination |
|
|
378
|
+
| `collection` | Paginated records |
|
|
379
|
+
| `resource_fields` | Column field names |
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
# Part 4 — Component Kit & Custom Components
|
|
384
|
+
|
|
385
|
+
## Built-in shorthand kit
|
|
386
|
+
|
|
387
|
+
Inside any `Plutonium::UI::Component::Base` (or any page/form/display):
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
PageHeader(title: "Dashboard", description: "...", actions: [...])
|
|
391
|
+
Panel(class: "mt-4") { p { "Content" } }
|
|
392
|
+
Block { TabList(items: tabs) }
|
|
393
|
+
EmptyCard("No items found")
|
|
394
|
+
ActionButton(action, url: "/posts/new")
|
|
395
|
+
DynaFrameHost(src: "/some/path", loading: :lazy)
|
|
396
|
+
DynaFrameContent(content) { |frame| frame.render_content }
|
|
397
|
+
TableSearchBar()
|
|
398
|
+
TableScopesBar()
|
|
399
|
+
TableInfo(pagy)
|
|
400
|
+
TablePagination(pagy)
|
|
401
|
+
Breadcrumbs()
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Custom Phlex components
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
class PostCardComponent < Plutonium::UI::Component::Base
|
|
408
|
+
def initialize(post:)
|
|
409
|
+
@post = post
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def view_template
|
|
413
|
+
div(class: "bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)] p-4") do
|
|
414
|
+
h3(class: "font-bold text-[var(--pu-text)]") { @post.title }
|
|
415
|
+
p(class: "text-[var(--pu-text-muted)] mt-2") { @post.excerpt }
|
|
416
|
+
a(href: resource_url_for(@post), class: "text-primary-600") { "Read more" }
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Use in a definition:
|
|
423
|
+
|
|
424
|
+
```ruby
|
|
425
|
+
display :card, as: PostCardComponent # custom display component
|
|
426
|
+
input :color, as: ColorPickerComponent # custom input component
|
|
427
|
+
|
|
428
|
+
display :metrics do |field|
|
|
429
|
+
MetricsChartComponent.new(data: field.value)
|
|
430
|
+
end
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## `DynaFrameContent` pattern
|
|
434
|
+
|
|
435
|
+
Enables frame-aware rendering: regular requests get the full page (header + content + footer); turbo-frame requests get only the content inside the frame.
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
def view_template(&block)
|
|
439
|
+
DynaFrameContent(page_content(block)) do |frame|
|
|
440
|
+
render_header # skipped for frame requests
|
|
441
|
+
frame.render_content # always rendered
|
|
442
|
+
render_footer # skipped for frame requests
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
All pages inherit this. Modals and frame navigation work without special handling.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
# Part 5 — Modals, Slideovers, Tabs
|
|
452
|
+
|
|
453
|
+
## Modal/slideover for `:new` / `:edit`
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
class PostDefinition < ResourceDefinition
|
|
457
|
+
modal :slideover # default — slide-in panel from the right
|
|
458
|
+
# modal :centered # centered dialog
|
|
459
|
+
# modal false # full standalone page
|
|
460
|
+
end
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Custom interactive actions render in their own dialog with their own per-action `modal:` option (`:centered` default, or `:slideover`). See [[plutonium-resource]] › Action Options.
|
|
464
|
+
|
|
465
|
+
## Tabs on the show page
|
|
466
|
+
|
|
467
|
+
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.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
# Part 6 — Layout (Chrome) & Eject
|
|
472
|
+
|
|
473
|
+
## Shell
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
Plutonium.configure do |config|
|
|
477
|
+
config.shell = :modern # default — topbar + icon rail
|
|
478
|
+
# config.shell = :classic # legacy header + sidebar (only when upgrading)
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Eject the chrome for per-portal customization
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
rails generate pu:eject:shell --dest=admin_portal
|
|
486
|
+
rails generate pu:eject:layout
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
These copy `_resource_header.html.erb`, `_resource_sidebar.html.erb`, and `layouts/resource.html.erb` into the portal so you can edit them directly.
|
|
490
|
+
|
|
491
|
+
## Custom layout class (Phlex)
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
module AdminPortal
|
|
495
|
+
class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
|
|
496
|
+
private
|
|
497
|
+
|
|
498
|
+
def body_attributes = {class: "antialiased bg-[var(--pu-body)]"}
|
|
499
|
+
|
|
500
|
+
def render_before_main
|
|
501
|
+
super
|
|
502
|
+
render AnnouncementBanner.new if Announcement.active.any?
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def render_body_scripts
|
|
506
|
+
super
|
|
507
|
+
script(src: "/custom-analytics.js")
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
| Hook | Position |
|
|
514
|
+
|---|---|
|
|
515
|
+
| `render_before_main` / `_after_main` | around the main content area |
|
|
516
|
+
| `render_before_content` / `_after_content` | inside main, around content |
|
|
517
|
+
| `render_flash` | flash messages |
|
|
518
|
+
| `render_head`, `render_title`, `render_metatags`, `render_assets` | head section |
|
|
519
|
+
| `render_body_scripts` | end-of-body scripts |
|
|
520
|
+
| `render_fonts` | font links |
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
# Part 7 — Assets, Tailwind, Stimulus
|
|
525
|
+
|
|
526
|
+
## Asset configuration
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
# config/initializers/plutonium.rb
|
|
530
|
+
Plutonium.configure do |config|
|
|
531
|
+
config.load_defaults 1.0
|
|
532
|
+
config.assets.stylesheet = "application"
|
|
533
|
+
config.assets.script = "application"
|
|
534
|
+
config.assets.logo = "my_logo.png"
|
|
535
|
+
config.assets.favicon = "my_favicon.ico"
|
|
536
|
+
end
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Generator
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
rails generate pu:core:assets
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
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.
|
|
546
|
+
|
|
547
|
+
## Tailwind config (generated)
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
// tailwind.config.js
|
|
551
|
+
const { execSync } = require('child_process');
|
|
552
|
+
const plutoniumGemPath = execSync("bundle show plutonium").toString().trim();
|
|
553
|
+
const plutoniumTailwindConfig = require(`${plutoniumGemPath}/tailwind.options.js`);
|
|
554
|
+
|
|
555
|
+
module.exports = {
|
|
556
|
+
darkMode: plutoniumTailwindConfig.darkMode, // selector
|
|
557
|
+
plugins: [].concat(plutoniumTailwindConfig.plugins),
|
|
558
|
+
theme: plutoniumTailwindConfig.merge(
|
|
559
|
+
plutoniumTailwindConfig.theme,
|
|
560
|
+
{ /* your overrides */ },
|
|
561
|
+
),
|
|
562
|
+
content: [
|
|
563
|
+
`${__dirname}/app/**/*.{erb,haml,html,slim,rb}`,
|
|
564
|
+
`${__dirname}/app/javascript/**/*.js`,
|
|
565
|
+
`${__dirname}/packages/**/app/**/*.{erb,haml,html,slim,rb}`,
|
|
566
|
+
].concat(plutoniumTailwindConfig.content),
|
|
567
|
+
};
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
🚨 Always use `plutoniumTailwindConfig.merge(...)`. A plain spread drops Plutonium's defaults.
|
|
571
|
+
|
|
572
|
+
## Default color palette
|
|
573
|
+
|
|
574
|
+
| Color | Use |
|
|
575
|
+
|---|---|
|
|
576
|
+
| `primary` | Brand primary (turquoise default) |
|
|
577
|
+
| `secondary` | Brand secondary (navy default) |
|
|
578
|
+
| `success` | Success state (green) |
|
|
579
|
+
| `info` | Informational (blue) |
|
|
580
|
+
| `warning` | Warning (amber) |
|
|
581
|
+
| `danger` | Error (red) |
|
|
582
|
+
| `accent` | Highlight (coral pink) |
|
|
583
|
+
|
|
584
|
+
```javascript
|
|
585
|
+
theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
586
|
+
extend: {
|
|
587
|
+
colors: {
|
|
588
|
+
primary: { 50: '#eff6ff', 500: '#3b82f6', 900: '#1e3a8a' },
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## CSS imports
|
|
595
|
+
|
|
596
|
+
```css
|
|
597
|
+
/* app/assets/stylesheets/application.tailwind.css */
|
|
598
|
+
@import "gem:plutonium/src/css/plutonium.css";
|
|
599
|
+
|
|
600
|
+
@import "tailwindcss";
|
|
601
|
+
@config '../../../tailwind.config.js';
|
|
602
|
+
|
|
603
|
+
/* your styles */
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Plutonium CSS includes core utilities, EasyMDE, Slim Select, intl-tel-input, Flatpickr.
|
|
607
|
+
|
|
608
|
+
## Stimulus
|
|
609
|
+
|
|
610
|
+
```javascript
|
|
611
|
+
// app/javascript/controllers/index.js
|
|
612
|
+
import { application } from "./application"
|
|
613
|
+
import { registerControllers } from "@radioactive-labs/plutonium"
|
|
614
|
+
|
|
615
|
+
registerControllers(application)
|
|
616
|
+
|
|
617
|
+
// Your custom controllers...
|
|
618
|
+
import CustomController from "./custom_controller"
|
|
619
|
+
application.register("custom", CustomController)
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Bundled controllers: `color-mode`, `form` (pre-submit), `nested-resource-form-fields`, `slim-select`, `flatpickr`, `easymde`, plus various internal UI controllers.
|
|
623
|
+
|
|
624
|
+
Custom controller — standard Stimulus:
|
|
625
|
+
|
|
626
|
+
```javascript
|
|
627
|
+
import { Controller } from "@hotwired/stimulus"
|
|
628
|
+
export default class extends Controller {
|
|
629
|
+
connect() { /* ... */ }
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## Typography
|
|
634
|
+
|
|
635
|
+
Default font: Lato. Override:
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
class MyLayout < Plutonium::UI::Layout::ResourceLayout
|
|
639
|
+
def render_fonts
|
|
640
|
+
link(rel: "preconnect", href: "https://fonts.googleapis.com")
|
|
641
|
+
link(href: "https://fonts.googleapis.com/css2?family=Inter&display=swap", rel: "stylesheet")
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
```javascript
|
|
647
|
+
theme: { fontFamily: { body: ['Inter', 'sans-serif'], sans: ['Inter', 'sans-serif'] } }
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Dark mode
|
|
651
|
+
|
|
652
|
+
`selector` strategy — toggle by adding/removing `dark` on `<html>`. The `color-mode` Stimulus controller handles it; Plutonium ships a switcher.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
# Part 8 — Design Tokens & `.pu-*` Component Classes
|
|
657
|
+
|
|
658
|
+
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`.
|
|
659
|
+
|
|
660
|
+
## Key tokens
|
|
661
|
+
|
|
662
|
+
| Token | Purpose |
|
|
663
|
+
|---|---|
|
|
664
|
+
| `--pu-body`, `--pu-surface`, `--pu-surface-alt`, `--pu-surface-raised`, `--pu-surface-overlay` | Backgrounds |
|
|
665
|
+
| `--pu-text`, `--pu-text-muted`, `--pu-text-subtle` | Text colors |
|
|
666
|
+
| `--pu-border`, `--pu-border-muted`, `--pu-border-strong` | Borders |
|
|
667
|
+
| `--pu-input-bg`, `--pu-input-border`, `--pu-input-focus-ring`, `--pu-input-placeholder` | Form inputs |
|
|
668
|
+
| `--pu-card-bg`, `--pu-card-border` | Cards |
|
|
669
|
+
| `--pu-shadow-sm/md/lg` | Shadows |
|
|
670
|
+
| `--pu-radius-sm/md/lg/xl/full` | Border radius |
|
|
671
|
+
| `--pu-space-xs/sm/md/lg/xl` | Spacing |
|
|
672
|
+
| `--pu-transition-fast/normal/slow` | Transitions |
|
|
673
|
+
|
|
674
|
+
🚨 Tokens are CSS variables — use `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
675
|
+
|
|
676
|
+
## Customizing tokens
|
|
677
|
+
|
|
678
|
+
```css
|
|
679
|
+
:root {
|
|
680
|
+
--pu-surface: #fafafa;
|
|
681
|
+
--pu-border: #d1d5db;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.dark {
|
|
685
|
+
--pu-surface: #111827;
|
|
686
|
+
--pu-border: #374151;
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## `.pu-*` component classes
|
|
691
|
+
|
|
692
|
+
Ready-to-use styled components in `src/css/components.css`. **Prefer these over hardcoded `gray-X/dark:gray-Y` pairs.**
|
|
693
|
+
|
|
694
|
+
### Buttons
|
|
695
|
+
|
|
696
|
+
```
|
|
697
|
+
.pu-btn (base)
|
|
698
|
+
.pu-btn-md / -sm / -xs (size)
|
|
699
|
+
.pu-btn-primary / -secondary / -danger / -success / -warning / -info / -accent
|
|
700
|
+
.pu-btn-ghost / -outline
|
|
701
|
+
.pu-btn-soft-primary / -soft-danger / ...
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
```erb
|
|
705
|
+
<%= form.submit "Save", class: "pu-btn pu-btn-md pu-btn-primary" %>
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Inputs, cards, panels, tables, toolbars, empty states
|
|
709
|
+
|
|
710
|
+
```
|
|
711
|
+
.pu-input / -invalid / -valid .pu-label / -required .pu-hint / .pu-error .pu-checkbox
|
|
712
|
+
.pu-card / .pu-card-body
|
|
713
|
+
.pu-panel-header / -title / -description
|
|
714
|
+
.pu-table-wrapper / .pu-table / -header / -header-cell / -body-row / -body-row-selected / -body-cell / .pu-selection-cell
|
|
715
|
+
.pu-toolbar / -text / -actions
|
|
716
|
+
.pu-empty-state / -icon / -title / -description
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Ruby constants
|
|
720
|
+
|
|
721
|
+
```ruby
|
|
722
|
+
ComponentClasses::Button.classes(variant: :primary, size: :default, soft: false)
|
|
723
|
+
# => "pu-btn pu-btn-md pu-btn-primary"
|
|
724
|
+
|
|
725
|
+
ComponentClasses::Form::INPUT # "pu-input"
|
|
726
|
+
ComponentClasses::Form::LABEL # "pu-label"
|
|
727
|
+
ComponentClasses::Table::WRAPPER # "pu-table-wrapper"
|
|
728
|
+
ComponentClasses::Card::BASE # "pu-card"
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## Migration from hardcoded classes
|
|
732
|
+
|
|
733
|
+
| Old | New |
|
|
734
|
+
|---|---|
|
|
735
|
+
| `text-gray-900 dark:text-white` | `text-[var(--pu-text)]` |
|
|
736
|
+
| `text-gray-500 dark:text-gray-400` | `text-[var(--pu-text-muted)]` |
|
|
737
|
+
| `bg-gray-50 dark:bg-gray-700` | `bg-[var(--pu-surface)]` |
|
|
738
|
+
| `border-gray-300 dark:border-gray-600` | `border-[var(--pu-border)]` |
|
|
739
|
+
| Long input class chain | `pu-input` |
|
|
740
|
+
| Long button class chain | `pu-btn pu-btn-md pu-btn-primary` |
|
|
741
|
+
|
|
742
|
+
## `tokens` and `classes` helpers
|
|
743
|
+
|
|
744
|
+
For conditional class composition in Phlex components:
|
|
745
|
+
|
|
746
|
+
```ruby
|
|
747
|
+
class MyComponent < Plutonium::UI::Component::Base
|
|
748
|
+
def initialize(active:) = @active = active
|
|
749
|
+
|
|
750
|
+
def view_template
|
|
751
|
+
div(class: tokens(
|
|
752
|
+
"base-class",
|
|
753
|
+
active?: "bg-primary-500 text-white",
|
|
754
|
+
inactive?: "bg-gray-200 text-gray-700"
|
|
755
|
+
)) { "Content" }
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
private
|
|
759
|
+
|
|
760
|
+
def active? = @active
|
|
761
|
+
def inactive? = !@active
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# `classes` returns the class as a kwarg-friendly hash
|
|
765
|
+
div(**classes("p-4 rounded", active?: "ring-2"))
|
|
766
|
+
# => <div class="p-4 rounded ring-2">
|
|
767
|
+
|
|
768
|
+
# Then/else branches
|
|
769
|
+
tokens("base", condition?: {then: "if-true", else: "if-false"})
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
# Part 9 — Phlexi Component Themes
|
|
775
|
+
|
|
776
|
+
Themes are Ruby classes nested under a Form/Display/Table override. They merge into Plutonium's defaults — never replace wholesale, always `super.merge(...)`.
|
|
777
|
+
|
|
778
|
+
## Form theme
|
|
779
|
+
|
|
780
|
+
```ruby
|
|
781
|
+
class PostDefinition < ResourceDefinition
|
|
782
|
+
class Form < Form
|
|
783
|
+
class Theme < Plutonium::UI::Form::Theme
|
|
784
|
+
def self.theme
|
|
785
|
+
super.merge(
|
|
786
|
+
base: "bg-[var(--pu-card-bg)] shadow-md rounded-lg p-6",
|
|
787
|
+
fields_wrapper: "grid grid-cols-2 gap-6",
|
|
788
|
+
actions_wrapper: "flex justify-end mt-6 space-x-2",
|
|
789
|
+
label: "block mb-2 text-base font-bold",
|
|
790
|
+
input: "pu-input",
|
|
791
|
+
error: "pu-error",
|
|
792
|
+
button: "pu-btn pu-btn-md pu-btn-primary"
|
|
793
|
+
)
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Form theme keys
|
|
801
|
+
|
|
802
|
+
`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`.
|
|
803
|
+
|
|
804
|
+
## Display theme
|
|
805
|
+
|
|
806
|
+
```ruby
|
|
807
|
+
class Display < Display
|
|
808
|
+
class Theme < Plutonium::UI::Display::Theme
|
|
809
|
+
def self.theme
|
|
810
|
+
super.merge(
|
|
811
|
+
fields_wrapper: "grid grid-cols-3 gap-8",
|
|
812
|
+
label: "text-sm font-bold text-[var(--pu-text-muted)] mb-1",
|
|
813
|
+
string: "text-lg text-[var(--pu-text)]",
|
|
814
|
+
markdown: "prose dark:prose-invert max-w-none"
|
|
815
|
+
)
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### Display theme keys
|
|
822
|
+
|
|
823
|
+
`fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`.
|
|
824
|
+
|
|
825
|
+
## Table theme
|
|
826
|
+
|
|
827
|
+
```ruby
|
|
828
|
+
class Table < Table
|
|
829
|
+
class Theme < Plutonium::UI::Table::Theme
|
|
830
|
+
def self.theme
|
|
831
|
+
super.merge(
|
|
832
|
+
wrapper: "pu-table-wrapper",
|
|
833
|
+
base: "pu-table",
|
|
834
|
+
header: "pu-table-header",
|
|
835
|
+
header_cell: "pu-table-header-cell",
|
|
836
|
+
body_row: "pu-table-body-row",
|
|
837
|
+
body_cell: "pu-table-body-cell"
|
|
838
|
+
)
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### Table theme keys
|
|
845
|
+
|
|
846
|
+
`wrapper`, `base`, `header`, `header_cell`, `body_row`, `body_cell`, `sort_icon`.
|
|
847
|
+
|
|
848
|
+
---
|
|
849
|
+
|
|
850
|
+
## Available context
|
|
851
|
+
|
|
852
|
+
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).
|
|
853
|
+
|
|
854
|
+
In Phlex components, Rails helpers are accessed via the `helpers` proxy:
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
class MyComponent < Plutonium::UI::Component::Base
|
|
858
|
+
def view_template
|
|
859
|
+
helpers.link_to(...)
|
|
860
|
+
helpers.number_to_currency(...)
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## Portal-specific overrides
|
|
868
|
+
|
|
869
|
+
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:
|
|
870
|
+
|
|
871
|
+
```ruby
|
|
872
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
873
|
+
class ShowPage < ShowPage # inherits from ::PostDefinition::ShowPage
|
|
874
|
+
def render_after_content
|
|
875
|
+
super
|
|
876
|
+
render AdminOnlySection.new(post: object)
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
## Gotchas
|
|
885
|
+
|
|
886
|
+
- **Don't override `view_template` in pages** when a render hook fits — you lose breadcrumbs / header / DynaFrame behavior.
|
|
887
|
+
- **Always register Stimulus controllers.** Without `registerControllers(application)` the entire UI's interactive layer is dead.
|
|
888
|
+
- **Use `plutoniumTailwindConfig.merge`** — plain object merge drops Plutonium's defaults.
|
|
889
|
+
- **Dark mode is `selector`, not `class`.** Toggle via `document.documentElement.classList.toggle('dark')`.
|
|
890
|
+
- **Tokens are CSS variables, not Tailwind keys** — `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
891
|
+
- **`render_actions` is mandatory in custom `form_template`** — otherwise no submit button.
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## Related skills
|
|
896
|
+
|
|
897
|
+
- [[plutonium-resource]] — field/input/display config (`as:`, `condition:`, blocks); modal options for actions.
|
|
898
|
+
- [[plutonium-behavior]] — controller presentation hooks (`present_parent?`), available helpers (`resource_record!`, `current_scoped_entity`).
|
|
899
|
+
- [[plutonium-app]] — `pu:eject:layout`, `pu:eject:shell`, portal package overrides.
|
|
900
|
+
- [[plutonium-tenancy]] — `permitted_associations` drives the show-page tablist.
|