plutonium 0.49.1 → 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 (206) 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 +37 -0
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1323 -1184
  14. data/app/assets/plutonium.js.map +4 -4
  15. data/app/assets/plutonium.min.js +50 -49
  16. data/app/assets/plutonium.min.js.map +4 -4
  17. data/app/views/plutonium/_resource_header.html.erb +4 -4
  18. data/app/views/plutonium/_resource_sidebar.html.erb +9 -9
  19. data/app/views/resource/_resource_grid.html.erb +1 -0
  20. data/config/brakeman.ignore +25 -2
  21. data/docs/.vitepress/config.ts +37 -27
  22. data/docs/getting-started/index.md +22 -29
  23. data/docs/getting-started/installation.md +37 -80
  24. data/docs/getting-started/tutorial/index.md +4 -5
  25. data/docs/guides/adding-resources.md +66 -377
  26. data/docs/guides/authentication.md +94 -463
  27. data/docs/guides/authorization.md +124 -370
  28. data/docs/guides/creating-packages.md +94 -296
  29. data/docs/guides/custom-actions.md +121 -441
  30. data/docs/guides/index.md +22 -42
  31. data/docs/guides/multi-tenancy.md +116 -187
  32. data/docs/guides/nested-resources.md +103 -431
  33. data/docs/guides/search-filtering.md +123 -240
  34. data/docs/guides/testing.md +5 -4
  35. data/docs/guides/theming.md +157 -407
  36. data/docs/guides/troubleshooting.md +5 -3
  37. data/docs/guides/user-invites.md +106 -425
  38. data/docs/guides/user-profile.md +76 -243
  39. data/docs/index.md +1 -1
  40. data/docs/reference/app/generators.md +517 -0
  41. data/docs/reference/app/index.md +158 -0
  42. data/docs/reference/app/packages.md +146 -0
  43. data/docs/reference/app/portals.md +377 -0
  44. data/docs/reference/auth/accounts.md +230 -0
  45. data/docs/reference/auth/index.md +88 -0
  46. data/docs/reference/auth/profile.md +185 -0
  47. data/docs/reference/behavior/controllers.md +395 -0
  48. data/docs/reference/behavior/index.md +22 -0
  49. data/docs/reference/behavior/interactions.md +341 -0
  50. data/docs/reference/behavior/policies.md +417 -0
  51. data/docs/reference/index.md +56 -49
  52. data/docs/reference/resource/actions.md +423 -0
  53. data/docs/reference/resource/definition.md +508 -0
  54. data/docs/reference/resource/index.md +50 -0
  55. data/docs/reference/resource/model.md +348 -0
  56. data/docs/reference/resource/query.md +305 -0
  57. data/docs/reference/tenancy/entity-scoping.md +361 -0
  58. data/docs/reference/tenancy/index.md +36 -0
  59. data/docs/reference/tenancy/invites.md +393 -0
  60. data/docs/reference/tenancy/nested-resources.md +267 -0
  61. data/docs/reference/testing/index.md +287 -0
  62. data/docs/reference/ui/assets.md +400 -0
  63. data/docs/reference/ui/components.md +165 -0
  64. data/docs/reference/ui/displays.md +104 -0
  65. data/docs/reference/ui/forms.md +284 -0
  66. data/docs/reference/ui/index.md +30 -0
  67. data/docs/reference/ui/layouts.md +106 -0
  68. data/docs/reference/ui/pages.md +189 -0
  69. data/docs/reference/ui/tables.md +117 -0
  70. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md +841 -0
  71. data/docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json +103 -0
  72. data/docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md +270 -0
  73. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  74. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  75. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  76. data/gemfiles/rails_7.gemfile.lock +1 -1
  77. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  78. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  79. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +1 -0
  80. data/lib/generators/pu/invites/install_generator.rb +1 -0
  81. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +54 -5
  82. data/lib/plutonium/action/base.rb +44 -1
  83. data/lib/plutonium/action/interactive.rb +1 -1
  84. data/lib/plutonium/configuration.rb +4 -0
  85. data/lib/plutonium/definition/actions.rb +3 -0
  86. data/lib/plutonium/definition/base.rb +8 -0
  87. data/lib/plutonium/definition/index_views.rb +95 -0
  88. data/lib/plutonium/definition/metadata.rb +40 -0
  89. data/lib/plutonium/helpers/turbo_helper.rb +12 -1
  90. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  91. data/lib/plutonium/interaction/response/redirect.rb +1 -1
  92. data/lib/plutonium/query/base.rb +8 -0
  93. data/lib/plutonium/query/filters/association.rb +30 -8
  94. data/lib/plutonium/query/filters/boolean.rb +5 -0
  95. data/lib/plutonium/resource/controller.rb +1 -0
  96. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  97. data/lib/plutonium/resource/controllers/presentable.rb +11 -2
  98. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  99. data/lib/plutonium/resource/definition.rb +42 -0
  100. data/lib/plutonium/resource/policy.rb +7 -0
  101. data/lib/plutonium/resource/query_object.rb +64 -6
  102. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  103. data/lib/plutonium/testing/resource_definition.rb +2 -2
  104. data/lib/plutonium/ui/action_button.rb +4 -2
  105. data/lib/plutonium/ui/component/kit.rb +12 -0
  106. data/lib/plutonium/ui/component/methods.rb +4 -0
  107. data/lib/plutonium/ui/display/base.rb +3 -1
  108. data/lib/plutonium/ui/display/resource.rb +109 -25
  109. data/lib/plutonium/ui/display/theme.rb +2 -1
  110. data/lib/plutonium/ui/dyna_frame/content.rb +8 -14
  111. data/lib/plutonium/ui/empty_card.rb +1 -1
  112. data/lib/plutonium/ui/form/base.rb +35 -3
  113. data/lib/plutonium/ui/form/components/hidden_wrapper.rb +25 -0
  114. data/lib/plutonium/ui/form/components/json.rb +58 -0
  115. data/lib/plutonium/ui/form/components/resource_select.rb +133 -1
  116. data/lib/plutonium/ui/form/components/secure_association.rb +105 -24
  117. data/lib/plutonium/ui/form/components/sticky_footer.rb +17 -0
  118. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  119. data/lib/plutonium/ui/form/resource.rb +45 -10
  120. data/lib/plutonium/ui/form/theme.rb +1 -1
  121. data/lib/plutonium/ui/frame_navigator_panel.rb +7 -4
  122. data/lib/plutonium/ui/grid/card.rb +235 -0
  123. data/lib/plutonium/ui/grid/resource.rb +149 -0
  124. data/lib/plutonium/ui/layout/base.rb +38 -1
  125. data/lib/plutonium/ui/layout/header.rb +1 -2
  126. data/lib/plutonium/ui/layout/icon_rail.rb +212 -0
  127. data/lib/plutonium/ui/layout/resource_layout.rb +10 -3
  128. data/lib/plutonium/ui/layout/sidebar.rb +12 -24
  129. data/lib/plutonium/ui/layout/topbar.rb +100 -0
  130. data/lib/plutonium/ui/modal/base.rb +109 -0
  131. data/lib/plutonium/ui/modal/centered.rb +21 -0
  132. data/lib/plutonium/ui/modal/slideover.rb +26 -0
  133. data/lib/plutonium/ui/page/base.rb +18 -6
  134. data/lib/plutonium/ui/page/edit.rb +13 -1
  135. data/lib/plutonium/ui/page/index.rb +40 -1
  136. data/lib/plutonium/ui/page/interactive_action.rb +8 -39
  137. data/lib/plutonium/ui/page/new.rb +13 -1
  138. data/lib/plutonium/ui/page/show.rb +8 -1
  139. data/lib/plutonium/ui/page_header.rb +8 -13
  140. data/lib/plutonium/ui/panel.rb +10 -19
  141. data/lib/plutonium/ui/sidebar_menu.rb +2 -25
  142. data/lib/plutonium/ui/tab_list.rb +29 -7
  143. data/lib/plutonium/ui/table/base.rb +106 -0
  144. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +12 -4
  145. data/lib/plutonium/ui/table/components/filter_form.rb +171 -0
  146. data/lib/plutonium/ui/table/components/filter_pills.rb +89 -0
  147. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +13 -12
  148. data/lib/plutonium/ui/table/components/scopes_pills.rb +67 -0
  149. data/lib/plutonium/ui/table/components/selection_column.rb +2 -11
  150. data/lib/plutonium/ui/table/components/toolbar.rb +104 -0
  151. data/lib/plutonium/ui/table/components/view_switcher.rb +81 -0
  152. data/lib/plutonium/ui/table/resource.rb +158 -89
  153. data/lib/plutonium/ui/table/theme.rb +14 -5
  154. data/lib/plutonium/version.rb +1 -1
  155. data/lib/plutonium.rb +14 -0
  156. data/lib/tasks/release.rake +15 -1
  157. data/package.json +10 -10
  158. data/src/css/components.css +304 -131
  159. data/src/css/slim_select.css +4 -0
  160. data/src/css/tokens.css +101 -85
  161. data/src/js/controllers/autosubmit_controller.js +24 -0
  162. data/src/js/controllers/bulk_actions_controller.js +15 -16
  163. data/src/js/controllers/capture_url_controller.js +14 -0
  164. data/src/js/controllers/filter_panel_controller.js +77 -19
  165. data/src/js/controllers/frame_navigator_controller.js +34 -6
  166. data/src/js/controllers/icon_rail_controller.js +22 -0
  167. data/src/js/controllers/icon_rail_flyout_controller.js +128 -0
  168. data/src/js/controllers/register_controllers.js +16 -0
  169. data/src/js/controllers/resource_tab_list_controller.js +56 -3
  170. data/src/js/controllers/row_click_controller.js +21 -0
  171. data/src/js/controllers/slim_select_controller.js +61 -0
  172. data/src/js/controllers/table_column_menu_controller.js +43 -0
  173. data/src/js/controllers/table_header_controller.js +16 -0
  174. data/src/js/controllers/view_switcher_controller.js +29 -0
  175. data/src/js/turbo/turbo_actions.js +33 -0
  176. data/yarn.lock +553 -543
  177. metadata +71 -32
  178. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  179. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  180. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  181. data/.claude/skills/plutonium-definition/SKILL.md +0 -1138
  182. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  183. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  184. data/.claude/skills/plutonium-installation/SKILL.md +0 -325
  185. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  186. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  187. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  188. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  189. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  190. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  191. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  192. data/.claude/skills/plutonium-views/SKILL.md +0 -592
  193. data/docs/reference/assets/index.md +0 -496
  194. data/docs/reference/controller/index.md +0 -412
  195. data/docs/reference/definition/actions.md +0 -449
  196. data/docs/reference/definition/fields.md +0 -383
  197. data/docs/reference/definition/index.md +0 -268
  198. data/docs/reference/definition/query.md +0 -351
  199. data/docs/reference/generators/index.md +0 -648
  200. data/docs/reference/interaction/index.md +0 -449
  201. data/docs/reference/model/features.md +0 -248
  202. data/docs/reference/model/index.md +0 -218
  203. data/docs/reference/policy/index.md +0 -456
  204. data/docs/reference/portal/index.md +0 -379
  205. data/docs/reference/views/forms.md +0 -411
  206. data/docs/reference/views/index.md +0 -501
