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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. 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)