plutonium 0.65.0 → 0.67.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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +43 -43
  3. data/.claude/skills/plutonium-app/SKILL.md +101 -59
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
  5. data/.claude/skills/plutonium-auth/SKILL.md +119 -53
  6. data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
  7. data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
  8. data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
  9. data/.claude/skills/plutonium-resource/SKILL.md +158 -141
  10. data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
  11. data/.claude/skills/plutonium-testing/SKILL.md +130 -33
  12. data/.claude/skills/plutonium-ui/SKILL.md +161 -98
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +43 -0
  15. data/README.md +9 -9
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/app/assets/plutonium.js +56 -3
  19. data/app/assets/plutonium.js.map +4 -4
  20. data/app/assets/plutonium.min.js +32 -32
  21. data/app/assets/plutonium.min.js.map +4 -4
  22. data/config/brakeman.ignore +1 -1
  23. data/config/locales/en/js.yml +4 -0
  24. data/config/locales/en/ui.yml +16 -0
  25. data/docs/.vitepress/sync-skills.mjs +6 -3
  26. data/docs/blog/introducing-plutonium-dashboards.md +4 -5
  27. data/docs/blog/introducing-plutonium-i18n.md +4 -5
  28. data/docs/getting-started/installation.md +5 -5
  29. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  30. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  31. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  32. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  33. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  34. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  35. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  36. data/docs/getting-started/tutorial/index.md +1 -1
  37. data/docs/guides/adding-resources.md +10 -7
  38. data/docs/guides/authentication.md +25 -25
  39. data/docs/guides/authorization.md +24 -24
  40. data/docs/guides/creating-packages.md +17 -17
  41. data/docs/guides/custom-actions.md +32 -32
  42. data/docs/guides/customizing-ui.md +29 -26
  43. data/docs/guides/dashboards.md +1 -1
  44. data/docs/guides/index.md +3 -3
  45. data/docs/guides/kanban.md +55 -55
  46. data/docs/guides/multi-tenancy.md +35 -22
  47. data/docs/guides/nested-resources.md +21 -21
  48. data/docs/guides/performance.md +3 -3
  49. data/docs/guides/search-filtering.md +13 -13
  50. data/docs/guides/testing.md +16 -12
  51. data/docs/guides/theming.md +32 -17
  52. data/docs/guides/troubleshooting.md +2 -2
  53. data/docs/guides/user-invites.md +17 -17
  54. data/docs/guides/user-profile.md +51 -24
  55. data/docs/guides/wizards.md +55 -55
  56. data/docs/reference/app/generators.md +22 -22
  57. data/docs/reference/app/index.md +15 -18
  58. data/docs/reference/app/packages.md +8 -8
  59. data/docs/reference/app/portals.md +75 -29
  60. data/docs/reference/auth/accounts.md +15 -15
  61. data/docs/reference/auth/index.md +12 -12
  62. data/docs/reference/auth/profile.md +67 -29
  63. data/docs/reference/behavior/async-interactions.md +24 -24
  64. data/docs/reference/behavior/controllers.md +28 -28
  65. data/docs/reference/behavior/index.md +5 -5
  66. data/docs/reference/behavior/interactions.md +44 -44
  67. data/docs/reference/behavior/policies.md +48 -28
  68. data/docs/reference/configuration.md +6 -6
  69. data/docs/reference/dashboard/dsl.md +2 -2
  70. data/docs/reference/dashboard/index.md +1 -1
  71. data/docs/reference/generators/lite.md +7 -7
  72. data/docs/reference/i18n.md +23 -0
  73. data/docs/reference/index.md +1 -1
  74. data/docs/reference/kanban/authorization.md +9 -9
  75. data/docs/reference/kanban/dsl.md +32 -32
  76. data/docs/reference/kanban/index.md +1 -1
  77. data/docs/reference/kanban/positioning.md +17 -15
  78. data/docs/reference/resource/actions.md +51 -51
  79. data/docs/reference/resource/definition.md +85 -80
  80. data/docs/reference/resource/export.md +6 -6
  81. data/docs/reference/resource/index.md +16 -16
  82. data/docs/reference/resource/model.md +24 -24
  83. data/docs/reference/resource/positioning.md +78 -76
  84. data/docs/reference/resource/query.md +24 -14
  85. data/docs/reference/tenancy/entity-scoping.md +65 -35
  86. data/docs/reference/tenancy/index.md +11 -11
  87. data/docs/reference/tenancy/invites.md +20 -20
  88. data/docs/reference/tenancy/nested-resources.md +13 -13
  89. data/docs/reference/testing/index.md +116 -22
  90. data/docs/reference/ui/assets.md +57 -25
  91. data/docs/reference/ui/components.md +20 -20
  92. data/docs/reference/ui/displays.md +32 -15
  93. data/docs/reference/ui/forms.md +89 -39
  94. data/docs/reference/ui/index.md +17 -15
  95. data/docs/reference/ui/layouts.md +21 -21
  96. data/docs/reference/ui/pages.md +22 -22
  97. data/docs/reference/ui/tables.md +7 -7
  98. data/docs/reference/wizard/anchoring-resume.md +33 -32
  99. data/docs/reference/wizard/dsl.md +44 -44
  100. data/docs/reference/wizard/index.md +6 -6
  101. data/docs/reference/wizard/one-time.md +18 -18
  102. data/docs/reference/wizard/registration-launch.md +32 -32
  103. data/docs/reference/wizard/storage-config.md +23 -23
  104. data/gemfiles/rails_7.gemfile +1 -0
  105. data/gemfiles/rails_7.gemfile.lock +13 -3
  106. data/gemfiles/rails_8.0.gemfile +1 -0
  107. data/gemfiles/rails_8.0.gemfile.lock +10 -1
  108. data/gemfiles/rails_8.1.gemfile +1 -0
  109. data/gemfiles/rails_8.1.gemfile.lock +10 -1
  110. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  111. data/lib/plutonium/auth/rodauth.rb +7 -0
  112. data/lib/plutonium/core/controller.rb +0 -2
  113. data/lib/plutonium/helpers/content_helper.rb +0 -3
  114. data/lib/plutonium/query/filter.rb +2 -1
  115. data/lib/plutonium/query/filters/tags.rb +36 -0
  116. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  117. data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
  118. data/lib/plutonium/ui/display/base.rb +50 -2
  119. data/lib/plutonium/ui/display/components/badge.rb +1 -1
  120. data/lib/plutonium/ui/display/components/binary.rb +20 -0
  121. data/lib/plutonium/ui/display/components/boolean.rb +1 -1
  122. data/lib/plutonium/ui/display/components/code.rb +23 -0
  123. data/lib/plutonium/ui/display/components/color.rb +14 -6
  124. data/lib/plutonium/ui/display/components/duration.rb +31 -0
  125. data/lib/plutonium/ui/display/components/file_size.rb +22 -0
  126. data/lib/plutonium/ui/display/components/key_value.rb +37 -0
  127. data/lib/plutonium/ui/display/components/list.rb +33 -0
  128. data/lib/plutonium/ui/display/components/number.rb +32 -0
  129. data/lib/plutonium/ui/display/components/progress.rb +36 -0
  130. data/lib/plutonium/ui/display/components/rating.rb +36 -0
  131. data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
  132. data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
  133. data/lib/plutonium/ui/display/components/tags.rb +18 -0
  134. data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
  135. data/lib/plutonium/ui/display/theme.rb +24 -2
  136. data/lib/plutonium/ui/form/base.rb +30 -1
  137. data/lib/plutonium/ui/form/components/binary.rb +40 -0
  138. data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
  139. data/lib/plutonium/ui/form/components/list.rb +46 -0
  140. data/lib/plutonium/ui/form/components/rating.rb +51 -0
  141. data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
  142. data/lib/plutonium/ui/form/components/tags.rb +45 -0
  143. data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
  144. data/lib/plutonium/ui/form/components/uppy.rb +1 -1
  145. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  146. data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
  147. data/lib/plutonium/ui/form/theme.rb +20 -4
  148. data/lib/plutonium/ui/grid/card.rb +13 -1
  149. data/lib/plutonium/ui/layout/base.rb +10 -1
  150. data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
  151. data/lib/plutonium/ui/options/secret_field.rb +32 -0
  152. data/lib/plutonium/ui/options/tags_field.rb +23 -0
  153. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  154. data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
  155. data/lib/plutonium/ui/table/display_theme.rb +30 -2
  156. data/lib/plutonium/version.rb +1 -1
  157. data/package.json +1 -1
  158. data/src/css/components.css +53 -10
  159. data/src/css/flatpickr.css +91 -0
  160. data/src/js/controllers/easymde_controller.js +1 -0
  161. data/src/js/controllers/register_controllers.js +2 -0
  162. data/src/js/controllers/slim_select_controller.js +32 -3
  163. data/src/js/controllers/timeago_controller.js +40 -0
  164. metadata +25 -2
