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,165 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
Plutonium ships a Phlex-based component kit. Use the built-in shorthand inside pages/forms/displays, or write your own custom Phlex components by inheriting `Plutonium::UI::Component::Base`.
|
|
4
|
+
|
|
5
|
+
## Built-in component kit
|
|
6
|
+
|
|
7
|
+
Inside any `Plutonium::UI::Component::Base` subclass (or any page/form/display class):
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
PageHeader(title: "Dashboard", description: "...", actions: [...])
|
|
11
|
+
Panel(class: "mt-4") { p { "Content" } }
|
|
12
|
+
Block { TabList(items: tabs) }
|
|
13
|
+
EmptyCard("No items found")
|
|
14
|
+
ActionButton(action, url: "/posts/new")
|
|
15
|
+
DynaFrameHost(src: "/some/path", loading: :lazy)
|
|
16
|
+
DynaFrameContent(content) { |frame| frame.render_content }
|
|
17
|
+
TableSearchBar()
|
|
18
|
+
TableScopesBar()
|
|
19
|
+
TableInfo(pagy)
|
|
20
|
+
TablePagination(pagy)
|
|
21
|
+
Breadcrumbs()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
These are shorthand for `render Plutonium::UI::PageHeader.new(...)` etc. — they work because every component class is exposed as a method on `Plutonium::UI::Component::Base`.
|
|
25
|
+
|
|
26
|
+
## Writing custom Phlex components
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# app/components/post_card_component.rb
|
|
30
|
+
class PostCardComponent < Plutonium::UI::Component::Base
|
|
31
|
+
def initialize(post:)
|
|
32
|
+
@post = post
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def view_template
|
|
36
|
+
div(class: "bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)] p-4") do
|
|
37
|
+
h3(class: "font-bold text-[var(--pu-text)]") { @post.title }
|
|
38
|
+
p(class: "text-[var(--pu-text-muted)] mt-2") { @post.excerpt }
|
|
39
|
+
|
|
40
|
+
div(class: "mt-4 flex justify-between items-center") do
|
|
41
|
+
span(class: "text-sm text-[var(--pu-text-subtle)]") {
|
|
42
|
+
@post.published_at&.strftime("%B %d, %Y")
|
|
43
|
+
}
|
|
44
|
+
a(href: resource_url_for(@post), class: "text-primary-600") { "Read more" }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
::: tip Always inherit `Plutonium::UI::Component::Base`
|
|
52
|
+
It gives you:
|
|
53
|
+
- The component kit (`PageHeader`, `Panel`, `Block`, …)
|
|
54
|
+
- Resource helpers (`resource_url_for`, `current_user`, `current_record!`, `current_definition`)
|
|
55
|
+
- A `helpers` proxy for Rails helpers (`helpers.link_to`, `helpers.number_to_currency`)
|
|
56
|
+
- Token / class helpers (`tokens`, `classes`)
|
|
57
|
+
:::
|
|
58
|
+
|
|
59
|
+
### Use in a definition
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
class PostDefinition < ResourceDefinition
|
|
63
|
+
display :card, as: PostCardComponent # custom display component
|
|
64
|
+
input :color, as: ColorPickerComponent # custom input component
|
|
65
|
+
|
|
66
|
+
display :metrics do |field|
|
|
67
|
+
MetricsChartComponent.new(data: field.value)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Use in a page / form / display
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class ShowPage < ShowPage
|
|
76
|
+
def render_after_content
|
|
77
|
+
render RelatedPostsComponent.new(post: object)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## `DynaFrameContent` pattern
|
|
83
|
+
|
|
84
|
+
Enables frame-aware rendering — regular requests get the full page (header + content + footer); turbo-frame requests get only the content inside the frame.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
def view_template(&block)
|
|
88
|
+
DynaFrameContent(page_content(block)) do |frame|
|
|
89
|
+
render_header # skipped for frame requests
|
|
90
|
+
frame.render_content # always rendered
|
|
91
|
+
render_footer # skipped for frame requests
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
All pages inherit this automatically. Modals and frame navigation work without special handling.
|
|
97
|
+
|
|
98
|
+
### When to call `DynaFrameContent` manually
|
|
99
|
+
|
|
100
|
+
Rarely. Use it when writing a custom non-resource page that needs the same frame-aware rendering as the built-in pages.
|
|
101
|
+
|
|
102
|
+
For typical custom pages, just inherit `Plutonium::UI::Page::Base` and override hooks like `render_content` — the DynaFrame wrapping happens in `view_template` automatically.
|
|
103
|
+
|
|
104
|
+
## Conditional class helpers
|
|
105
|
+
|
|
106
|
+
For class composition in Phlex components:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class MyComponent < Plutonium::UI::Component::Base
|
|
110
|
+
def initialize(active:)
|
|
111
|
+
@active = active
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def view_template
|
|
115
|
+
div(class: tokens(
|
|
116
|
+
"base-class",
|
|
117
|
+
active?: "bg-primary-500 text-white",
|
|
118
|
+
inactive?: "bg-gray-200 text-gray-700"
|
|
119
|
+
)) { "Content" }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def active? = @active
|
|
125
|
+
def inactive? = !@active
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`classes` returns the class as a kwarg-friendly hash:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
div(**classes("p-4 rounded", active?: "ring-2"))
|
|
133
|
+
# => <div class="p-4 rounded ring-2">
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`tokens` supports then/else branches:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
tokens("base", condition?: {then: "if-true", else: "if-false"})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Accessing Rails helpers
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class MyComponent < Plutonium::UI::Component::Base
|
|
146
|
+
def view_template
|
|
147
|
+
helpers.link_to(...)
|
|
148
|
+
helpers.image_tag(...)
|
|
149
|
+
helpers.number_to_currency(...)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The `helpers` proxy gives you everything `ApplicationController#helpers` exposes — including any custom helpers in `app/helpers/`.
|
|
155
|
+
|
|
156
|
+
## Available context
|
|
157
|
+
|
|
158
|
+
Inside any custom component, the same set of helpers as pages/forms/displays — see [Pages › Available context](./pages#available-context).
|
|
159
|
+
|
|
160
|
+
## Related
|
|
161
|
+
|
|
162
|
+
- [Pages](./pages) — `render_*` hooks call your components
|
|
163
|
+
- [Forms](./forms) — using custom input components via `as: MyComponent`
|
|
164
|
+
- [Displays](./displays) — using custom display components
|
|
165
|
+
- [Assets](./assets) — design tokens (`var(--pu-*)`) and `.pu-*` component classes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Displays
|
|
2
|
+
|
|
3
|
+
The show page's record rendering. Override the `Display` nested class in your definition for custom layouts.
|
|
4
|
+
|
|
5
|
+
## Custom display template
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class PostDefinition < ResourceDefinition
|
|
9
|
+
class Display < Display
|
|
10
|
+
def display_template
|
|
11
|
+
div(class: "bg-gradient-to-r from-primary-500 to-secondary-600 p-8 rounded-lg text-white mb-6") do
|
|
12
|
+
h1(class: "text-3xl font-bold") { object.title }
|
|
13
|
+
p(class: "mt-2 opacity-90") { object.excerpt }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Block do
|
|
17
|
+
fields_wrapper do
|
|
18
|
+
render_resource_field :author
|
|
19
|
+
render_resource_field :published_at
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Block do
|
|
24
|
+
div(class: "prose max-w-none") { raw object.content }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
render_associations if present_associations?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Methods
|
|
34
|
+
|
|
35
|
+
| Method | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `render_fields` | All permitted fields |
|
|
38
|
+
| `render_resource_field(name)` | One field |
|
|
39
|
+
| `render_associations` | Association tabs (driven by `permitted_associations` — see [Behavior › Policy](/reference/behavior/policies#association-permissions)) |
|
|
40
|
+
| `object` | The record |
|
|
41
|
+
| `resource_fields`, `resource_associations` | Permitted lists |
|
|
42
|
+
|
|
43
|
+
## Custom rendering per field
|
|
44
|
+
|
|
45
|
+
For per-field custom rendering, prefer declaring it in the **definition** rather than overriding the entire `Display`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
class PostDefinition < ResourceDefinition
|
|
49
|
+
# Block — returns any Phlex component
|
|
50
|
+
display :status do |field|
|
|
51
|
+
StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# phlexi_tag — proc whose body is rendered inside a Phlex context
|
|
55
|
+
display :priority, as: :phlexi_tag, with: ->(value, attrs) {
|
|
56
|
+
case value
|
|
57
|
+
when 'high' then span(class: "badge badge-danger") { "High" }
|
|
58
|
+
when 'medium' then span(class: "badge badge-warning") { "Medium" }
|
|
59
|
+
else span(class: "badge badge-info") { "Low" }
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Custom component class
|
|
64
|
+
display :chart, as: ChartComponent
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See [Resource › Definition › Custom rendering](/reference/resource/definition#custom-rendering) for the full per-field rendering surface.
|
|
69
|
+
|
|
70
|
+
## Theming
|
|
71
|
+
|
|
72
|
+
Override the theme via a nested `Theme` class:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class PostDefinition < ResourceDefinition
|
|
76
|
+
class Display < Display
|
|
77
|
+
class Theme < Plutonium::UI::Display::Theme
|
|
78
|
+
def self.theme
|
|
79
|
+
super.merge(
|
|
80
|
+
fields_wrapper: "grid grid-cols-3 gap-8",
|
|
81
|
+
label: "text-sm font-bold text-[var(--pu-text-muted)] mb-1",
|
|
82
|
+
string: "text-lg text-[var(--pu-text)]",
|
|
83
|
+
markdown: "prose dark:prose-invert max-w-none"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Theme keys
|
|
92
|
+
|
|
93
|
+
`fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`.
|
|
94
|
+
|
|
95
|
+
## Metadata panel
|
|
96
|
+
|
|
97
|
+
A right-side aside on the show page. Configured at the definition level, not the Display class — see [Resource › Definition › Metadata panel](/reference/resource/definition#metadata-panel-show-page).
|
|
98
|
+
|
|
99
|
+
## Related
|
|
100
|
+
|
|
101
|
+
- [Pages](./pages) — `ShowPage` render hooks (often a lighter alternative to overriding `Display`)
|
|
102
|
+
- [Components](./components) — building reusable Phlex display components
|
|
103
|
+
- [Resource › Definition](/reference/resource/definition) — field-level display configuration (`as:`, `condition:`, blocks)
|
|
104
|
+
- [Behavior › Policy](/reference/behavior/policies) — `permitted_associations` drives the show-page tablist
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
Built on [Phlexi::Form](https://github.com/radioactive-labs/phlexi-form). Override the `Form` nested class in your definition to customize templates, layouts, and field rendering.
|
|
4
|
+
|
|
5
|
+
## 🚨 Critical
|
|
6
|
+
|
|
7
|
+
- **`render_actions` is mandatory in custom `form_template`** — without it, the form has no submit button.
|
|
8
|
+
- **Configure inputs in the definition, render them with `render_resource_field`** in the form template. Don't reimplement field widgets from scratch.
|
|
9
|
+
- **Override via nested classes** (`class Form < Form; end`) inside the definition. Don't replace the root `Plutonium::UI::Form::Resource` class.
|
|
10
|
+
|
|
11
|
+
## Hierarchy
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Phlexi::Form::Base
|
|
15
|
+
└── Plutonium::UI::Form::Base
|
|
16
|
+
├── Plutonium::UI::Form::Resource # CRUD
|
|
17
|
+
│ └── Plutonium::UI::Form::Interaction # action forms
|
|
18
|
+
└── Plutonium::UI::Form::Query # search/filter
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Override the form
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class PostDefinition < ResourceDefinition
|
|
25
|
+
class Form < Form
|
|
26
|
+
def form_template
|
|
27
|
+
render_fields # render every permitted field
|
|
28
|
+
render_actions # submit buttons — REQUIRED
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Form methods
|
|
35
|
+
|
|
36
|
+
| Method | Purpose |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `form_template` | Main override point |
|
|
39
|
+
| `render_fields` | All permitted fields in default layout |
|
|
40
|
+
| `render_resource_field(name)` | One field, using the definition's `input` config |
|
|
41
|
+
| `render_actions` | Submit + secondary buttons |
|
|
42
|
+
| `fields_wrapper { ... }` | Grid wrapper div (themeable) |
|
|
43
|
+
| `actions_wrapper { ... }` | Button wrapper div (themeable) |
|
|
44
|
+
| `object` / `record` | The form record |
|
|
45
|
+
| `resource_fields` | Array of permitted field names |
|
|
46
|
+
| `resource_definition` | The definition instance |
|
|
47
|
+
|
|
48
|
+
## Custom layouts
|
|
49
|
+
|
|
50
|
+
### Sectioned form
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class Form < Form
|
|
54
|
+
def form_template
|
|
55
|
+
section("Basic Information") do
|
|
56
|
+
render_resource_field :title
|
|
57
|
+
render_resource_field :slug
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
section("Content") do
|
|
61
|
+
render_resource_field :content
|
|
62
|
+
render_resource_field :excerpt
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
section("Publishing") do
|
|
66
|
+
render_resource_field :published_at
|
|
67
|
+
render_resource_field :category
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
render_actions
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def section(title, &)
|
|
76
|
+
div(class: "mb-8") do
|
|
77
|
+
h3(class: "text-lg font-semibold mb-4 text-[var(--pu-text)]") { title }
|
|
78
|
+
fields_wrapper(&)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Two-column layout
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
def form_template
|
|
88
|
+
div(class: "grid grid-cols-1 lg:grid-cols-3 gap-6") do
|
|
89
|
+
div(class: "lg:col-span-2") do
|
|
90
|
+
fields_wrapper do
|
|
91
|
+
render_resource_field :title
|
|
92
|
+
render_resource_field :content
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
div(class: "space-y-4") do
|
|
97
|
+
Panel do
|
|
98
|
+
h4(class: "font-medium mb-2") { "Settings" }
|
|
99
|
+
render_resource_field :status
|
|
100
|
+
render_resource_field :visibility
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
render_actions
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Field builder (`field(:foo).input_tag`)
|
|
109
|
+
|
|
110
|
+
`render_resource_field` uses the input config from the definition. For ad-hoc rendering — when you want fine-grained control over a specific field — use `field(...)` directly:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
render field(:title).wrapped { |f| f.input_tag } # wrapped: label + hint + errors
|
|
114
|
+
render field(:title).input_tag # bare element only
|
|
115
|
+
render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Tag methods (standard)
|
|
119
|
+
|
|
120
|
+
| Tag | Input |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `input_tag` | text (auto-detected type) |
|
|
123
|
+
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
|
|
124
|
+
| `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
|
|
125
|
+
|
|
126
|
+
### Plutonium-enhanced tags
|
|
127
|
+
|
|
128
|
+
| Tag | Component |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `easymde_tag` / `markdown_tag` | EasyMDE markdown editor |
|
|
131
|
+
| `slim_select_tag` | Slim Select (enhanced dropdown) |
|
|
132
|
+
| `flatpickr_tag` | Flatpickr date/time picker |
|
|
133
|
+
| `phone_tag` / `int_tel_input_tag` | intl-tel-input phone field |
|
|
134
|
+
| `uppy_tag` / `file_tag` | Uppy file upload |
|
|
135
|
+
| `secure_association_tag` | Association with policy-checked options (inline `+` add, typeahead) |
|
|
136
|
+
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
137
|
+
| `key_value_store_tag` | Key/value pairs editor |
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
render field(:published_at).wrapped { |f| f.flatpickr_tag(min_date: Date.today, enable_time: true) }
|
|
141
|
+
|
|
142
|
+
render field(:avatar).wrapped do |f|
|
|
143
|
+
f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes)
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Wrapped vs unwrapped
|
|
148
|
+
|
|
149
|
+
- `wrapped` — includes label, hint, and error rendering. Use for normal form fields.
|
|
150
|
+
- Bare tag — just the input element. Use when you're laying out custom wrappers.
|
|
151
|
+
- `wrapped(class: "...")` — pass classes to the wrapper div.
|
|
152
|
+
|
|
153
|
+
## Association inputs (`secure_association_tag`)
|
|
154
|
+
|
|
155
|
+
Association inputs render with two affordances out of the box:
|
|
156
|
+
|
|
157
|
+
- **Inline `+` add** — a button next to the select opens the target resource's `:new` action. Inherits the target's modal mode. If the parent form is already in a modal, the `+` opens a **stacked secondary modal** (see [Pages › Stacked modals](./pages#stacked-modals-secondary-frame)) so the in-progress form isn't lost — on success the secondary closes and the parent reloads.
|
|
158
|
+
- **Typeahead** — server-side autocomplete is on by default. Uses the target's `search` block if defined; otherwise falls back to a `LIKE` on the input's `label_method:` column or the first match from `[name, title, label, slug, display_name, email]`. See [Resource › Query › Search](/reference/resource/query#search) for the typeahead fallback details.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# Opt out of the + button
|
|
162
|
+
input :author, add_action: false
|
|
163
|
+
|
|
164
|
+
# Custom add URL
|
|
165
|
+
input :author, add_action: "/internal/users/new"
|
|
166
|
+
|
|
167
|
+
# Opt out of typeahead (use slim-select's client filter only)
|
|
168
|
+
input :author, typeahead: false
|
|
169
|
+
|
|
170
|
+
# Pick a non-default searchable column
|
|
171
|
+
input :author, label_method: :email
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
::: tip Large association tables
|
|
175
|
+
For large target tables, write an explicit `search` block on the target resource definition — the fallback's leading-wildcard `LIKE` can't use a b-tree index.
|
|
176
|
+
:::
|
|
177
|
+
|
|
178
|
+
## Submit buttons
|
|
179
|
+
|
|
180
|
+
Default `render_actions` produces the primary submit, plus an optional "Save and add another" / "Update and continue editing" secondary button.
|
|
181
|
+
|
|
182
|
+
Control the secondary button via the definition:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
class PostDefinition < ResourceDefinition
|
|
186
|
+
submit_and_continue false # nil (default — auto), true (always show), false (always hide)
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Singular resources auto-hide it (creating "another" doesn't make sense for `/profile`).
|
|
191
|
+
|
|
192
|
+
### Custom action strip
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
def render_actions
|
|
196
|
+
actions_wrapper do
|
|
197
|
+
a(href: resource_url_for(resource_class), class: "pu-btn pu-btn-md pu-btn-secondary") { "Cancel" }
|
|
198
|
+
button(type: :submit, name: "draft", value: "1", class: "pu-btn pu-btn-md") { "Save Draft" }
|
|
199
|
+
render submit_button
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Pre-submit, nested inputs, interaction forms
|
|
205
|
+
|
|
206
|
+
These all live in the definition layer:
|
|
207
|
+
|
|
208
|
+
- **Pre-submit / dynamic forms** — see [Resource › Definition › Dynamic forms](/reference/resource/definition#dynamic-forms-pre-submit)
|
|
209
|
+
- **Nested inputs** (`nested_input :variants`) — see [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs)
|
|
210
|
+
- **Interaction forms** — interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [Behavior › Interactions](/reference/behavior/interactions)
|
|
211
|
+
|
|
212
|
+
## Theming
|
|
213
|
+
|
|
214
|
+
Forms use a theme system for consistent styling. Override per-resource by nesting a `Theme` class inside `Form`:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
class PostDefinition < ResourceDefinition
|
|
218
|
+
class Form < Form
|
|
219
|
+
class Theme < Plutonium::UI::Form::Theme
|
|
220
|
+
def self.theme
|
|
221
|
+
super.merge(
|
|
222
|
+
base: "bg-[var(--pu-card-bg)] shadow-md rounded-lg p-6",
|
|
223
|
+
fields_wrapper: "grid grid-cols-2 gap-6",
|
|
224
|
+
actions_wrapper: "flex justify-end mt-6 space-x-2",
|
|
225
|
+
label: "block mb-2 text-base font-bold",
|
|
226
|
+
input: "pu-input",
|
|
227
|
+
error: "pu-error",
|
|
228
|
+
button: "pu-btn pu-btn-md pu-btn-primary"
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
::: warning Always `super.merge(...)`
|
|
237
|
+
Don't replace the theme wholesale — Plutonium's defaults handle invalid states, focus rings, and dark mode. `super.merge` keeps them.
|
|
238
|
+
:::
|
|
239
|
+
|
|
240
|
+
### Theme keys
|
|
241
|
+
|
|
242
|
+
`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`.
|
|
243
|
+
|
|
244
|
+
See [Assets › Phlexi component themes](./assets#phlexi-component-themes) for the underlying theme system.
|
|
245
|
+
|
|
246
|
+
## Context inside form templates
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
class Form < Form
|
|
250
|
+
def form_template
|
|
251
|
+
# Form object
|
|
252
|
+
object # the record
|
|
253
|
+
record # alias for object
|
|
254
|
+
object.new_record? # check if creating
|
|
255
|
+
|
|
256
|
+
# Request context
|
|
257
|
+
current_user
|
|
258
|
+
current_parent
|
|
259
|
+
current_scoped_entity
|
|
260
|
+
request
|
|
261
|
+
params
|
|
262
|
+
|
|
263
|
+
# Definition
|
|
264
|
+
resource_definition
|
|
265
|
+
resource_fields # permitted fields
|
|
266
|
+
|
|
267
|
+
# URL helpers
|
|
268
|
+
resource_url_for(object)
|
|
269
|
+
resource_url_for(Post, action: :new)
|
|
270
|
+
|
|
271
|
+
# Rails helpers
|
|
272
|
+
helpers.link_to(...)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Related
|
|
278
|
+
|
|
279
|
+
- [Pages](./pages) — `NewPage` / `EditPage` page hooks
|
|
280
|
+
- [Components](./components) — building reusable Phlex components for forms
|
|
281
|
+
- [Assets](./assets) — `.pu-*` classes, design tokens, dark mode
|
|
282
|
+
- [Resource › Definition](/reference/resource/definition) — input configuration (`as:`, `hint:`, `condition:`, blocks)
|
|
283
|
+
- [Behavior › Interactions](/reference/behavior/interactions) — interaction forms (`Plutonium::UI::Form::Interaction`)
|
|
284
|
+
- [Tenancy › Nested resources](/reference/tenancy/nested-resources) — parent fields hidden by URL
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# UI Reference
|
|
2
|
+
|
|
3
|
+
Plutonium uses [Phlex](https://www.phlex.fun/) for all view components and TailwindCSS 4 + Stimulus for the frontend.
|
|
4
|
+
|
|
5
|
+
## Sub-pages
|
|
6
|
+
|
|
7
|
+
- [Pages](./pages) — `IndexPage`, `ShowPage`, `NewPage`, `EditPage`, render hooks, custom ERB views, context detection
|
|
8
|
+
- [Forms](./forms) — `Form` class, field builder, association inputs (typeahead + inline add), themes
|
|
9
|
+
- [Displays](./displays) — `Display` class, custom rendering, `phlexi_tag`
|
|
10
|
+
- [Tables](./tables) — `Table` class, custom rendering, search/scopes bar
|
|
11
|
+
- [Components](./components) — built-in component kit, custom Phlex components, `DynaFrameContent` pattern, modals & tabs
|
|
12
|
+
- [Layouts](./layouts) — shell config, ejecting chrome, custom `ResourceLayout` class
|
|
13
|
+
- [Assets](./assets) — Tailwind config, Stimulus controllers, design tokens, `.pu-*` component classes, Phlexi themes
|
|
14
|
+
|
|
15
|
+
## 🚨 Critical (applies across all sub-pages)
|
|
16
|
+
|
|
17
|
+
- **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end`. Don't replace the entire view layer.
|
|
18
|
+
- **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.
|
|
19
|
+
- **All pages inherit `DynaFrameContent`** — turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work".
|
|
20
|
+
- **Custom components inherit `Plutonium::UI::Component::Base`** — gives you the component kit (`PageHeader`, `Panel`, `Block`), resource helpers, and the `helpers` proxy for Rails helpers.
|
|
21
|
+
- **`render_actions` is mandatory in custom `form_template`** — without it, the form has no submit button.
|
|
22
|
+
- **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.
|
|
23
|
+
- **Use `plutoniumTailwindConfig.merge`** when extending Tailwind theme — plain object merge drops Plutonium's defaults.
|
|
24
|
+
- **Prefer `.pu-*` classes and `var(--pu-*)` tokens** over hardcoded `gray-X/dark:gray-Y` pairs — they switch with dark mode automatically.
|
|
25
|
+
- **Configure inputs in the definition; render them with `render_resource_field` in the form.** Don't reimplement field widgets from scratch.
|
|
26
|
+
|
|
27
|
+
## Related
|
|
28
|
+
|
|
29
|
+
- [Resource › Definition](/reference/resource/definition) — field-level rendering (`field :foo, as: :markdown`, `display :status do |f| … end`)
|
|
30
|
+
- [Behavior › Controllers](/reference/behavior/controllers) — controller render-context hooks (`present_parent?`, `submit_parent?`)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Layouts
|
|
2
|
+
|
|
3
|
+
The overall page chrome — topbar, sidebar, footer, body wrapping. Plutonium ships two shells; you can eject the templates or write a custom `ResourceLayout` for total control.
|
|
4
|
+
|
|
5
|
+
## Shell
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Plutonium.configure do |config|
|
|
9
|
+
config.shell = :modern # default — topbar + icon rail
|
|
10
|
+
# config.shell = :classic # legacy header + sidebar (only when upgrading)
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
::: tip `:classic` is only for upgrade paths
|
|
15
|
+
If you're starting fresh, use `:modern`. `:classic` exists so apps upgrading from pre-`:modern` versions can preserve their chrome while migrating.
|
|
16
|
+
:::
|
|
17
|
+
|
|
18
|
+
## Eject the chrome for per-portal customization
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
rails generate pu:eject:shell --dest=admin_portal
|
|
22
|
+
rails generate pu:eject:layout
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`pu:eject:shell` copies `_resource_header.html.erb` and `_resource_sidebar.html.erb` into the portal's `app/views/plutonium/`. The eject is independent of `shell` — you can run it on either.
|
|
26
|
+
|
|
27
|
+
`pu:eject:layout` copies `layouts/resource.html.erb` for layout-level edits.
|
|
28
|
+
|
|
29
|
+
## Custom layout class
|
|
30
|
+
|
|
31
|
+
For full Phlex-level control over the layout:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
module AdminPortal
|
|
35
|
+
class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def body_attributes
|
|
39
|
+
{class: "antialiased bg-[var(--pu-body)]"}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_before_main
|
|
43
|
+
super
|
|
44
|
+
render AnnouncementBanner.new if Announcement.active.any?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_body_scripts
|
|
48
|
+
super
|
|
49
|
+
script(src: "/custom-analytics.js")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Layout hooks
|
|
56
|
+
|
|
57
|
+
| Hook | Position |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `render_before_main` / `_after_main` | around the main content area |
|
|
60
|
+
| `render_before_content` / `_after_content` | inside main, around content |
|
|
61
|
+
| `render_flash` | flash messages |
|
|
62
|
+
| `render_head`, `render_title`, `render_metatags`, `render_assets` | head section |
|
|
63
|
+
| `render_body_scripts` | end-of-body scripts |
|
|
64
|
+
| `render_fonts` | font links |
|
|
65
|
+
|
|
66
|
+
## Typography
|
|
67
|
+
|
|
68
|
+
Plutonium uses Lato by default. Override via `render_fonts`:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class MyLayout < Plutonium::UI::Layout::ResourceLayout
|
|
72
|
+
def render_fonts
|
|
73
|
+
link(rel: "preconnect", href: "https://fonts.googleapis.com")
|
|
74
|
+
link(href: "https://fonts.googleapis.com/css2?family=Inter&display=swap", rel: "stylesheet")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Then configure Tailwind to match:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
// tailwind.config.js
|
|
83
|
+
theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
84
|
+
fontFamily: {
|
|
85
|
+
body: ['Inter', 'sans-serif'],
|
|
86
|
+
sans: ['Inter', 'sans-serif']
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
See [Assets › Tailwind config](./assets#tailwind-config) for the full merge story.
|
|
92
|
+
|
|
93
|
+
## Dark mode
|
|
94
|
+
|
|
95
|
+
`selector` strategy — toggle by adding/removing `dark` on `<html>`. The bundled `color-mode` Stimulus controller handles toggling; Plutonium ships a switcher.
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// Manual toggle if needed
|
|
99
|
+
document.documentElement.classList.toggle('dark')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Related
|
|
103
|
+
|
|
104
|
+
- [Assets](./assets) — Tailwind config, design tokens, `.pu-*` classes
|
|
105
|
+
- [Components](./components) — custom components used in layout hooks (`AnnouncementBanner`, etc.)
|
|
106
|
+
- [Pages](./pages) — page-level hooks (a lighter alternative for per-page chrome)
|