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,508 @@
1
+ # Definition
2
+
3
+ Definitions configure **how** a resource is rendered and interacted with — which fields appear, how they render, what page chrome looks like. Auto-detection from the model handles the defaults; declare only what you're overriding.
4
+
5
+ For search/filters/scopes/sorting see [Query](./query). For custom actions see [Actions](./actions).
6
+
7
+ ## 🚨 Critical
8
+
9
+ - **Don't declare for completeness.** A `field :title` matching what Plutonium auto-detects is dead code. Declare ONLY when you need a different type, an option (`hint:`, `placeholder:`, `wrapper:`, `class:`), a `condition:`, a block, or a custom component.
10
+ - **Use `condition:` for UI state, the policy for authorization.** `condition: -> { object.published? }` is fine. "Only admins see this field" belongs in `permitted_attributes_for_*`.
11
+ - **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (see [Behavior › Policy](/reference/behavior/policies)).
12
+ - **`has_cents` fields use the virtual name** (`field :price`), never `:price_cents`.
13
+ - **Nested inputs need `accepts_nested_attributes_for` AND `inverse_of:` on the child's `belongs_to`** — without `inverse_of:`, validation fails with "Parent must exist" because the parent isn't saved yet.
14
+
15
+ ## File location
16
+
17
+ ```
18
+ app/definitions/post_definition.rb
19
+ packages/blogging/app/definitions/blogging/post_definition.rb
20
+ ```
21
+
22
+ | Model | Definition |
23
+ |---|---|
24
+ | `Post` | `PostDefinition` |
25
+ | `Blogging::Post` | `Blogging::PostDefinition` |
26
+
27
+ ## Hierarchy
28
+
29
+ Definitions inherit from each other so portals can override:
30
+
31
+ ```ruby
32
+ # app/definitions/resource_definition.rb (installed once)
33
+ class ResourceDefinition < Plutonium::Resource::Definition
34
+ action :archive, interaction: ArchiveInteraction, color: :danger, position: 1000
35
+ end
36
+
37
+ # app/definitions/post_definition.rb (scaffolded)
38
+ class PostDefinition < ResourceDefinition
39
+ scope :published
40
+ input :content, as: :markdown
41
+ end
42
+
43
+ # packages/admin_portal/app/definitions/admin_portal/post_definition.rb (per-portal)
44
+ class AdminPortal::PostDefinition < ::PostDefinition
45
+ input :internal_notes, as: :text # admins see this; customers don't
46
+ scope :pending_review
47
+ end
48
+ ```
49
+
50
+ ## Auto-detection
51
+
52
+ Empty definition = everything auto-detected from the model:
53
+
54
+ ```ruby
55
+ class PostDefinition < Plutonium::Resource::Definition
56
+ end
57
+ ```
58
+
59
+ Plutonium detects, from the model:
60
+
61
+ - Database columns (string, text, integer, boolean, datetime, etc.)
62
+ - Associations (`belongs_to`, `has_many`, `has_one`)
63
+ - ActiveStorage attachments (`has_one_attached`, `has_many_attached`)
64
+ - Enums
65
+ - Virtual attributes (when they have accessor methods)
66
+
67
+ | Database type | Detected as |
68
+ |---|---|
69
+ | `string`, `text` | `:string` / `:text` |
70
+ | `integer`, `bigint` | `:integer` |
71
+ | `float`, `decimal` | `:float` / `:decimal` |
72
+ | `boolean` | `:boolean` |
73
+ | `date`, `datetime`, `time` | `:date` / `:datetime` / `:time` |
74
+ | `json`, `jsonb` | `:json` |
75
+
76
+ Validations on the model inform the UI too: `validates :title, presence: true` → required field; `validates :role, inclusion: { in: [...] }` → select choices.
77
+
78
+ ## Core methods
79
+
80
+ | Method | Applies to | Use when |
81
+ |---|---|---|
82
+ | `field` | Forms + Show + Table | Universal type override |
83
+ | `input` | Forms only | Form-specific options |
84
+ | `display` | Show page only | Display-specific options |
85
+ | `column` | Table only | Table-specific options |
86
+
87
+ ```ruby
88
+ class PostDefinition < Plutonium::Resource::Definition
89
+ field :content, as: :markdown # everywhere
90
+ input :title, hint: "Be descriptive"
91
+ display :content, wrapper: {class: "col-span-full"}
92
+ column :view_count, align: :end
93
+ end
94
+ ```
95
+
96
+ ## Available field types
97
+
98
+ ### Input types (forms)
99
+
100
+ | Category | Types |
101
+ |---|---|
102
+ | Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
103
+ | Rich text | `:markdown` (EasyMDE editor) |
104
+ | Numeric | `:number`, `:integer`, `:decimal`, `:range` |
105
+ | Boolean | `:boolean` |
106
+ | Date/Time | `:date`, `:time`, `:datetime` |
107
+ | Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
108
+ | Files | `:file`, `:uppy`, `:attachment` |
109
+ | Associations | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
110
+ | Special | `:hidden`, `:color`, `:phone` |
111
+
112
+ ### Display types (show / index)
113
+
114
+ `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
115
+
116
+ ## Field options
117
+
118
+ ```ruby
119
+ input :title,
120
+ # Wrapper-level (label, hint, placeholder, description)
121
+ label: "Custom Label",
122
+ hint: "Help text",
123
+ placeholder: "Enter value",
124
+ description: "Shown on the show page",
125
+
126
+ # Tag-level (HTML attributes)
127
+ class: "custom-class",
128
+ data: {controller: "custom"},
129
+ required: true,
130
+ readonly: true,
131
+ disabled: true,
132
+
133
+ # Layout
134
+ wrapper: {class: "col-span-full"}
135
+ ```
136
+
137
+ ## Select / choices
138
+
139
+ ### Static
140
+
141
+ ```ruby
142
+ input :category, as: :select, choices: %w[Tech Business Lifestyle]
143
+ input :status, as: :select, choices: Post.statuses.keys
144
+ ```
145
+
146
+ ### Dynamic (block required)
147
+
148
+ ```ruby
149
+ input :author do |f|
150
+ f.select_tag choices: User.active.pluck(:name, :id)
151
+ end
152
+
153
+ # With context: current_user, current_parent, object, request, params all available
154
+ input :team_members do |f|
155
+ f.select_tag choices: current_user.organization.users.pluck(:name, :id)
156
+ end
157
+
158
+ # Based on object state
159
+ input :related_posts do |f|
160
+ choices = object.persisted? ?
161
+ Post.where.not(id: object.id).published.pluck(:title, :id) : []
162
+ f.select_tag choices: choices
163
+ end
164
+ ```
165
+
166
+ ## Conditional rendering
167
+
168
+ ```ruby
169
+ display :published_at, condition: -> { object.published? }
170
+ display :rejection_reason, condition: -> { object.rejected? }
171
+ field :debug_info, condition: -> { Rails.env.development? }
172
+ ```
173
+
174
+ ::: warning UI state, not authorization
175
+ `condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_*` — see [Behavior › Policy](/reference/behavior/policies).
176
+ :::
177
+
178
+ ## Dynamic forms (`pre_submit`)
179
+
180
+ A field with `pre_submit: true` triggers a server re-render on change, re-evaluating `condition:` procs. Use for cascading or context-dependent forms.
181
+
182
+ ```ruby
183
+ class QuestionDefinition < ResourceDefinition
184
+ # Trigger field
185
+ input :question_type, as: :select,
186
+ choices: %w[text choice scale],
187
+ pre_submit: true
188
+
189
+ # Dependents — no `as:` needed when the model column type matches
190
+ input :max_length, condition: -> { object.question_type == "text" }
191
+ input :choices, condition: -> { object.question_type == "choice" }
192
+ input :min_value, condition: -> { object.question_type == "scale" }
193
+ end
194
+ ```
195
+
196
+ How it works:
197
+
198
+ 1. User changes a `pre_submit: true` field.
199
+ 2. Form submits via Turbo (no page reload).
200
+ 3. Server re-renders the form with updated `object` state.
201
+ 4. `condition:` procs are re-evaluated. Newly visible fields appear; newly hidden ones disappear.
202
+
203
+ Tips:
204
+
205
+ - Only add `pre_submit:` to fields that gate visibility of others.
206
+ - Avoid on frequently-changed fields (every keystroke = submit).
207
+
208
+ ## Custom rendering
209
+
210
+ ### Block syntax
211
+
212
+ **Display (any return value, can be a component):**
213
+
214
+ ```ruby
215
+ display :status do |field|
216
+ StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
217
+ end
218
+
219
+ display :metrics do |field|
220
+ field.value.present? ?
221
+ MetricsChartComponent.new(data: field.value) :
222
+ EmptyStateComponent.new(message: "No metrics")
223
+ end
224
+ ```
225
+
226
+ **Input (must call form builder methods):**
227
+
228
+ ```ruby
229
+ input :birth_date do |f|
230
+ case object.age_category
231
+ when 'adult' then f.date_tag(min: 18.years.ago.to_date)
232
+ when 'minor' then f.date_tag(max: 18.years.ago.to_date)
233
+ else f.date_tag
234
+ end
235
+ end
236
+ ```
237
+
238
+ ### `phlexi_tag` for declarative custom display
239
+
240
+ `with:` takes either a Phlex component class OR a proc whose body is **rendered inside a Phlex context** — HTML tag methods (`span`, `div`, `a`) and Tailwind classes are first-class. The proc receives `(value, attrs)`.
241
+
242
+ ```ruby
243
+ # Component — preferred for anything reusable
244
+ display :status, as: :phlexi_tag, with: StatusBadgeComponent
245
+
246
+ # Inline proc — `span` here is a Phlex tag method, not a Rails helper
247
+ display :priority, as: :phlexi_tag, with: ->(value, attrs) {
248
+ case value
249
+ when 'high' then span(class: "badge badge-danger") { "High" }
250
+ when 'medium' then span(class: "badge badge-warning") { "Medium" }
251
+ else span(class: "badge badge-info") { "Low" }
252
+ end
253
+ }
254
+ ```
255
+
256
+ See [UI › Components](/reference/ui/components) for writing reusable Phlex components.
257
+
258
+ ### Custom component class
259
+
260
+ ```ruby
261
+ input :color_picker, as: ColorPickerComponent
262
+ display :chart, as: ChartComponent
263
+ ```
264
+
265
+ ## Column options
266
+
267
+ ```ruby
268
+ column :title, align: :start # default
269
+ column :status, align: :center
270
+ column :amount, align: :end
271
+ ```
272
+
273
+ ### Value formatting
274
+
275
+ `formatter:` receives just the value. Use a block when you need the full record.
276
+
277
+ ```ruby
278
+ column :description, formatter: ->(value) { value&.truncate(30) }
279
+ column :price, formatter: ->(value) { "$%.2f" % value if value }
280
+ column :status, formatter: ->(value) { value&.humanize&.upcase }
281
+
282
+ # Block — full record access
283
+ column :full_name do |record|
284
+ "#{record.first_name} #{record.last_name}"
285
+ end
286
+ ```
287
+
288
+ ## Nested inputs
289
+
290
+ Inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
291
+
292
+ ```ruby
293
+ class Post < ResourceRecord
294
+ has_many :comments
295
+ has_one :metadata
296
+
297
+ accepts_nested_attributes_for :comments, allow_destroy: true, limit: 10
298
+ accepts_nested_attributes_for :metadata, update_only: true
299
+ end
300
+
301
+ class PostDefinition < ResourceDefinition
302
+ nested_input :comments do |n|
303
+ n.input :body, as: :text
304
+ n.input :author_name
305
+ end
306
+
307
+ # Or use another definition
308
+ nested_input :metadata, using: PostMetadataDefinition, fields: %i[seo_title seo_description]
309
+ end
310
+ ```
311
+
312
+ ### Options
313
+
314
+ | Option | Description |
315
+ |---|---|
316
+ | `limit` | Max records (auto-detected from model; default 10) |
317
+ | `allow_destroy` | Show delete checkbox (auto-detected) |
318
+ | `update_only` | Hide "Add" button — only edit existing |
319
+ | `description` | Help text above the section |
320
+ | `condition` | Proc to show/hide |
321
+ | `using` | Another Definition class |
322
+ | `fields` | Subset of fields from the referenced definition |
323
+
324
+ ### Gotchas
325
+
326
+ - **`inverse_of:` is required** on the child's `belongs_to`:
327
+ ```ruby
328
+ class Comment < ResourceRecord
329
+ belongs_to :post, inverse_of: :comments # ← without this, validation fails with "Parent must exist"
330
+ end
331
+ ```
332
+ - **Don't put `*_attributes` hashes in the policy.** Plutonium extracts nested params from the form definition, not the policy. The policy permits just the association name (`:variants`); `nested_input :variants` handles the rest. Adding `{variants_attributes: [...]}` to `permitted_attributes_for_create` renders as a literal text input. See [Behavior › Policy](/reference/behavior/policies).
333
+ - **`update_only: true` hides the Add button** — for `has_one` and "settings"-style associations.
334
+ - **Custom class names** — use `class_name:` in the model AND `using:` in the definition.
335
+
336
+ ## File uploads
337
+
338
+ ```ruby
339
+ input :avatar, as: :file
340
+ input :avatar, as: :uppy
341
+
342
+ input :documents, as: :file, multiple: true
343
+ input :documents, as: :uppy,
344
+ allowed_file_types: %w[.pdf .doc],
345
+ max_file_size: 5.megabytes
346
+ ```
347
+
348
+ ## Context in blocks
349
+
350
+ Inside `condition:` procs and block-form `input`/`display`:
351
+
352
+ - `object` — the record being edited or displayed
353
+ - `current_user`
354
+ - `current_parent` — parent record for nested resources
355
+ - `request`, `params`
356
+ - All view helpers (via the same context as controllers)
357
+
358
+ ## Runtime customization hooks
359
+
360
+ Override these methods for dynamic per-request configuration:
361
+
362
+ ```ruby
363
+ class PostDefinition < ResourceDefinition
364
+ def customize_fields # add/modify fields
365
+ def customize_inputs
366
+ def customize_displays
367
+ def customize_columns
368
+ def customize_filters
369
+ def customize_scopes
370
+ def customize_sorts
371
+ def customize_actions
372
+ end
373
+ ```
374
+
375
+ Useful when configuration depends on `current_user`, the environment, or feature flags.
376
+
377
+ ## Page configuration
378
+
379
+ ### Titles and descriptions
380
+
381
+ ```ruby
382
+ class PostDefinition < ResourceDefinition
383
+ index_page_title "All Posts"
384
+ index_page_description "Manage your blog posts"
385
+
386
+ new_page_title "Create Post"
387
+ show_page_title -> { current_record!.title } # dynamic
388
+ edit_page_title -> { "Edit: #{current_record!.title}" }
389
+ end
390
+ ```
391
+
392
+ ### Breadcrumbs
393
+
394
+ ```ruby
395
+ breadcrumbs true # global default
396
+ index_page_breadcrumbs false # per-page override
397
+ show_page_breadcrumbs true
398
+ new_page_breadcrumbs true
399
+ edit_page_breadcrumbs true
400
+ interactive_action_page_breadcrumbs true
401
+ ```
402
+
403
+ ### Form configuration
404
+
405
+ ```ruby
406
+ class PostDefinition < ResourceDefinition
407
+ # "Save and add another" / "Update and continue editing"
408
+ # nil (default) — auto: hidden for singular resources, shown for plural
409
+ # true — always show
410
+ # false — always hide
411
+ submit_and_continue false
412
+
413
+ # How :new / :edit render
414
+ # :slideover (default) — slide-in panel from the right
415
+ # :centered — centered dialog
416
+ # false — full standalone pages (no modal)
417
+ modal :centered
418
+ end
419
+ ```
420
+
421
+ `modal:` only affects framework `:new`/`:edit` actions. Custom interactive actions have their own per-action `modal:` option — see [Actions](./actions).
422
+
423
+ ## Metadata panel (show page)
424
+
425
+ A right-side aside on the show page rendering label/value rows. Keeps the main card focused on substance; chrome (timestamps, ownership, system flags) lives in the aside.
426
+
427
+ ```ruby
428
+ class PostDefinition < ResourceDefinition
429
+ metadata :author, :state, :created_at, :updated_at
430
+ end
431
+ ```
432
+
433
+ Behavior:
434
+
435
+ - **Opt-in.** No `metadata` call → show page renders full-width.
436
+ - **Policy-aware.** Fields intersect with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
437
+ - **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
438
+ - **Responsive.** Side-by-side at `lg+`, stacked below.
439
+ - **Formatting inherits.** Field labels and `as:` declarations propagate — the metadata panel uses the same field-rendering machinery as the main card.
440
+
441
+ ## Index views (Table & Grid)
442
+
443
+ Resources can offer both Table and Grid views. The user switches via the toolbar; the choice persists per-resource via cookie.
444
+
445
+ ```ruby
446
+ class UserDefinition < ResourceDefinition
447
+ # No `index_views :table, :grid` needed — declaring grid_fields auto-enables :grid.
448
+ grid_fields(
449
+ image: :avatar, # ActiveStorage attachment, Shrine, or URL
450
+ header: :name, # falls back to to_label
451
+ subheader: :email,
452
+ body: :bio,
453
+ meta: [:role, :status], # rendered as small pills
454
+ footer: :last_seen_at # falls back to :created_at
455
+ )
456
+
457
+ default_index_view :grid # optional — initial view when no cookie
458
+ grid_layout :media # :compact (default) or :media
459
+ grid_columns 3 # pin lg+ cols; default is 1/2/3/4 responsive
460
+ end
461
+ ```
462
+
463
+ | Method | Purpose |
464
+ |---|---|
465
+ | `index_views :table, :grid` | Which views are available. Default `[:table]`. Usually unnecessary. |
466
+ | `default_index_view :grid` | Initial view when no cookie. Falls back to first available view. |
467
+ | `grid_fields(...)` | Map card slots to fields. **Implicitly enables `:grid`**. |
468
+ | `grid_layout :compact \| :media` | `:compact` puts image left of content; `:media` stacks image full-width on top. |
469
+ | `grid_columns N` | Override responsive column count on `lg+`. Default is 1/2/3/4 at sm/md/lg/xl. |
470
+
471
+ Grid slots — `:image`, `:header`, `:subheader`, `:body`, `:meta`, `:footer` — are all optional. `:meta` accepts an array; the rest are single fields. Slots pointing at policy-blocked fields collapse silently.
472
+
473
+ Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
474
+
475
+ ## Custom page classes
476
+
477
+ Override the rendered page entirely — full control via Phlex:
478
+
479
+ ```ruby
480
+ class PostDefinition < ResourceDefinition
481
+ class IndexPage < IndexPage # inherits the parent's nested class
482
+ def view_template(&block)
483
+ div(class: "custom-header") { h1 { "Custom" } }
484
+ super(&block)
485
+ end
486
+ end
487
+
488
+ class Form < Form
489
+ def form_template
490
+ div(class: "grid grid-cols-2") do
491
+ render field(:title).input_tag
492
+ render field(:content).easymde_tag
493
+ end
494
+ render_actions
495
+ end
496
+ end
497
+ end
498
+ ```
499
+
500
+ See [UI › Pages](/reference/ui/pages) and [UI › Forms](/reference/ui/forms) for the full page-class surface.
501
+
502
+ ## Related
503
+
504
+ - [Query](./query) — search, filters, scopes, sorting
505
+ - [Actions](./actions) — custom + bulk actions
506
+ - [Behavior › Policy](/reference/behavior/policies) — `permitted_attributes_for_*`, authorization
507
+ - [UI › Forms](/reference/ui/forms) — field builder, association inputs, theming
508
+ - [UI › Pages](/reference/ui/pages) — custom page classes
@@ -0,0 +1,50 @@
1
+ # Resource Reference
2
+
3
+ A **resource** is the unit Plutonium gives you full CRUD for — list, show, create, edit, delete — automatically. It's four cooperating layers, plus an optional fifth for business logic.
4
+
5
+ | Layer | File | What it controls |
6
+ |---|---|---|
7
+ | [Model](./model) | `app/models/post.rb` | Data, validations, associations |
8
+ | [Definition](./definition) | `app/definitions/post_definition.rb` | UI — which fields, how they render, what actions exist |
9
+ | Policy | `app/policies/post_policy.rb` | Authorization — see [Behavior › Policy](/reference/behavior/policies) |
10
+ | Controller | `app/controllers/posts_controller.rb` | Request handling — see [Behavior › Controller](/reference/behavior/controllers) |
11
+ | Interaction *(optional)* | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions — see [Behavior › Interaction](/reference/behavior/interactions) |
12
+
13
+ ## How a resource is born
14
+
15
+ ```bash
16
+ rails g pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
17
+ rails db:migrate
18
+ rails g pu:res:conn Post --dest=admin_portal
19
+ ```
20
+
21
+ That single scaffold gives you a working model + migration + controller + policy + definition. `pu:res:conn` adds it to a portal. See [App › Generators](/reference/app/generators) for the full generator catalog.
22
+
23
+ ## Auto-detection is the default
24
+
25
+ Plutonium reads your model and renders every attribute automatically — type, label, form widget, display formatter, table column. You only declare overrides:
26
+
27
+ ```ruby
28
+ class PostDefinition < Plutonium::Resource::Definition
29
+ # No field/input/display/column needed unless you're overriding the default.
30
+ field :content, as: :markdown # override: render as markdown editor + viewer
31
+ input :title, hint: "Be descriptive"
32
+ end
33
+ ```
34
+
35
+ ::: warning Don't declare for completeness
36
+ A `field :title` with no options that matches what Plutonium would auto-detect is **dead code** — it does nothing and clutters the file. Declare ONLY when you need a different type, an option, a `condition:`, a block, or a custom component.
37
+ :::
38
+
39
+ ## Sub-pages
40
+
41
+ - [Model](./model) — `Plutonium::Resource::Record`, `has_cents`, SGID, custom routing, labeling
42
+ - [Definition](./definition) — fields, inputs, displays, columns, page chrome, metadata panel, index views
43
+ - [Query](./query) — search, filters, scopes, sorting
44
+ - [Actions](./actions) — custom actions, bulk actions, interaction integration
45
+
46
+ ## Related
47
+
48
+ - [Guides › Adding Resources](/guides/adding-resources) — task recipe
49
+ - [App › Generators](/reference/app/generators) — `pu:res:scaffold` / `pu:res:conn` reference
50
+ - [Tenancy](/reference/tenancy/) — multi-tenant scoping