@@ -1,6 +1,6 @@
1
1
  # Definition
2
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.
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
4
 
5
5
  For search/filters/scopes/sorting see [Query](./query). For custom actions see [Actions](./actions).
6
6
 
@@ -10,7 +10,7 @@ For search/filters/scopes/sorting see [Query](./query). For custom actions see [
10
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
11
  - **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (see [Behavior › Policy](/reference/behavior/policies)).
12
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.
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
14
 
15
15
  ## File location
16
16
 
@@ -72,6 +72,7 @@ Plutonium detects, from the model:
72
72
  | `boolean` | `:boolean` |
73
73
  | `date`, `datetime`, `time` | `:date` / `:datetime` / `:time` |
74
74
  | `json`, `jsonb` | `:json` |
75
+ | `binary` | `:binary` |
75
76
 
76
77
  Validations on the model inform the UI too: `validates :title, presence: true` → required field; `validates :role, inclusion: { in: [...] }` → select choices.
77
78
 
@@ -99,28 +100,32 @@ end
99
100
 
100
101
  | Category | Types |
101
102
  |---|---|
102
- | Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
103
- | Rich text | `:markdown` (EasyMDE editor) |
104
- | Numeric | `:number`, `:integer`, `:decimal`, `:range` |
105
- | Boolean | `:toggle` / `:switch` (switch — **default** for boolean columns), `:boolean` (plain checkbox) |
103
+ | Text | `:string`, `:text`, `:email`, `:url`, `:phone`, `:password` (alias `:secret`) |
104
+ | Rich text | `:rich_text` (Action Text editor: Trix, or Lexxy once installed; **default** for `has_rich_text`), `:markdown` (EasyMDE) |
105
+ | Numeric | `:number`, `:integer`, `:decimal`, `:range`, `:rating` (star picker, `max:`) |
106
+ | Boolean | `:toggle` / `:switch` (switch: **default** for boolean columns), `:boolean` (plain checkbox) |
106
107
  | Date/Time | `:date`, `:time`, `:datetime` |
107
- | Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
108
- | Files | `:file`, `:uppy`, `:attachment` |
108
+ | Selection | `:select`, `:slim_select`, `:collection_radio_buttons`, `:collection_checkboxes` |
109
+ | Lists | `:list` (chip input for array values; `suggestions:`, `addable:`, `limit:`), `:tags` (no_fly_list `has_tags` contexts, **auto-detected**) |
110
+ | Files | `:file`, `:uppy`, `:attachment` (Active Storage), `:binary` (bytes in a `binary` column) |
109
111
  | Associations | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
110
- | Special | `:hidden`, `:color`, `:phone` |
112
+ | Special | `:hidden`, `:color`, `:json`, `:key_value` |
111
113
 
112
114
  ### Display types (show / index)
113
115
 
114
- `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`
116
+ `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`, `:key_value`, `:binary`, `:list`, `:tags`, `:rich_text`, `:relative_time`, `:code`, `:duration`, `:file_size`, `:progress`, `:rating`
115
117
 
116
118
  #### Auto-inferred display formatting
117
119
 
118
- These render automatically — declare an `as:` only to override or pass options:
120
+ These render automatically; declare an `as:` only to override or pass options:
119
121
 
120
122
  | Column | Renders as | Notes |
121
123
  |---|---|---|
122
124
  | `boolean` | Yes/No pill (`:boolean`) | green "Yes" / neutral "No"; override with `true_label:` / `false_label:` |
123
125
  | `enum` | status badge (`:badge`) | known statuses auto-colored; unknown values get a stable decorative color; override per-value with `colors:` |
126
+ | `binary` | size (`:binary`) | `Binary data (12 KB)`, never the bytes |
127
+ | `has_rich_text` | HTML (`:rich_text`) | rendered and sanitized by Action Text |
128
+ | no_fly_list `has_tags` | chips (`:tags`) | declare by context name (`:labels`), not `labels_list` |
124
129
  | `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals; symbol from `unit:` on `has_cents` (model-wide) or per-display, else `config.default_currency_unit` / the i18n default (see below) |
125
130
 
126
131
  ```ruby
@@ -132,11 +137,11 @@ display :active, as: :boolean, true_label: "Live", false_label: "Off"
132
137
  **Currency symbol.** The `unit:` can be set on the model's `has_cents` declaration
133
138
  (`has_cents :price_cents, unit: "£"`, or `unit: :currency_symbol` to read a method
134
139
  off the record for per-row currencies). That model-level unit is used everywhere the
135
- value renders as currency — the show page, tables, **and grid/kanban cards**. A
140
+ value renders as currency: the show page, tables, **and grid/kanban cards**. A
136
141
  per-display `unit:` overrides it for that one display; `unit: false` explicitly
137
142
  renders no symbol. When neither is set, currency falls back to
138
143
  `Plutonium.configuration.default_currency_unit` (default: the i18n
139
- `number.currency.format.unit` if the locale defines it — `$` in `en` — else no symbol).
144
+ `number.currency.format.unit` if the locale defines it, `$` in `en`, else no symbol).
140
145
 