@@ -1,592 +0,0 @@
1
- ---
2
- name: plutonium-views
3
- description: Use BEFORE building a custom page, panel, table, layout, or Phlex component in Plutonium. Also when overriding IndexPage/ShowPage/Form classes in a definition.
4
- ---
5
-
6
- # Plutonium Views
7
-
8
- ## 🚨 Critical (read first)
9
- - **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end` — don't replace the entire view layer.
10
- - **Use the render hooks.** `render_before_content`, `render_after_content`, `render_before_toolbar`, etc. — they exist so you don't have to override `view_template` and re-implement everything.
11
- - **All pages inherit `DynaFrameContent`** so turbo-frame requests render only the content. Don't fight it — modals and frame nav "just work".
12
- - **For custom components, use `Plutonium::UI::Component::Base`** so you inherit the component kit (`PageHeader`, `Panel`, `Block`, etc.) and access to resource helpers.
13
- - **Related skills:** `plutonium-forms` (form customization), `plutonium-assets` (theming + component classes), `plutonium-definition` (field-level rendering), `plutonium-controller` (presentation hooks like `present_parent?`).
14
-
15
- Plutonium uses [Phlex](https://www.phlex.fun/) for all view components. This provides a Ruby-first approach to building HTML with full IDE support and type safety.
16
-
17
- ## Architecture Overview
18
-
19
- ```
20
- Definition
21
- ├── IndexPage → renders Table
22
- ├── ShowPage → renders Display
23
- ├── NewPage → renders Form
24
- ├── EditPage → renders Form
25
- └── InteractiveActionPage → renders Form
26
- ```
27
-
28
- Each definition has nested classes you can override:
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 Form < Form; end
37
- class Table < Table; end
38
- class Display < Display; end
39
- end
40
- ```
41
-
42
- ## Page Customization
43
-
44
- ### Page Configuration
45
-
46
- Set page titles and descriptions in definitions:
47
-
48
- ```ruby
49
- class PostDefinition < ResourceDefinition
50
- # Static titles
51
- index_page_title "Blog Posts"
52
- index_page_description "Manage all published articles"
53
-
54
- show_page_title "Article Details"
55
- new_page_title "Write New Article"
56
- edit_page_title "Edit Article"
57
-
58
- # Control breadcrumbs
59
- breadcrumbs true # Global default
60
- index_page_breadcrumbs false # Per-page override
61
- show_page_breadcrumbs true
62
- end
63
- ```
64
-
65
- ### Custom Page Class
66
-
67
- Override page rendering by subclassing:
68
-
69
- ```ruby
70
- class PostDefinition < ResourceDefinition
71
- class ShowPage < ShowPage
72
- private
73
-
74
- # Custom title logic
75
- def page_title
76
- "#{object.title} - #{object.author.name}"
77
- end
78
-
79
- # Add content before the main area
80
- def render_before_content
81
- div(class: "alert alert-info") {
82
- "This post has #{object.comments.count} comments"
83
- }
84
- end
85
-
86
- # Add content after
87
- def render_after_content
88
- render RelatedPostsComponent.new(post: object)
89
- end
90
-
91
- # Override the toolbar
92
- def render_toolbar
93
- div(class: "flex gap-2") {
94
- button(class: "btn") { "Preview" }
95
- button(class: "btn btn-primary") { "Publish" }
96
- }
97
- end
98
- end
99
- end
100
- ```
101
-
102
- ### Page Hooks
103
-
104
- All pages inherit these customization hooks:
105
-
106
- | Hook | Purpose |
107
- |------|---------|
108
- | `render_before_header` | Before entire header section |
109
- | `render_after_header` | After entire header section |
110
- | `render_before_breadcrumbs` | Before breadcrumbs |
111
- | `render_after_breadcrumbs` | After breadcrumbs |
112
- | `render_before_page_header` | Before title/actions |
113
- | `render_after_page_header` | After title/actions |
114
- | `render_before_toolbar` | Before toolbar |
115
- | `render_after_toolbar` | After toolbar |
116
- | `render_before_content` | Before main content |
117
- | `render_after_content` | After main content |
118
- | `render_before_footer` | Before footer |
119
- | `render_after_footer` | After footer |
120
-
121
- ### Custom View Files
122
-
123
- For complete control, create custom ERB view files that replace the default entirely.
124
-
125
- **File locations:**
126
-
127
- ```
128
- # Main app (for a PostsController)
129
- app/views/posts/index.html.erb
130
- app/views/posts/show.html.erb
131
- app/views/posts/new.html.erb
132
- app/views/posts/edit.html.erb
133
-
134
- # Portal-specific
135
- packages/admin_portal/app/views/admin_portal/posts/show.html.erb
136
- ```
137
-
138
- **Default view structure:**
139
-
140
- The default views simply render the page class:
141
-
142
- ```erb
143
- <%# app/views/resource/show.html.erb %>
144
- <%= render current_definition.show_page_class.new %>
145
- ```
146
-
147
- **Custom view example:**
148
-
149
- ```erb
150
- <%# app/views/posts/show.html.erb %>
151
- <div class="max-w-4xl mx-auto">
152
- <article class="prose lg:prose-xl">
153
- <h1><%= resource_record!.title %></h1>
154
- <div class="meta text-gray-500">
155
- By <%= resource_record!.author.name %> on <%= resource_record!.created_at.strftime("%B %d, %Y") %>
156
- </div>
157
- <%= raw resource_record!.content %>
158
- </article>
159
-
160
- <div class="mt-8">
161
- <%= link_to "Edit", resource_url_for(resource_record!, action: :edit), class: "btn" %>
162
- <%= link_to "Back", resource_url_for(Post), class: "btn" %>
163
- </div>
164
- </div>
165
- ```
166
-
167
- **Mixing approaches:**
168
-
169
- Render the default page with additions:
170
-
171
- ```erb
172
- <%# app/views/posts/show.html.erb %>
173
- <div class="announcement-banner">
174
- Special announcement here
175
- </div>
176
-
177
- <%= render current_definition.show_page_class.new %>
178
-
179
- <div class="related-posts">
180
- <%= render partial: "related_posts", locals: { post: resource_record! } %>
181
- </div>
182
- ```
183
-
184
- ## Form Customization
185
-
186
- ### Custom Form Template
187
-
188
- Override how fields are rendered:
189
-
190
- ```ruby
191
- class PostDefinition < ResourceDefinition
192
- class Form < Form
193
- def form_template
194
- # Custom layout with sections
195
- div(class: "grid grid-cols-2 gap-6") {
196
- div {
197
- h3(class: "text-lg font-medium") { "Basic Info" }
198
- render_resource_field :title
199
- render_resource_field :slug
200
- }
201
-
202
- div {
203
- h3(class: "text-lg font-medium") { "Content" }
204
- render_resource_field :content
205
- }
206
- }
207
-
208
- div(class: "mt-6") {
209
- h3(class: "text-lg font-medium") { "Publishing" }
210
- render_resource_field :published_at
211
- render_resource_field :category
212
- }
213
-
214
- render_actions
215
- end
216
- end
217
- end
218
- ```
219
-
220
- ### Form Methods
221
-
222
- | Method | Purpose |
223
- |--------|---------|
224
- | `render_fields` | Render all permitted fields |
225
- | `render_resource_field(name)` | Render a single field |
226
- | `render_actions` | Render submit buttons |
227
- | `record` | The form object (alias: `object`) |
228
- | `resource_fields` | List of permitted field names |
229
- | `resource_definition` | The definition instance |
230
-
231
- ## Display Customization
232
-
233
- ### Custom Display Template
234
-
235
- Override the show page detail rendering:
236
-
237
- ```ruby
238
- class PostDefinition < ResourceDefinition
239
- class Display < Display
240
- def display_template
241
- # Hero section
242
- div(class: "bg-gradient-to-r from-blue-500 to-purple-600 p-8 rounded-lg text-white mb-6") {
243
- h1(class: "text-3xl font-bold") { object.title }
244
- p(class: "mt-2 opacity-90") { object.excerpt }
245
- }
246
-
247
- # Main content
248
- Block do
249
- fields_wrapper do
250
- render_resource_field :author
251
- render_resource_field :published_at
252
- render_resource_field :category
253
- end
254
- end
255
-
256
- # Full-width content
257
- Block do
258
- div(class: "prose max-w-none") {
259
- raw object.content
260
- }
261
- end
262
-
263
- # Associations (tabs)
264
- render_associations if present_associations?
265
- end
266
- end
267
- end
268
- ```
269
-
270
- ### Display Methods
271
-
272
- | Method | Purpose |
273
- |--------|---------|
274
- | `render_fields` | Render all permitted fields in a block |
275
- | `render_resource_field(name)` | Render single field |
276
- | `render_associations` | Render association tabs |
277
- | `object` | The record being displayed |
278
- | `resource_fields` | List of permitted field names |
279
- | `resource_associations` | List of permitted associations |
280
-
281
- ## Table Customization
282
-
283
- ### Custom Table Rendering
284
-
285
- Override list page table:
286
-
287
- ```ruby
288
- class PostDefinition < ResourceDefinition
289
- class Table < Table
290
- def view_template
291
- render_search_bar
292
- render_scopes_bar
293
-
294
- if collection.empty?
295
- render_empty_card
296
- else
297
- # Custom card grid instead of table
298
- div(class: "grid grid-cols-3 gap-4") {
299
- collection.each do |post|
300
- render PostCardComponent.new(post:)
301
- end
302
- }
303
- end
304
-
305
- render_footer
306
- end
307
- end
308
- end
309
- ```
310
-
311
- ### Table Methods
312
-
313
- | Method | Purpose |
314
- |--------|---------|
315
- | `render_search_bar` | Search input |
316
- | `render_scopes_bar` | Scope tabs |
317
- | `render_table` | Default table |
318
- | `render_empty_card` | Empty state |
319
- | `render_footer` | Pagination |
320
- | `collection` | The paginated records |
321
- | `resource_fields` | Column field names |
322
-
323
- ## Component Kit
324
-
325
- Plutonium provides shorthand methods for common components:
326
-
327
- ```ruby
328
- class MyPage < Plutonium::UI::Page::Base
329
- def render_content
330
- # These are automatically rendered
331
- PageHeader(title: "Dashboard")
332
-
333
- Panel(class: "mt-4") {
334
- p { "Content here" }
335
- }
336
-
337
- Block {
338
- TabList(items: tabs)
339
- }
340
-
341
- EmptyCard("No items found")
342
-
343
- ActionButton(action, url: "/posts/new")
344
- end
345
- end
346
- ```
347
-
348
- Available kit methods:
349
- - `Breadcrumbs()`
350
- - `PageHeader(title:, description:, actions:)`
351
- - `Panel(**attrs)`
352
- - `Block(**attrs)`
353
- - `TabList(items:)`
354
- - `EmptyCard(message)`
355
- - `ActionButton(action, url:)`
356
- - `DynaFrameHost(src:, loading:)` - Lazy-loading turbo frame
357
- - `DynaFrameContent(content) { |frame| ... }` - Frame-aware content wrapper
358
- - `TableSearchBar()`
359
- - `TableScopesBar()`
360
- - `TableInfo(pagy)`
361
- - `TablePagination(pagy)`
362
-
363
- ## DynaFrameContent Pattern
364
-
365
- `DynaFrameContent` enables frame-aware rendering. For turbo-frame requests, only the content is rendered inside the frame. For regular requests, the full page renders with header/footer.
366
-
367
- ```ruby
368
- # How Page::Base uses DynaFrameContent
369
- def view_template(&block)
370
- DynaFrameContent(page_content(block)) do |frame|
371
- render_header # Skipped for frame requests
372
- frame.render_content # Always rendered (in turbo-frame for frame requests)
373
- render_footer # Skipped for frame requests
374
- end
375
- end
376
- ```
377
-
378
- This pattern means:
379
- - **Regular page load**: Full page with header, content, footer
380
- - **Turbo-frame request**: Only `<turbo-frame id="...">` with content inside
381
-
382
- All pages automatically inherit this behavior. No special handling needed for modals or frame navigation.
383
-
384
- ## Custom Components
385
-
386
- ### Creating a Phlex Component
387
-
388
- ```ruby
389
- # app/components/post_card_component.rb
390
- class PostCardComponent < Plutonium::UI::Component::Base
391
- def initialize(post:)
392
- @post = post
393
- end
394
-
395
- def view_template
396
- div(class: "bg-white rounded-lg shadow p-4") {
397
- h3(class: "font-bold") { @post.title }
398
- p(class: "text-gray-600 mt-2") { @post.excerpt }
399
-
400
- div(class: "mt-4 flex justify-between items-center") {
401
- span(class: "text-sm text-gray-500") { @post.published_at&.strftime("%B %d, %Y") }
402
- a(href: resource_url_for(@post), class: "text-blue-600") { "Read more" }
403
- }
404
- }
405
- end
406
- end
407
- ```
408
-
409
- ### Using in Definitions
410
-
411
- Reference components in field definitions:
412
-
413
- ```ruby
414
- class PostDefinition < ResourceDefinition
415
- # Custom display component
416
- display :status, as: StatusBadgeComponent
417
-
418
- # Custom input component
419
- input :color, as: ColorPickerComponent
420
-
421
- # Block with component
422
- display :metrics do |field|
423
- MetricsChartComponent.new(data: field.value)
424
- end
425
- end
426
- ```
427
-
428
- ## Layout Customization
429
-
430
- ### Eject Layout
431
-
432
- Copy the layout template to your project:
433
-
434
- ```bash
435
- rails generate pu:eject:layout
436
- ```
437
-
438
- This copies `layouts/resource.html.erb` to your portal.
439
-
440
- ### Custom Layout Class
441
-
442
- Override the Phlex layout:
443
-
444
- ```ruby
445
- # packages/admin_portal/app/views/layouts/admin_portal/resource_layout.rb
446
- module AdminPortal
447
- class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
448
- private
449
-
450
- # Custom body classes
451
- def body_attributes
452
- {class: "antialiased bg-slate-100 dark:bg-slate-900"}
453
- end
454
-
455
- # Add custom header content
456
- def render_before_main
457
- super
458
- render AnnouncementBanner.new if Announcement.active.any?
459
- end
460
-
461
- # Custom scripts
462
- def render_body_scripts
463
- super
464
- script(src: "/custom-analytics.js")
465
- end
466
- end
467
- end
468
- ```
469
-
470
- ### Layout Hooks
471
-
472
- | Hook | Purpose |
473
- |------|---------|
474
- | `render_before_main` | Before main content area |
475
- | `render_after_main` | After main (modals, etc.) |
476
- | `render_before_content` | Inside main, before content |
477
- | `render_after_content` | Inside main, after content |
478
- | `render_flash` | Flash messages |
479
- | `render_head` | HTML head section |
480
- | `render_title` | Page title tag |
481
- | `render_metatags` | Meta tags |
482
- | `render_assets` | CSS/JS assets |
483
- | `render_body_scripts` | Scripts at end of body |
484
-
485
- ## Available Context
486
-
487
- Both ERB views and Phlex components have access to the same context.
488
-
489
- ### Resource Methods
490
-
491
- | Method | Description |
492
- |--------|-------------|
493
- | `resource_class` | The model class (e.g., `Post`) |
494
- | `resource_record!` | Current record (raises if not found) |
495
- | `resource_record?` | Current record (nil if not found) |
496
- | `current_parent` | Parent record for nested routes |
497
- | `current_scoped_entity` | Entity for multi-tenant portals |
498
-
499
- ### Definition & Policy
500
-
501
- | Method | Description |
502
- |--------|-------------|
503
- | `current_definition` | Definition instance for current resource |
504
- | `current_policy` | Policy instance for current record |
505
- | `current_authorized_scope` | Scoped collection user can access |
506
-
507
- ### Authentication
508
-
509
- | Method | Description |
510
- |--------|-------------|
511
- | `current_user` | Authenticated user (if using Rodauth) |
512
-
513
- ### URL Helpers
514
-
515
- | Method | Description |
516
- |--------|-------------|
517
- | `resource_url_for(record)` | URL for a record |
518
- | `resource_url_for(record, action: :edit)` | Action URL for record |
519
- | `resource_url_for(Model)` | Index URL for model |
520
- | `resource_url_for(Model, action: :new)` | New URL for model |
521
- | `resource_url_for(record, parent: parent)` | Nested resource URL |
522
-
523
- ### Display Helpers
524
-
525
- | Method | Description |
526
- |--------|-------------|
527
- | `display_name_of(record)` | Human-readable name for record |
528
- | `resource_name(klass)` | Singular model name |
529
- | `resource_name_plural(klass)` | Plural model name |
530
-
531
- ### In Phlex Components
532
-
533
- ```ruby
534
- class MyComponent < Plutonium::UI::Component::Base
535
- def view_template
536
- # All the above methods work directly
537
- current_user
538
- resource_record!
539
- resource_url_for(@post)
540
-
541
- # Rails helpers via helpers proxy
542
- helpers.link_to(...)
543
- helpers.image_tag(...)
544
- helpers.number_to_currency(...)
545
- end
546
- end
547
- ```
548
-
549
- ### In ERB Views
550
-
551
- ```erb
552
- <%# All methods available directly %>
553
- <%= resource_record!.title %>
554
- <%= current_user.name %>
555
- <%= link_to "Edit", resource_url_for(resource_record!, action: :edit) %>
556
-
557
- <%# Render Phlex components %>
558
- <%= render current_definition.show_page_class.new %>
559
- <%= render MyCustomComponent.new(post: resource_record!) %>
560
- ```
561
-
562
- ## Portal-Specific Views
563
-
564
- Each portal can have its own view overrides:
565
-
566
- ```ruby
567
- # Base definition
568
- class PostDefinition < ResourceDefinition
569
- class ShowPage < ShowPage
570
- # Default behavior
571
- end
572
- end
573
-
574
- # Admin portal override
575
- class AdminPortal::PostDefinition < ::PostDefinition
576
- class ShowPage < ShowPage # Inherits from ::PostDefinition::ShowPage
577
- def render_after_content
578
- super
579
- render AdminOnlySection.new(post: object)
580
- end
581
- end
582
- end
583
- ```
584
-
585
- ## Related Skills
586
-
587
- - `plutonium-forms` - Custom form templates and field builders
588
- - `plutonium-assets` - TailwindCSS and component theming
589
- - `plutonium-definition` - Field/input/display configuration
590
- - `plutonium-definition` - Action buttons and interactions
591
- - `plutonium-controller` - Presentation hooks (`present_parent?`, etc.)
592
- - `plutonium-portal` - Portal-specific customization