141
146
  ## Field options
142
147
 
@@ -206,22 +211,22 @@ Keep a `field` condition to context that exists on every surface (`Rails.env`, a
206
211
  feature flag, `current_user`) and put record checks on `input`, `display` or `column`.
207
212
 
208
213
  ::: warning UI state, not authorization
209
- `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).
214
+ `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).
210
215
  :::
211
216
 
212
217
  ## Options that vary per render
213
218
 
214
- Any option may be a **proc**, resolved on every render rather than frozen when the class loads. This holds across the whole form DSL — `field`, `input`, `section`/`ungrouped`, `structured_input` and nested inputs. Arity says **whether you want the form**:
219
+ Any option may be a **proc**, resolved on every render rather than frozen when the class loads. This holds across the whole form DSL: `field`, `input`, `section`/`ungrouped`, `structured_input` and nested inputs. Arity says **whether you want the form**:
215
220
 
216
221
  ```ruby
217
222
  input :tier, as: :select, choices: ->(form) { form.object.account.available_tiers }
218
223
  input :notes, placeholder: -> { "Updated #{Time.current.year}" }
219
224
  ```
220
225
 
221
- - **`-> { … }`** is called as-is, keeping whatever it closed over — it means what it reads like where you wrote it. Nothing rebinds `self`. That is what lets an option declared inside an interaction's `customize_inputs` reach the interaction, private helpers included: `choices: -> { reviewer_choices }`.
226
+ - **`-> { … }`** is called as-is, keeping whatever it closed over: it means what it reads like where you wrote it. Nothing rebinds `self`. That is what lets an option declared inside an interaction's `customize_inputs` reach the interaction, private helpers included: `choices: -> { reviewer_choices }`.
222
227
  - **`->(form) { … }`** is handed the form, so `object` (the record being edited), `params` and view helpers are reachable. Use it whenever the value depends on what is being rendered.
223
228
 
224
- The rule holds on wizard steps too — but there a zero-argument proc closes over an internal field recorder, so options must take the form and read the run off it: `->(form) { form.wizard.anchor.tiers }`. See [Wizard DSL › Runtime input options](/reference/wizard/dsl#runtime-input-options).
229
+ The rule holds on wizard steps too, but there a zero-argument proc closes over an internal field recorder, so options must take the form and read the run off it: `->(form) { form.wizard.anchor.tiers }`. See [Wizard DSL › Runtime input options](/reference/wizard/dsl#runtime-input-options).
225
230
 
226
231
  ### `condition:` is not an option
227
232
 
@@ -229,10 +234,10 @@ The rule holds on wizard steps too — but there a zero-argument proc closes ove
229
234
 
230
235
  | | asks | so it | receiver |
231
236
  |---|---|---|---|
232
- | an **option** (`choices:`, `label:`, `collapsed:`, …) | "what value should this have?" | may or may not care about the render — so it means what it reads like where you wrote it, and takes `form` when it does care | its own closure, or the form |
233
- | **`condition:`** | "should this render *here, now*?" | is a question about the render context by definition — there is no useful reading of it that ignores that context | always the thing doing the rendering |
237
+ | an **option** (`choices:`, `label:`, `collapsed:`, …) | "what value should this have?" | may or may not care about the render, so it means what it reads like where you wrote it, and takes `form` when it does care | its own closure, or the form |
238
+ | **`condition:`** | "should this render *here, now*?" | is a question about the render context by definition, there is no useful reading of it that ignores that context | always the thing doing the rendering |
234
239
 
235
- So `condition:` always runs **against** its context and reads it with no argument — and "its context" is whatever is rendering: the form for a field, section or nested input; the component for a `column` or `display`; the **wizard** for a step's `condition:` (evaluated in the runner to decide which steps exist, before any form is built); a condition context for an action or scope.
240
+ So `condition:` always runs **against** its context and reads it with no argument, and "its context" is whatever is rendering: the form for a field, section or nested input; the component for a `column` or `display`; the **wizard** for a step's `condition:` (evaluated in the runner to decide which steps exist, before any form is built); a condition context for an action or scope.
236
241
 
237
242
  ```ruby
238
243
  input :notes, condition: -> { object.published? } # form
@@ -253,7 +258,7 @@ class QuestionDefinition < ResourceDefinition
253
258
  choices: %w[text choice scale],
254
259
  pre_submit: true
255
260
 
256
- # Dependents — no `as:` needed when the model column type matches
261
+ # Dependents: no `as:` needed when the model column type matches
257
262
  input :max_length, condition: -> { object.question_type == "text" }
258
263
  input :choices, condition: -> { object.question_type == "choice" }
259
264
  input :min_value, condition: -> { object.question_type == "scale" }
@@ -341,7 +346,7 @@ See [UI › Components](/reference/ui/components) for writing reusable Phlex com
341
346
 
342
347
  ### Custom component class
343
348
 
344
- `as:` takes a **field component** — Plutonium constructs it as
349
+ `as:` takes a **field component**: Plutonium constructs it as
345
350
  `YourComponent.new(field, **attributes)`, so it subclasses
346
351
  `Phlexi::Form::Components::Base` (inputs) or `Phlexi::Display::Components::Base`
347
352
  (displays) and reads the value off `field`:
@@ -352,7 +357,7 @@ display :chart, as: ChartComponent
352
357
  ```
353
358
 
354
359
  A component with its own constructor (e.g. `PostCardComponent.new(post:)`) is not
355
- an `as:` candidate — it would raise `ArgumentError`. Build it in a block instead:
360
+ an `as:` candidate; it would raise `ArgumentError`. Build it in a block instead:
356
361
 
357
362
  ```ruby
358
363
  display :card do |field|
@@ -453,7 +458,7 @@ end
453
458
  |---|---|
454
459
  | `limit` | Max records (auto-detected from model; default 10) |
455
460
  | `allow_destroy` | Show delete checkbox (auto-detected) |
456
- | `update_only` | Hide "Add" button — only edit existing |
461
+ | `update_only` | Hide "Add" button: only edit existing |
457
462
  | `description` | Help text above the section |
458
463
  | `condition` | Proc to show/hide |
459
464
  | `using` | Another Definition class |
@@ -468,13 +473,13 @@ end
468
473
  end
469
474
  ```
470
475
  - **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).
471
- - **`update_only: true` hides the Add button** — for `has_one` and "settings"-style associations.
472
- - **Custom class names** — use `class_name:` in the model AND `using:` in the definition.
476
+ - **`update_only: true` hides the Add button**: for `has_one` and "settings"-style associations.
477
+ - **Custom class names**: use `class_name:` in the model AND `using:` in the definition.
473
478
 
474
479
  ## Structured inputs
475
480
 
476
481
  Classless inline fieldsets backed by a JSON/jsonb column. No model associations
477
- required — the whole sub-form is serialised into a single column as a hash
482
+ required: the whole sub-form is serialised into a single column as a hash
478
483
  (single form) or an array of hashes (repeater).
479
484
 
480
485
  ![A single structured input (Payload) and a repeater (Rows)](/images/reference/structured-inputs.png)
@@ -513,16 +518,16 @@ end
513
518
  ### Removing rows
514
519
 
515
520
  Each repeater row has a **Remove** button. Removing a row collapses it to a
516
- compact _Removed — Restore_ bar and disables its inputs, so the browser omits
521
+ compact bar (a _Removed_ label and a **Restore** button) and disables its inputs, so the browser omits
517
522
  them from the submission. The server simply rebuilds the JSON column from the
518
- rows it receives — there is no `_destroy` marker. **Restore** brings the row
523
+ rows it receives; there is no `_destroy` marker. **Restore** brings the row
519
524
  back before saving.
520
525
 
521
526
  ![A removed row collapsed to a Restore bar](/images/reference/structured-inputs-removed.png)
522
527
 
523
528
  ### Policy
524
529
 
525
- Permit the column name as a plain symbol — Plutonium handles the nested hash
530
+ Permit the column name as a plain symbol; Plutonium handles the nested hash
526
531
  params automatically:
527
532
 
528
533
  ```ruby
@@ -544,13 +549,13 @@ attribute is declared automatically; `execute` receives the value as a `Hash`
544
549
  The fields are classless render declarations, so there is nothing for Plutonium
545
550
  to attach validations to (unlike [`nested_input`](#nested-inputs), whose nested
546
551
  records run their own model validations). Whatever the form submits is stored
547
- as-is, after blank rows are dropped — **no per-field server-side validation**.
552
+ as-is, after blank rows are dropped: **no per-field server-side validation**.
548
553
  :::
549
554
 
550
555
  Specifically:
551
556
 
552
557
  - **HTML constraints are client-side only.** A field's `required:` and a
553
- select's `choices:` guide the browser but are **not** enforced on the server —
558
+ select's `choices:` guide the browser but are **not** enforced on the server:
554
559
  an API call or a crafted request can submit anything.
555
560
  - **Selects silently drop unknown values.** If a stored value is not among a
556
561
  `as: :select` field's `choices:`, the `<select>` renders **blank**, and saving
@@ -560,7 +565,7 @@ Specifically:
560
565
  `choices:` can drift. Keep `choices:` a stable superset, or use a free-text
561
566
  input, when values can change over time.
562
567
 
563
- To enforce anything, add the validation yourself — it runs server-side:
568
+ To enforce anything, add the validation yourself; it runs server-side:
564
569
 
565
570
  ```ruby
566
571
  # resource: validate the JSON column on the model
@@ -607,7 +612,7 @@ end
607
612
 
608
613
  The block is evaluated once and stored on the class. Re-declaring `form_layout` in a subclass replaces the parent layout as a unit; per-field `input` config inherits normally.
609
614
 
610
- With no `form_layout` declared the form renders unchanged as a single responsive grid — fully backwards-compatible.
615
+ With no `form_layout` declared the form renders unchanged as a single responsive grid, fully backwards-compatible.
611
616
 
612
617
  ### `section(key, *fields, **opts)`
613
618
 
@@ -615,16 +620,16 @@ Groups a set of fields under an optional heading.
615
620
 
616
621
  | Argument | Description |
617
622
  |---|---|
618
- | `key` | Symbol. `:ungrouped` is reserved — use the `ungrouped` macro instead (raises `ArgumentError` otherwise). |
623
+ | `key` | Symbol. `:ungrouped` is reserved: use the `ungrouped` macro instead (raises `ArgumentError` otherwise). |
619
624
  | `*fields` | Ordered field keys to place in this section. |
620
625
  | `label:` | Section heading. Defaults to `key.to_s.humanize` (e.g. `:shipping_address` → `"Shipping address"`). |
621
626
  | `description:` | Optional help line rendered below the heading. |
622
627
  | `collapsible:` | Boolean (default `false`). Wraps the section in a native `<details>/<summary>` (no JS). |
623
628
  | `collapsed:` | Boolean (default `false`). Initial collapsed state when `collapsible: true`. |
624
- | `columns:` | Positive Integer. Overrides the section grid column count (e.g. `columns: 2`). Omit to use the form's default responsive grid. Must be a positive Integer — any other value raises. (Literal only — not dynamic.) |
625
- | `condition:` | Lambda evaluated in the form instance context — same semantics as `input ..., condition:`. `object`, `current_user`, helpers etc. are all available. A falsey result hides the entire section and withholds its fields (they do not spill into `ungrouped`). |
629
+ | `columns:` | Positive Integer. Overrides the section grid column count (e.g. `columns: 2`). Omit to use the form's default responsive grid. Must be a positive Integer, any other value raises. (Literal only, not dynamic.) |
630
+ | `condition:` | Lambda evaluated in the form instance context: same semantics as `input ..., condition:`. `object`, `current_user`, helpers etc. are all available. A falsey result hides the entire section and withholds its fields (they do not spill into `ungrouped`). |
626
631
 
627
- Every option except `columns:` may be either a literal **or a proc** resolved at render time, following the same arity rule as every other option ([Options that vary per render](#options-that-vary-per-render)): take a `form` argument to read the render context. This makes the layout record-aware — e.g. collapse a section by default only for existing records:
632
+ Every option except `columns:` may be either a literal **or a proc** resolved at render time, following the same arity rule as every other option ([Options that vary per render](#options-that-vary-per-render)): take a `form` argument to read the render context. This makes the layout record-aware, e.g. collapse a section by default only for existing records:
628
633
 
629
634
  ```ruby
630
635
  section :advanced, :seo_title, :notes,
@@ -634,19 +639,19 @@ section :advanced, :seo_title, :notes,
634
639
  ```
635
640
 
636
641
  ::: warning Breaking change in 0.63
637
- Section options previously took a **zero-argument** proc evaluated against the form (`collapsed: -> { object.persisted? }`). They now follow the same rule as every other option, where a zero-argument proc keeps its own binding — and a `form_layout` block is evaluated against the layout builder, so `object` there is a `NameError`.
642
+ Section options previously took a **zero-argument** proc evaluated against the form (`collapsed: -> { object.persisted? }`). They now follow the same rule as every other option, where a zero-argument proc keeps its own binding, and a `form_layout` block is evaluated against the layout builder, so `object` there is a `NameError`.
638
643
 
639
644
  ```ruby
640
645
  - collapsed: -> { object.persisted? }
641
646
  + collapsed: ->(form) { form.object.persisted? }
642
647
  ```
643
648
 
644
- It fails loudly, never silently. `condition:` is unchanged — it is still evaluated against the form and still reads `object` with no argument.
649
+ It fails loudly, never silently. `condition:` is unchanged: it is still evaluated against the form and still reads `object` with no argument.
645
650
  :::
646
651
 
647
- A section that resolves to **zero fields** — every declared field filtered out by the permitted set, or no field assigned — renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
652
+ A section that resolves to **zero fields** (every declared field filtered out by the permitted set, or no field assigned) renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
648
653
 
649
- The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records — re-evaluated on `pre_submit` — can be grouped in a section without any extra wiring:
654
+ The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records, re-evaluated on `pre_submit`, can be grouped in a section without any extra wiring:
650
655
 
651
656
  ```ruby
652
657
  form_layout do
@@ -667,10 +672,10 @@ section :shipping, :address, :city, :postcode,
667
672
 
668
673
  ### `ungrouped(**opts)`
669
674
 
670
- A macro (not a `section` call) that configures the implicit bucket collecting every permitted field not claimed by any `section`. Takes **no field list** — its fields are computed at render time.
675
+ A macro (not a `section` call) that configures the implicit bucket collecting every permitted field not claimed by any `section`. Takes **no field list**; its fields are computed at render time.
671
676
 
672
677
  - Accepts the same options as `section`: `label:`, `description:`, `collapsible:`, `collapsed:`, `columns:`, `condition:`.
673
- - **Position** — where you call `ungrouped` in the block is where leftovers appear. Omit it entirely and leftovers render **last**, after every declared section, with no heading. (Declaring `ungrouped` at the very end is therefore equivalent to omitting it, except that the explicit form lets you add a `label:` and other options.)
678
+ - **Position**: where you call `ungrouped` in the block is where leftovers appear. Omit it entirely and leftovers render **last**, after every declared section, with no heading. (Declaring `ungrouped` at the very end is therefore equivalent to omitting it, except that the explicit form lets you add a `label:` and other options.)
674
679
  - Declaring `ungrouped` more than once in a single `form_layout` raises `ArgumentError`.
675
680
 
676
681
  ```ruby
@@ -688,9 +693,9 @@ end
688
693
 
689
694
  ### Layout references keys; config stays on `input`
690
695
 
691
- `form_layout` and `section` carry section-level options only. All per-field rendering config — `as:`, the field's own `label:`, `choices:`, per-field `condition:`, `pre_submit:`, blocks — remains on the `input` declaration. Layout never duplicates field config.
696
+ `form_layout` and `section` carry section-level options only. All per-field rendering config (`as:`, the field's own `label:`, `choices:`, per-field `condition:`, `pre_submit:`, blocks) remains on the `input` declaration. Layout never duplicates field config.
692
697
 
693
- This includes a field's **column span**. In a section with `columns:`, fields flow into single grid cells by default; a field that declares its own span via `wrapper: {class: "col-span-..."}` keeps it — a field-level span always wins, so you can opt one field back to full width inside a multi-column section:
698
+ This includes a field's **column span**. In a section with `columns:`, fields flow into single grid cells by default; a field that declares its own span via `wrapper: {class: "col-span-..."}` keeps it; a field-level span always wins, so you can opt one field back to full width inside a multi-column section:
694
699
 
695
700
  ```ruby
696
701
  input :notes, wrapper: {class: "col-span-full"} # spans the whole row...
@@ -702,7 +707,7 @@ end
702
707
 
703
708
  ```ruby
704
709
  class ArticleDefinition < ResourceDefinition
705
- # per-field config on input — untouched by form_layout
710
+ # per-field config on input: untouched by form_layout
706
711
  input :body, as: :markdown
707
712
  input :published_at, hint: "Leave blank to save as draft"
708
713
  input :visibility, as: :select, choices: %w[public private unlisted]
@@ -716,13 +721,13 @@ end
716
721
 
717
722
  ### Fields not in the permitted set are skipped
718
723
 
719
- A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there — a typo, or a field excluded by policy, per-action `permitted_attributes`, entity scoping, or nesting — is **silently dropped**, never an error. This lets a single `form_layout` reference conditionally-permitted fields without crashing the form in the contexts where they're filtered out. And when every field a section lists is dropped this way, the section's chrome is dropped with it (see the zero-fields note above) — so the same layout can serve a richly-permitted `edit` and a minimal `new` without leaving empty headings behind.
724
+ A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there (a typo, or a field excluded by policy, per-action `permitted_attributes`, entity scoping, or nesting) is **silently dropped**, never an error. This lets a single `form_layout` reference conditionally-permitted fields without crashing the form in the contexts where they're filtered out. And when every field a section lists is dropped this way, the section's chrome is dropped with it (see the zero-fields note above), so the same layout can serve a richly-permitted `edit` and a minimal `new` without leaving empty headings behind.
720
725
 
721
726
  ### On interactions
722
727
 
723
- `form_layout` is also available on `Plutonium::Interaction::Base`. The same DSL groups the interaction's `attribute` declarations into sections. Interaction forms (`Plutonium::UI::Form::Interaction`) pick up the layout automatically — no extra wiring needed.
728
+ `form_layout` is also available on `Plutonium::Interaction::Base`. The same DSL groups the interaction's `attribute` declarations into sections. Interaction forms (`Plutonium::UI::Form::Interaction`) pick up the layout automatically, no extra wiring needed.
724
729
 
725
- Dynamic options and `condition:` work here too, with one difference: on an interaction form the form's `object` is the **interaction instance** (not a record). For a record action, the record is `object.resource` — so e.g. `collapsed: ->(form) { form.object.resource.archived? }`, and `condition: -> { object.resource.archived? }` (which is form-evaluated, so it needs no argument).
730
+ Dynamic options and `condition:` work here too, with one difference: on an interaction form the form's `object` is the **interaction instance** (not a record). For a record action, the record is `object.resource`, so e.g. `collapsed: ->(form) { form.object.resource.archived? }`, and `condition: -> { object.resource.archived? }` (which is form-evaluated, so it needs no argument).
726
731
 
727
732
  ```ruby
728
733
  class PublishPostInteraction < Plutonium::Interaction::Base
@@ -742,7 +747,7 @@ end
742
747
 
743
748
  ## Display layout
744
749
 
745
- The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules — first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections and sections whose fields are all condition-hidden dropped entirely — applied to the show page instead of the form.
750
+ The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules: first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections and sections whose fields are all condition-hidden dropped entirely, applied to the show page instead of the form.
746
751
 
747
752
  ```ruby
748
753
  class PostDefinition < ResourceDefinition
@@ -756,7 +761,7 @@ class PostDefinition < ResourceDefinition
756
761
  end
757
762
  ```
758
763
 
759
- With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid — fully backwards-compatible.
764
+ With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid, fully backwards-compatible.
760
765
 
761
766
  ### Independent of `form_layout`
762
767
 
@@ -782,9 +787,9 @@ Raising rather than ignoring the option means a `form_layout` block copied acros
782
787
 
783
788
  ### Section options
784
789
 
785
- `label:`, `description:`, `collapsible:`, `collapsed:`, `condition:` — the same set as [`section(key, *fields, **opts)`](#section-key-fields-opts) minus `columns:`. A collapsible display section behaves exactly as a form one does, `collapsed:` included.
790
+ `label:`, `description:`, `collapsible:`, `collapsed:`, `condition:`, the same set as [`section(key, *fields, **opts)`](#section-key-fields-opts) minus `columns:`. A collapsible display section behaves exactly as a form one does, `collapsed:` included.
786
791
 
787
- Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses — a zero-arity proc keeps its own binding, a one-arity proc is handed the display:
792
+ Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses: a zero-arity proc keeps its own binding, a one-arity proc is handed the display:
788
793
 
789
794
  ```ruby
790
795
  section :audit, :created_at, collapsible: true, collapsed: ->(display) { display.object.active? }
@@ -794,11 +799,11 @@ section :audit, :created_at, collapsible: true, collapsed: ->(display) { display
794
799
 
795
800
  ### Rendering
796
801
 
797
- Each section renders as its own card, stacked by a `sections_wrapper` container — so a sectioned show page has **no single outer card**. Fields declared via [`metadata`](#metadata-panel-show-page) are excluded from the sections and render in the metadata panel instead. Section chrome is themeable; see [UI › Displays › Theming](/reference/ui/displays#theming).
802
+ Each section renders as its own card, stacked by a `sections_wrapper` container, so a sectioned show page has **no single outer card**. Fields declared via [`metadata`](#metadata-panel-show-page) are excluded from the sections and render in the metadata panel instead. Section chrome is themeable; see [UI › Displays › Theming](/reference/ui/displays#theming).
798
803
 
799
804
  ## Page width
800
805
 
801
- Detail-style pages — the show page and resource forms — are constrained to a readable column by default. Inputs and values stretch to their container, so at full content width they become ~1200px-wide text boxes: past a comfortable measure, and a long eye-travel between a label and the value beside it.
806
+ Detail-style pages, the show page and resource forms, are constrained to a readable column by default. Inputs and values stretch to their container, so at full content width they become ~1200px-wide text boxes: past a comfortable measure, and a long eye-travel between a label and the value beside it.
802
807
 
803
808
  Index and table pages are deliberately **not** affected; a table wants every pixel.
804
809
 
@@ -816,7 +821,7 @@ end
816
821
  `:sm` `:md` `:lg` `:xl` `:full`. `:full` opts out of any constraint. An unknown value raises `ArgumentError` at declaration rather than silently rendering at some other width.
817
822
 
818
823
  ::: warning Size tokens are relative to their surface
819
- These are the same token *names* [modal sizes](#modals) use, but **not the same widths**. Each surface has its own scale, because the surfaces aren't comparable — a "small page" is reasonably larger than a "small dialog":
824
+ These are the same token *names* [modal sizes](#modals) use, but **not the same widths**. Each surface has its own scale, because the surfaces aren't comparable: a "small page" is reasonably larger than a "small dialog":
820
825
 
821
826
  | Token | Page width | Centered modal | Slideover |
822
827
  |---|---|---|---|
@@ -846,7 +851,7 @@ All three inherit to subclasses, so a portal-specific definition keeps its paren
846
851
 
847
852
  ### Scope
848
853
 
849
- - **Modals are unaffected** — a dialog sizes itself via `modal_size`.
854
+ - **Modals are unaffected**: a dialog sizes itself via `modal_size`.
850
855
  - **Interactions** (`Plutonium::Interaction::Base`) support the same settings, for interactive actions rendered as standalone pages.
851
856
  - **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `:md` independently of `default_page_width`, so widening resource pages leaves wizard steps where they are. Set both if you want them to match.
852
857
 
@@ -866,9 +871,9 @@ input :documents, as: :uppy,
866
871
 
867
872
  Inside `condition:` procs and block-form `input`/`display`:
868
873
 
869
- - `object` — the record being edited or displayed
874
+ - `object`: the record being edited or displayed
870
875
  - `current_user`
871
- - `current_parent` — parent record for nested resources
876
+ - `current_parent`: parent record for nested resources
872
877
  - `request`, `params`
873
878
  - All view helpers (via the same context as controllers)
874
879
 
@@ -922,23 +927,23 @@ interactive_action_page_breadcrumbs true
922
927
  ```ruby
923
928
  class PostDefinition < ResourceDefinition
924
929
  # "Save and add another" / "Update and continue editing"
925
- # nil (default) — auto: hidden for singular resources, shown for plural
926
- # true — always show
927
- # false — always hide
930
+ # nil (default): auto: hidden for singular resources, shown for plural
931
+ # true: always show
932
+ # false: always hide
928
933
  submit_and_continue false
929
934
 
930
935
  # How :new / :edit and interactive actions render
931
- # :slideover (default) — slide-in panel from the right
932
- # :centered — centered dialog
933
- # false — full standalone pages (no modal)
936
+ # :slideover (default): slide-in panel from the right
937
+ # :centered: centered dialog
938
+ # false: full standalone pages (no modal)
934
939
  # size: optional, one of :sm, :md (default), :lg, :xl, :auto, :full
935
- # (widths are per-surface — see Page width; a slideover's :md is 480px,
940
+ # (widths are per-surface: see Page width; a slideover's :md is 480px,
936
941
  # a centered dialog's is 576px, a page's is 896px)
937
942
  modal :centered, size: :lg
938
943
  end
939
944
  ```
940
945
 
941
- `modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win — see [Actions](./actions).
946
+ `modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win; see [Actions](./actions).
942
947
 
943
948
  ### `show_in` {#show_in}
944
949
 
@@ -951,8 +956,8 @@ end
951
956
 
952
957
  Controls how the **show page** opens when a record is clicked in the table or grid (and serves as the default for a [kanban board](/reference/kanban/dsl#show_in), which can override it per-board):
953
958
 
954
- - `:page` (default) — full-page navigation to the show route.
955
- - `:modal` — the show page opens in a **centered** dialog. This is deliberately independent of `modal:`/`modal_mode` above (which styles `:new`/`:edit`) — show is always centered, never a slideover. From inside the modal an expand icon opens the full page in a new tab; ⌘/Ctrl-click (or middle-click) on the row/card does the same directly.
959
+ - `:page` (default): full-page navigation to the show route.
960
+ - `:modal`, the show page opens in a **centered** dialog. This is deliberately independent of `modal:`/`modal_mode` above (which styles `:new`/`:edit`), show is always centered, never a slideover. From inside the modal an expand icon opens the full page in a new tab; ⌘/Ctrl-click (or middle-click) on the row/card does the same directly.
956
961
 
957
962
  An unknown mode raises `ArgumentError`.
958
963
 
@@ -972,7 +977,7 @@ Behavior:
972
977
  - **Policy-aware.** Fields intersect with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
973
978
  - **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
974
979
  - **Responsive.** Side-by-side at `lg+`, stacked below.
975
- - **Formatting inherits.** Field labels and `as:` declarations propagate — the metadata panel uses the same field-rendering machinery as the main card.
980
+ - **Formatting inherits.** Field labels and `as:` declarations propagate: the metadata panel uses the same field-rendering machinery as the main card.
976
981
 
977
982
  ## Index views (Table & Grid)
978
983
 
@@ -980,7 +985,7 @@ Resources can offer both Table and Grid views. The user switches via the toolbar
980
985
 
981
986
  ```ruby
982
987
  class UserDefinition < ResourceDefinition
983
- # No `index_views :table, :grid` needed — declaring grid_fields auto-enables :grid.
988
+ # No `index_views :table, :grid` needed: declaring grid_fields auto-enables :grid.
984
989
  grid_fields(
985
990
  image: :avatar, # ActiveStorage attachment, Shrine, or URL
986
991
  header: :name, # falls back to to_label
@@ -990,7 +995,7 @@ class UserDefinition < ResourceDefinition
990
995
  footer: :last_seen_at # falls back to :created_at
991
996
  )
992
997
 
993
- default_index_view :grid # optional — initial view when no cookie
998
+ default_index_view :grid # optional: initial view when no cookie
994
999
  grid_layout :media # :compact (default) or :media
995
1000
  grid_columns 3 # pin lg+ cols; default is 1/2/3/4 responsive
996
1001
  end
@@ -1004,13 +1009,13 @@ end
1004
1009
  | `grid_layout :compact \| :media` | `:compact` puts image left of content; `:media` stacks image full-width on top. |
1005
1010
  | `grid_columns N` | Override responsive column count on `lg+`. Default is 1/2/3/4 at sm/md/lg/xl. |
1006
1011
 
1007
- 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.
1012
+ 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.
1008
1013
 
1009
1014
  Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
1010
1015
 
1011
1016
  ## Custom page classes
1012
1017
 
1013
- Override the rendered page entirely — full control via Phlex:
1018
+ Override the rendered page entirely: full control via Phlex:
1014
1019
 
1015
1020
  ```ruby
1016
1021
  class PostDefinition < ResourceDefinition
@@ -1037,8 +1042,8 @@ See [UI › Pages](/reference/ui/pages) and [UI › Forms](/reference/ui/forms)
1037
1042
 
1038
1043
  ## Related
1039
1044
 
1040
- - [Query](./query) — search, filters, scopes, sorting
1041
- - [Actions](./actions) — custom + bulk actions
1042
- - [Behavior › Policy](/reference/behavior/policies) — `permitted_attributes_for_*`, authorization
1043
- - [UI › Forms](/reference/ui/forms) — field builder, association inputs, theming
1044
- - [UI › Pages](/reference/ui/pages) — custom page classes
1045
+ - [Query](./query): search, filters, scopes, sorting
1046
+ - [Actions](./actions): custom + bulk actions
1047
+ - [Behavior › Policy](/reference/behavior/policies): `permitted_attributes_for_*`, authorization
1048
+ - [UI › Forms](/reference/ui/forms): field builder, association inputs, theming
1049
+ - [UI › Pages](/reference/ui/pages): custom page classes
@@ -1,7 +1,7 @@
1
1
  # CSV Export
2
2
 
3
3
  Every resource ships with a streamed CSV export, **disabled by default**. It is *not* an
4
- [action](./actions.md) — it streams a file and opens in a new tab — so it is enabled
4
+ [action](./actions.md) (it streams a file and opens in a new tab), so it is enabled
5
5
  through the policy rather than declared with `action :export_csv`. The route
6
6
  (`GET /<resources>/export_csv`) is auto-mounted on every resource; a split "Export"
7
7
  button appears on the index page once the policy permits it.
@@ -25,8 +25,8 @@ The control is a split button with two behaviours:
25
25
 
26
26
  | | Source | Filename |
27
27
  |---|---|---|
28
- | **Export** (primary) | The current view — selected scope + filters + search (the index's `?q`), **all** matching rows (not just the visible page) | `posts_<date>.csv` |
29
- | **Export all** (dropdown) | The entire authorized scope — ignores scope, filters, search, and default scope | `posts_all_<date>.csv` |
28
+ | **Export** (primary) | The current view: selected scope + filters + search (the index's `?q`), **all** matching rows (not just the visible page) | `posts_<date>.csv` |
29
+ | **Export all** (dropdown) | The entire authorized scope: ignores scope, filters, search, and default scope | `posts_all_<date>.csv` |
30
30
 
31
31
  "Export all" always exports everything the user is authorized to read, regardless of the
32
32
  current scope/filters.
@@ -73,12 +73,12 @@ For a column **without** an `export` block, the value is read straight off the r
73
73
  (`record.public_send(name)`):
74
74
 
75
75
  - **Scalars** (strings, numbers, booleans, dates) are written as-is.
76
- - **Associations** render as their display label — the same `display_name_of` the index
77
- uses (e.g. `User #5`, or the record's `to_label`/`name`/`title` if defined) — never
76
+ - **Associations** render as their display label: the same `display_name_of` the index
77
+ uses (e.g. `User #5`, or the record's `to_label`/`name`/`title` if defined), never
78
78
  `#<User:0x…>`. Add an `export` block to export a specific field instead (e.g. the email).
79
79
  - A name that is **neither** an `export` block **nor** a real method on the record renders
80
80
  the placeholder `<<invalid column>>` rather than aborting the (already-streaming) download.
81
- To export a computed or virtual column, give it an `export` block — a `label:`-only
81
+ To export a computed or virtual column, give it an `export` block: a `label:`-only
82
82
  `export` does **not** supply a value, so it too renders the placeholder.
83
83
 
84
84
  ## Notes & limits
@@ -1,14 +1,14 @@
1
1
  # Resource Reference
2
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.
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
4
 
5
5
  | Layer | File | What it controls |
6
6
  |---|---|---|
7
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) |
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
12
 
13
13
  ## How a resource is born
14
14
 
@@ -22,7 +22,7 @@ That single scaffold gives you a working model + migration + controller + policy
22
22
 
23
23
  ## Auto-detection is the default
24
24
 
25
- Plutonium reads your model and renders every attribute automatically — type, label, form widget, display formatter, table column. You only declare overrides:
25
+ Plutonium reads your model and renders every attribute automatically: type, label, form widget, display formatter, table column. You only declare overrides:
26
26
 
27
27
  ```ruby
28
28
  class PostDefinition < Plutonium::Resource::Definition
@@ -33,20 +33,20 @@ end
33
33
  ```
34
34
 
35
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.
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
37
  :::
38
38
 
39
39
  ## Sub-pages
40
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
- - [Positioning & drag-to-reorder](./positioning) — `positioned_on`, `position_on`, the drag grip, `reposition?`
46
- - [CSV Export](./export) — streamed export, opt-in through the policy
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
+ - [Positioning & drag-to-reorder](./positioning): `positioned_on`, `position_on`, the drag grip, `reposition?`
46
+ - [CSV Export](./export): streamed export, opt-in through the policy
47
47
 
48
48
  ## Related
49
49
 
50
- - [Guides › Adding Resources](/guides/adding-resources) — task recipe
51
- - [App › Generators](/reference/app/generators) — `pu:res:scaffold` / `pu:res:conn` reference
52
- - [Tenancy](/reference/tenancy/) — multi-tenant scoping
50
+ - [Guides › Adding Resources](/guides/adding-resources): task recipe
51
+ - [App › Generators](/reference/app/generators): `pu:res:scaffold` / `pu:res:conn` reference
52
+ - [Tenancy](/reference/tenancy/): multi-tenant scoping