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
@@ -4,7 +4,7 @@ Built on [Phlexi::Form](https://github.com/radioactive-labs/phlexi-form). Overri
4
4
 
5
5
  ## 🚨 Critical
6
6
 
7
- - **`render_actions` is mandatory in custom `form_template`** — without it, the form has no submit button.
7
+ - **`render_actions` is mandatory in custom `form_template`**: without it, the form has no submit button.
8
8
  - **Configure inputs in the definition, render them with `render_resource_field`** in the form template. Don't reimplement field widgets from scratch.
9
9
  - **Override via nested classes** (`class Form < Form; end`) inside the definition. Don't replace the root `Plutonium::UI::Form::Resource` class.
10
10
 
@@ -25,7 +25,7 @@ class PostDefinition < ResourceDefinition
25
25
  class Form < Form
26
26
  def form_template
27
27
  render_fields # render every permitted field
28
- render_actions # submit buttons — REQUIRED
28
+ render_actions # submit buttons, REQUIRED
29
29
  end
30
30
  end
31
31
  end
@@ -47,9 +47,9 @@ end
47
47
 
48
48
  ## Custom layouts
49
49
 
50
- ### Sectioned form (declarative — preferred)
50
+ ### Sectioned form (declarative: preferred)
51
51
 
52
- Declare sections in the **definition** using `form_layout`. The form picks up the layout automatically — no `Form` subclass needed for common cases.
52
+ Declare sections in the **definition** using `form_layout`. The form picks up the layout automatically, no `Form` subclass needed for common cases.
53
53
 
54
54
  ```ruby
55
55
  class PostDefinition < ResourceDefinition
@@ -66,11 +66,11 @@ class PostDefinition < ResourceDefinition
66
66
  end
67
67
  ```
68
68
 
69
- This handles headings, collapsible panels, per-section column counts, and `condition:`-based visibility — all with no view code. See [Resource › Definition › Form layout](/reference/resource/definition#form-layout) for the full DSL reference, including `ungrouped`, `condition:`, `columns:`, and the "On interactions" note.
69
+ This handles headings, collapsible panels, per-section column counts, and `condition:`-based visibility, all with no view code. See [Resource › Definition › Form layout](/reference/resource/definition#form-layout) for the full DSL reference, including `ungrouped`, `condition:`, `columns:`, and the "On interactions" note.
70
70
 
71
71
  ### Full control: override `render_fields`
72
72
 
73
- When the declarative DSL doesn't cover your use case — asymmetric multi-column layouts, embedding a panel widget between sections, etc. — override `render_fields` in a nested `Form` class:
73
+ When the declarative DSL doesn't cover your use case (asymmetric multi-column layouts, embedding a panel widget between sections, etc.) override `render_fields` in a nested `Form` class:
74
74
 
75
75
  ```ruby
76
76
  class PostDefinition < ResourceDefinition
@@ -109,7 +109,7 @@ class PostDefinition < ResourceDefinition
109
109
  end
110
110
  ```
111
111
 
112
- Prefer `form_layout` in the definition — it keeps layout config out of view code and works for interactions too.
112
+ Prefer `form_layout` in the definition; it keeps layout config out of view code and works for interactions too.
113
113
 
114
114
  ### Two-column layout
115
115
 
@@ -137,7 +137,7 @@ end
137
137
 
138
138
  ## Field builder (`field(:foo).input_tag`)
139
139
 
140
- `render_resource_field` uses the input config from the definition. For ad-hoc rendering — when you want fine-grained control over a specific field — use `field(...)` directly:
140
+ `render_resource_field` uses the input config from the definition. For ad-hoc rendering (when you want fine-grained control over a specific field) use `field(...) ` directly:
141
141
 
142
142
  ```ruby
143
143
  render field(:title).wrapped { |f| f.input_tag } # wrapped: label + hint + errors
@@ -150,9 +150,9 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
150
150
  | Tag | Input |
151
151
  |---|---|
152
152
  | `input_tag` | text (auto-detected type) |
153
- | `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
154
- | `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
155
- | `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`) — the **default** for boolean columns; same behavior as a checkbox. Use `checkbox_tag` (`as: :boolean`) for a plain checkbox. |
153
+ | `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `hidden_tag` | standard HTML inputs |
154
+ | `checkbox_tag`, `select_tag`, `radio_button_tag`, `collection_radio_buttons_tag`, `collection_checkboxes_tag` | standard |
155
+ | `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`), the **default** for boolean columns; same behavior as a checkbox. Use `checkbox_tag` (`as: :boolean`) for a plain checkbox. |
156
156
 
157
157
  ### Plutonium-enhanced tags
158
158
 
@@ -164,12 +164,17 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
164
164
  | `phone_tag` / `int_tel_input_tag` | intl-tel-input phone field |
165
165
  | `currency_tag` | Money input (number field + optional unit prefix) |
166
166
  | `uppy_tag` / `file_tag` | Uppy file upload |
167
+ | `binary_tag` | Plain file input whose bytes go into a `binary` column |
168
+ | `rich_text_tag` | Action Text editor (Trix, or Lexxy once installed) |
169
+ | `list_tag` | Chip input for array values |
170
+ | `tags_tag` | Chip input for a no_fly_list `has_tags` context |
171
+ | `rating_tag` | Star picker (radio buttons, no JS); an optional rating gets a Clear option |
167
172
  | `secure_association_tag` | Association with policy-checked options (inline `+` add, typeahead) |
168
173
  | `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
169
174
  | `key_value_store_tag` | Key/value pairs editor |
170
175
 
171
176
  ```ruby
172
- render field(:published_at).wrapped { |f| f.flatpickr_tag(min_date: Date.today, enable_time: true) }
177
+ render field(:published_at).wrapped { |f| f.flatpickr_tag }
173
178
 
174
179
  render field(:avatar).wrapped do |f|
175
180
  f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes)
@@ -180,8 +185,8 @@ end
180
185
 
181
186
  `as: :phone` renders an [intl-tel-input](https://github.com/jackocnr/intl-tel-input) field. Forward library options two ways:
182
187
 
183
- - `initial_country:` — a convenient shortcut for the library's `initialCountry` (ISO2, e.g. `"gh"`). This preselects a country so the widget doesn't show *"No country selected"* and a bare local number validates.
184
- - `intl_options:` — any other library option, using the library's own camelCase names. Merged over the shortcut, so it wins on conflict.
188
+ - `initial_country:`: a convenient shortcut for the library's `initialCountry` (ISO2, e.g. `"gh"`). This preselects a country so the widget doesn't show *"No country selected"* and a bare local number validates.
189
+ - `intl_options:`: any other library option, using the library's own camelCase names. Merged over the shortcut, so it wins on conflict.
185
190
 
186
191
  ```ruby
187
192
  input :phone, as: :phone, initial_country: "gh"
@@ -207,14 +212,14 @@ The field defaults to `strictMode: true`; override it via `intl_options: {strict
207
212
  ```ruby
208
213
  input :price, as: :currency # unit from has_cents / config / i18n
209
214
  input :price, as: :currency, unit: "£" # a literal symbol
210
- input :price, as: :currency, unit: false # no prefix — a plain number input
215
+ input :price, as: :currency, unit: false # no prefix, a plain number input
211
216
  ```
212
217
 
213
218
  When nothing resolves (or `unit: false`), the prefix is omitted and it's an ordinary number input.
214
219
 
215
- **`has_cents` fields infer it automatically** — just like the display. A bare `input :price` on a `has_cents` attribute renders the currency input with the unit read off `has_cents`; no `as: :currency` needed. Use the explicit `as: :currency` for a non-`has_cents` decimal, or to override the unit.
220
+ **`has_cents` fields infer it automatically**: just like the display. A bare `input :price` on a `has_cents` attribute renders the currency input with the unit read off `has_cents`; no `as: :currency` needed. Use the explicit `as: :currency` for a non-`has_cents` decimal, or to override the unit.
216
221
 
217
- In a **wizard** step the data snapshot has no `has_cents` reflection, so there's nothing to infer from — declare `as: :currency` and pass `unit:` explicitly (`input :price, as: :currency, unit: "$"`); the review summary reads it back and formats the value as currency.
222
+ In a **wizard** step the data snapshot has no `has_cents` reflection, so there's nothing to infer from; declare `as: :currency` and pass `unit:` explicitly (`input :price, as: :currency, unit: "$"`); the review summary reads it back and formats the value as currency.
218
223
 
219
224
  ### Password & secret fields {#password-fields}
220
225
 
@@ -222,10 +227,10 @@ In a **wizard** step the data snapshot has no `has_cents` reflection, so there's
222
227
 
223
228
  | field state | result |
224
229
  |---|---|
225
- | untouched (sentinel) | kept — the stored secret is left unchanged |
230
+ | untouched (sentinel) | kept, the stored secret is left unchanged |
226
231
  | edited to a new value, then failed re-render | comes back **blank + `required`** so the user re-types it (a submitted secret is never echoed back) |
227
- | cleared, then failed re-render | comes back blank, **not** `required` — the clear may be intentional, so it's allowed to stand |
228
- | emptied | explicit clear (clear-by-blank) — the `required` guard only prevents an *accidental* blank submit |
232
+ | cleared, then failed re-render | comes back blank, **not** `required`; the clear may be intentional, so it's allowed to stand |
233
+ | emptied | explicit clear (clear-by-blank), the `required` guard only prevents an *accidental* blank submit |
229
234
  | typed | set as the new value |
230
235
 
231
236
  The sentinel is guarded client-side by the `password-sentinel` Stimulus controller: the first edit (a keystroke, paste, or **backspace**) wipes the whole field, so a partial edit can't corrupt the sentinel into a literal new password. New records and interaction forms (set-password, reset-password) render an honest empty field.
@@ -237,33 +242,78 @@ The sentinel is guarded client-side by the `password-sentinel` Stimulus controll
237
242
  - ends with `_password`, `_digest`, `_hash`, `_token`, `_key`, or `_salt`;
238
243
  - contains `secret`.
239
244
 
240
- This is a naming convenience, **not** a security guarantee — tune it per field:
245
+ The same names are masked everywhere the value would otherwise print: the show page, table cells, the wizard summary, and grid and kanban card slots all render `••••••••` instead of the value. A `field`/`display` `as:` overrides the heuristic on those surfaces too.
246
+
247
+ This is a naming convenience, **not** a security guarantee, tune it per field:
241
248
 
242
249
  ```ruby
243
250
  # Opt OUT: render the value as a normal, readable text input
244
- field :api_token, as: :string # a token the admin needs to copy
251
+ field :api_token, as: :string # a token the admin needs to copy (shown on the show page and cards too)
245
252
  field :content_hash, as: :string # a checksum, not a secret
246
253
  field :public_key, as: :string # *_key matches, but a public key is not secret
247
254
 
248
255
  # Opt IN: mask a secret the heuristic still misses (e.g. no telltale name)
249
256
  field :recovery_phrase, as: :password
257
+ field :pin, as: :secret # alias of :password
250
258
  ```
251
259
 
252
260
  > [!WARNING]
253
261
  > The heuristic is name-based and best-effort. A secret column with an unconventional name (e.g. `recovery_phrase`, `pin`) still renders its value into the page unless you set `as: :password`. Audit secret-bearing columns explicitly.
254
262
 
263
+ ### Rich text (Action Text and Lexxy) {#rich-text-fields}
264
+
265
+ A `has_rich_text` field renders Action Text's own editor tag, so it gets whichever editor the app has set up: Trix by default, or [Lexxy](https://lexxy.dev) once the `lexxy` gem is in the bundle (it takes over Action Text's helpers on Rails 8.0/8.1 and registers itself as the editor on 8.2). Lexxy needs Rails 8.0.2 or later. The editor's JavaScript and CSS come from the app's bundle, as Action Text's or Lexxy's install instructions describe. The show page renders the HTML through Action Text, which sanitizes it.
266
+
267
+ ### Lists and tags {#list-fields}
268
+
269
+ `as: :list` edits an array value (a JSON or array column) as chips. Options go on `input`:
270
+
271
+ ```ruby
272
+ field :nicknames, as: :list
273
+ input :nicknames, suggestions: %w[Basin Tub], limit: 5 # addable: false allows only suggestions
274
+ ```
275
+
276
+ Removing every chip saves an empty array.
277
+
278
+ [no_fly_list](https://github.com/radioactive-labs/no_fly_list) `has_tags` contexts are detected automatically. Permit them by context name (`labels`, not `labels_list`) and they render as chips on the form, the show page and the table:
279
+
280
+ ```ruby
281
+ class Article < ResourceRecord
282
+ include NoFlyList::TaggableRecord
283
+ has_tags :labels, scope: :organization # per-tenant vocabulary
284
+ has_tags :topics, restrict_to_existing: true, limit: 3
285
+ end
286
+
287
+ # policy
288
+ def permitted_attributes_for_create = %i[title labels topics]
289
+ ```
290
+
291
+ - The input suggests tags that already exist in the record's tag scope.
292
+ - `restrict_to_existing` only allows picking existing tags. Plutonium doesn't decide where that vocabulary is managed: create the tags however suits the app (register the tag model as a resource, seed them, or add them in an interaction).
293
+ - `limit` caps how many tags can be chosen.
294
+ - Add `filter :labels, with: :tags` for a table filter.
295
+ - The table reads tags through the context's association, so preload it (`includes(:labels)`) to avoid a query per row.
296
+
297
+ ### Binary columns {#binary-fields}
298
+
299
+ A `binary` column renders `binary_tag`: a plain file input. The uploaded file's bytes are written to the column, and the show page and table print its size (`Binary data (12 KB)`), never the bytes.
300
+
301
+ - Saving without choosing a file keeps the stored bytes. There is no control to clear the column.
302
+ - Browsers can't refill a file input, so after a failed validation the user has to choose the file again.
303
+ - The whole file is read into memory and stored in the row. Use Active Storage (`has_one_attached` + `:uppy`) for anything that isn't small.
304
+
255
305
  ### Wrapped vs unwrapped
256
306
 
257
- - `wrapped` — includes label, hint, and error rendering. Use for normal form fields.
258
- - Bare tag — just the input element. Use when you're laying out custom wrappers.
259
- - `wrapped(class: "...")` — pass classes to the wrapper div.
307
+ - `wrapped`: includes label, hint, and error rendering. Use for normal form fields.
308
+ - Bare tag, just the input element. Use when you're laying out custom wrappers.
309
+ - `wrapped(class: "...")`: pass classes to the wrapper div.
260
310
 
261
311
  ## Association inputs (`secure_association_tag`) {#association-inputs}
262
312
 
263
313
  Association inputs render with two affordances out of the box:
264
314
 
265
- - **Inline `+` add** — a button next to the select opens the target resource's `:new` action. Inherits the target's modal mode. If the parent form is already in a modal, the `+` opens a **stacked secondary modal** (see [Pages › Stacked modals](./pages#stacked-modals-secondary-frame)) so the in-progress form isn't lost — on success the secondary closes and the parent reloads.
266
- - **Typeahead** — server-side autocomplete is on by default. Uses the target's `search` block if defined; otherwise falls back to a `LIKE` on the input's `label_method:` column or the first match from `[name, title, label, slug, display_name, email]`. See [Resource › Query › Search](/reference/resource/query#search) for the typeahead fallback details.
315
+ - **Inline `+` add**: a button next to the select opens the target resource's `:new` action. Inherits the target's modal mode. If the parent form is already in a modal, the `+` opens a **stacked secondary modal** (see [Pages › Stacked modals](./pages#stacked-modals-secondary-frame)) so the in-progress form isn't lost; on success the secondary closes and the parent reloads.
316
+ - **Typeahead**: server-side autocomplete is on by default. Uses the target's `search` block if defined; otherwise falls back to a `LIKE` on the input's `label_method:` column or the first match from `[name, title, label, slug, display_name, email]`. See [Resource › Query › Search](/reference/resource/query#search) for the typeahead fallback details.
267
317
 
268
318
  ```ruby
269
319
  # Opt out of the + button
@@ -280,7 +330,7 @@ input :author, label_method: :email
280
330
  ```
281
331
 
282
332
  ::: tip Large association tables
283
- For large target tables, write an explicit `search` block on the target resource definition — the fallback's leading-wildcard `LIKE` can't use a b-tree index.
333
+ For large target tables, write an explicit `search` block on the target resource definition; the fallback's leading-wildcard `LIKE` can't use a b-tree index.
284
334
  :::
285
335
 
286
336
  ## Submit buttons
@@ -291,7 +341,7 @@ Control the secondary button via the definition:
291
341
 
292
342
  ```ruby
293
343
  class PostDefinition < ResourceDefinition
294
- submit_and_continue false # nil (default — auto), true (always show), false (always hide)
344
+ submit_and_continue false # nil (default, auto), true (always show), false (always hide)
295
345
  end
296
346
  ```
297
347
 
@@ -313,9 +363,9 @@ end
313
363
 
314
364
  These all live in the definition layer:
315
365
 
316
- - **Pre-submit / dynamic forms** — see [Resource › Definition › Dynamic forms](/reference/resource/definition#dynamic-forms-pre-submit)
317
- - **Nested inputs** (`nested_input :variants`) — see [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs)
318
- - **Interaction forms** — interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [Behavior › Interactions](/reference/behavior/interactions)
366
+ - **Pre-submit / dynamic forms**: see [Resource › Definition › Dynamic forms](/reference/resource/definition#dynamic-forms-pre-submit)
367
+ - **Nested inputs** (`nested_input :variants`), see [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs)
368
+ - **Interaction forms**: interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [Behavior › Interactions](/reference/behavior/interactions)
319
369
 
320
370
  ## Theming
321
371
 
@@ -342,7 +392,7 @@ end
342
392
  ```
343
393
 
344
394
  ::: warning Always `super.merge(...)`
345
- Don't replace the theme wholesale — Plutonium's defaults handle invalid states, focus rings, and dark mode. `super.merge` keeps them.
395
+ Don't replace the theme wholesale, Plutonium's defaults handle invalid states, focus rings, and dark mode. `super.merge` keeps them.
346
396
  :::
347
397
 
348
398
  ### Theme keys
@@ -384,9 +434,9 @@ end
384
434
 
385
435
  ## Related
386
436
 
387
- - [Pages](./pages) — `NewPage` / `EditPage` page hooks
388
- - [Components](./components) — building reusable Phlex components for forms
389
- - [Assets](./assets) — `.pu-*` classes, design tokens, dark mode
390
- - [Resource › Definition](/reference/resource/definition) — input configuration (`as:`, `hint:`, `condition:`, blocks)
391
- - [Behavior › Interactions](/reference/behavior/interactions) — interaction forms (`Plutonium::UI::Form::Interaction`)
392
- - [Tenancy › Nested resources](/reference/tenancy/nested-resources) — parent fields hidden by URL
437
+ - [Pages](./pages): `NewPage` / `EditPage` page hooks
438
+ - [Components](./components): building reusable Phlex components for forms
439
+ - [Assets](./assets): `.pu-*` classes, design tokens, dark mode
440
+ - [Resource › Definition](/reference/resource/definition): input configuration (`as:`, `hint:`, `condition:`, blocks)
441
+ - [Behavior › Interactions](/reference/behavior/interactions): interaction forms (`Plutonium::UI::Form::Interaction`)
442
+ - [Tenancy › Nested resources](/reference/tenancy/nested-resources): parent fields hidden by URL
@@ -4,27 +4,29 @@ Plutonium uses [Phlex](https://www.phlex.fun/) for all view components and Tailw
4
4
 
5
5
  ## Sub-pages
6
6
 
7
- - [Pages](./pages) — `IndexPage`, `ShowPage`, `NewPage`, `EditPage`, render hooks, custom ERB views, context detection
8
- - [Forms](./forms) — `Form` class, field builder, association inputs (typeahead + inline add), themes
9
- - [Displays](./displays) — `Display` class, custom rendering, block-form displays
10
- - [Tables](./tables) — `Table` class, custom rendering, search/scopes bar
11
- - [Components](./components) — built-in component kit, custom Phlex components, `DynaFrameContent` pattern, modals & tabs
12
- - [Layouts](./layouts) — shell config, ejecting chrome, custom `ResourceLayout` class
13
- - [Assets](./assets) — Tailwind config, Stimulus controllers, design tokens, `.pu-*` component classes, Phlexi themes
7
+ - [Pages](./pages): `IndexPage`, `ShowPage`, `NewPage`, `EditPage`, render hooks, custom ERB views, context detection
8
+ - [Forms](./forms): `Form` class, field builder, association inputs (typeahead + inline add), themes
9
+ - [Displays](./displays): `Display` class, custom rendering, block-form displays
10
+ - [Tables](./tables): `Table` class, custom rendering, search/scopes bar
11
+ - [Components](./components): built-in component kit, custom Phlex components, `DynaFrameContent` pattern, modals & tabs
12
+ - [Layouts](./layouts): shell config, ejecting chrome, custom `ResourceLayout` class
13
+ - [Assets](./assets): Tailwind config, Stimulus controllers, design tokens, `.pu-*` component classes, Phlexi themes
14
14
 
15
15
  ## 🚨 Critical (applies across all sub-pages)
16
16
 
17
17
  - **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end`. Don't replace the entire view layer.
18
18
  - **Use render hooks, not `view_template`.** `render_before_content`, `render_after_content`, `render_before_toolbar`, etc. exist so you don't reimplement the whole page.
19
- - **All pages inherit `DynaFrameContent`** — turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work".
20
- - **Custom components inherit `Plutonium::UI::Component::Base`** — gives you the component kit (`PageHeader`, `Panel`, `Block`), resource helpers, and the `helpers` proxy for Rails helpers.
21
- - **`render_actions` is mandatory in custom `form_template`** — without it, the form has no submit button.
22
- - **Always `registerControllers(application)`** in `app/javascript/controllers/index.js`. Without it, Plutonium's Stimulus controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead.
23
- - **Use `plutoniumTailwindConfig.merge`** when extending Tailwind theme — plain object merge drops Plutonium's defaults.
24
- - **Prefer `.pu-*` classes and `var(--pu-*)` tokens** over hardcoded `gray-X/dark:gray-Y` pairs — they switch with dark mode automatically.
19
+ - **All pages inherit `DynaFrameContent`**: turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work".
20
+ - **Custom components inherit `Plutonium::UI::Component::Base`**: gives you the component kit (`PageHeader`, `Panel`, `Block`), resource helpers, and the `helpers` proxy for Rails helpers.
21
+ - **`render_actions` is mandatory in custom `form_template`**: without it, the form has no submit button.
22
+ - **Custom CSS, brand colors, or your own Stimulus controllers need `pu:core:assets` first.** Out of the box the app serves the gem's prebuilt `plutonium.css` / `plutonium.min.js`; the generator switches it to your own bundles. Don't hand-write the Tailwind/PostCSS pipeline.
23
+ - **Once the app owns its JS bundle, `registerControllers(application)`** must be in `app/javascript/controllers/index.js` (`pu:core:assets` adds it). Your bundle replaces the gem's, so without it Plutonium's Stimulus controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead.
24
+ - **Use `plutoniumTailwindConfig.merge`** when extending Tailwind theme, plain object merge drops Plutonium's defaults.
25
+ - **Style with `.pu-*` classes first, `var(--pu-*)` tokens second, raw palette pairs last.** Banners, badges, cards and buttons all have a `.pu-*` class that carries its own `.dark` rule. A hand-written `bg-warning-50 dark:bg-warning-950/30` pair duplicates that, drifts from the theme, and may not even exist in the prebuilt CSS.
26
+ - **User-facing copy goes through `t(...)` with a locale key**, in components, pages, displays and definitions alike. See [i18n](/reference/i18n).
25
27
  - **Configure inputs in the definition; render them with `render_resource_field` in the form.** Don't reimplement field widgets from scratch.
26
28
 
27
29
  ## Related
28
30
 
29
- - [Resource › Definition](/reference/resource/definition) — field-level rendering (`field :foo, as: :markdown`, `display :status do |f| … end`)
30
- - [Behavior › Controllers](/reference/behavior/controllers) — controller render-context hooks (`present_parent?`, `submit_parent?`)
31
+ - [Resource › Definition](/reference/resource/definition): field-level rendering (`field :foo, as: :markdown`, `display :status do |f| … end`)
32
+ - [Behavior › Controllers](/reference/behavior/controllers): controller render-context hooks (`present_parent?`, `submit_parent?`)
@@ -1,12 +1,12 @@
1
1
  # Layouts
2
2
 
3
- The overall page chrome — topbar, sidebar, footer, body wrapping. Plutonium ships three shells; you can eject the templates or write a custom `ResourceLayout` for total control.
3
+ The overall page chrome, topbar, sidebar, footer, body wrapping. Plutonium ships three shells; you can eject the templates or write a custom `ResourceLayout` for total control.
4
4
 
5
5
  ## Shell
6
6
 
7
7
  ```ruby
8
8
  Plutonium.configure do |config|
9
- config.shell = :modern # default — topbar + icon rail
9
+ config.shell = :modern # default, topbar + icon rail
10
10
  # config.shell = :plain # topbar, no icon rail (rail-less app)
11
11
  # config.shell = :classic # legacy header + sidebar (only when upgrading)
12
12
  end
@@ -20,9 +20,9 @@ If you're starting fresh, use `:modern`. `:classic` exists so apps upgrading fro
20
20
 
21
21
  The shell variant selects the page chrome:
22
22
 
23
- - `:modern` (default) — Topbar plus the desktop icon rail.
24
- - `:plain` — Topbar but **no** icon rail. The Topbar is kept; only the rail is removed, so the surface is rail-less.
25
- - `:classic` — legacy Header/Sidebar (upgrade paths only).
23
+ - `:modern` (default), Topbar plus the desktop icon rail.
24
+ - `:plain`: Topbar but **no** icon rail. The Topbar is kept; only the rail is removed, so the surface is rail-less.
25
+ - `:classic`: legacy Header/Sidebar (upgrade paths only).
26
26
 
27
27
  ### Resolving the shell (global → engine → controller)
28
28
 
@@ -32,7 +32,7 @@ The shell resolves across three layers, each overriding the one above it:
32
32
  # 1. Global default (config/initializers/plutonium.rb)
33
33
  Plutonium.configure { |config| config.shell = :modern }
34
34
 
35
- # 2. Per-engine — set it on a portal engine (lib/engine.rb)
35
+ # 2. Per-engine: set it on a portal engine (lib/engine.rb)
36
36
  module CustomerPortal
37
37
  class Engine < Rails::Engine
38
38
  include Plutonium::Portal::Engine
@@ -43,19 +43,19 @@ module CustomerPortal
43
43
  end
44
44
  end
45
45
 
46
- # 3. Per-controller — overrides the engine/global default for one controller (and subclasses)
46
+ # 3. Per-controller: overrides the engine/global default for one controller (and subclasses)
47
47
  class DashboardController < ResourceController
48
48
  shell :modern # opt this controller back into the rail
49
49
  end
50
50
  ```
51
51
 
52
- `shell` takes a plain symbol, so it's safe in the class body too — but the generated engine already has a `config.after_initialize` block (where `scope_to_entity` lives), so keeping it there is the consistent home.
52
+ `shell` takes a plain symbol, so it's safe in the class body too, but the generated engine already has a `config.after_initialize` block (where `scope_to_entity` lives), so keeping it there is the consistent home.
53
53
 
54
54
  An unset engine/controller value (`nil`) falls through to the next layer. Read the resolved value with the `shell` helper (`controller.shell`).
55
55
 
56
- ### `rail` — a controller-level rail toggle
56
+ ### `rail`: a controller-level rail toggle
57
57
 
58
- Alongside `shell`, any resource controller exposes a class-level `rail` DSL that flips just the icon rail without changing the shell. It's a `class_attribute`, so it's inherited — a portal opts its entire surface in or out by calling `rail false` (or `rail true`) once in its controller concern:
58
+ Alongside `shell`, any resource controller exposes a class-level `rail` DSL that flips just the icon rail without changing the shell. It's a `class_attribute`, so it's inherited; a portal opts its entire surface in or out by calling `rail false` (or `rail true`) once in its controller concern:
59
59
 
60
60
  ```ruby
61
61
  module CustomerPortal
@@ -68,15 +68,15 @@ module CustomerPortal
68
68
  end
69
69
  ```
70
70
 
71
- `rail nil` (the default) inherits the resolved shell — the rail shows when the resolved `shell == :modern`. Read the resolved value with the `rail?` predicate.
71
+ `rail nil` (the default) inherits the resolved shell, the rail shows when the resolved `shell == :modern`. Read the resolved value with the `rail?` predicate.
72
72
 
73
73
  ### Stable CSS hooks
74
74
 
75
75
  Rail-less rendering exposes a few stable hooks for custom overrides:
76
76
 
77
- - `pu-topbar` — class on the Topbar nav.
78
- - `pu-sticky-footer` — class on the form sticky-footer div.
79
- - `html.pu-no-rail` — root class present whenever the current page is rail-less.
77
+ - `pu-topbar`: class on the Topbar nav.
78
+ - `pu-sticky-footer`: class on the form sticky-footer div.
79
+ - `html.pu-no-rail`: root class present whenever the current page is rail-less.
80
80
 
81
81
  A built-in rule cancels the desktop rail inset on `.pu-topbar` and `.pu-sticky-footer` under `html.pu-no-rail`; target these hooks to layer your own CSS.
82
82
 
@@ -87,7 +87,7 @@ rails generate pu:eject:shell --dest=admin_portal
87
87
  rails generate pu:eject:layout
88
88
  ```
89
89
 
90
- `pu:eject:shell` copies `_resource_header.html.erb` and `_resource_sidebar.html.erb` into the portal's `app/views/plutonium/`. The eject is independent of `shell` — you can run it on either.
90
+ `pu:eject:shell` copies `_resource_header.html.erb` and `_resource_sidebar.html.erb` into the portal's `app/views/plutonium/`. The eject is independent of `shell`; you can run it on either.
91
91
 
92
92
  `pu:eject:layout` copies `layouts/resource.html.erb` for layout-level edits.
93
93
 
@@ -111,7 +111,7 @@ The sidebar/icon-rail navigation is built with `Phlexi::Menu::Builder` in the ej
111
111
 
112
112
  ### Per-item link attributes
113
113
 
114
- Any extra options you pass to `item` are spread straight onto the rendered `<a>` — so a menu entry can opt into `target`, `rel`, `data-*`, `aria-*`, etc. Useful for items that open in their own tab or drive a Stimulus/Turbo behavior:
114
+ Any extra options you pass to `item` are spread straight onto the rendered `<a>`, so a menu entry can opt into `target`, `rel`, `data-*`, `aria-*`, etc. Useful for items that open in their own tab or drive a Stimulus/Turbo behavior:
115
115
 
116
116
  ```ruby
117
117
  m.item "Inbox",
@@ -122,7 +122,7 @@ m.item "Inbox",
122
122
  data: {turbo_frame: "_top"}
123
123
  ```
124
124
 
125
- This works across both shells — the `:modern` icon-rail (leaf items, parent flyout triggers, and flyout children) and the `:classic` sidebar. Framework attributes always win on conflict: a custom `class:` is **merged** with the component's base classes, and on a parent trigger your `data:` / `aria:` merge with the flyout's own wiring (so you can't accidentally break the toggle). The `:active` key is reserved by Phlexi for [custom active-state logic](https://github.com/radioactive-labs/phlexi-menu) and is never emitted as an attribute.
125
+ This works across both shells, the `:modern` icon-rail (leaf items, parent flyout triggers, and flyout children) and the `:classic` sidebar. Framework attributes always win on conflict: a custom `class:` is **merged** with the component's base classes, and on a parent trigger your `data:` / `aria:` merge with the flyout's own wiring (so you can't accidentally break the toggle). The `:active` key is reserved by Phlexi for [custom active-state logic](https://github.com/radioactive-labs/phlexi-menu) and is never emitted as an attribute.
126
126
 
127
127
  ## Custom layout class
128
128
 
@@ -190,7 +190,7 @@ See [Assets › Tailwind config](./assets#tailwind-config) for the full merge st
190
190
 
191
191
  ## Dark mode
192
192
 
193
- `selector` strategy — toggle by adding/removing `dark` on `<html>`. The bundled `color-mode` Stimulus controller handles toggling; Plutonium ships a switcher.
193
+ `selector` strategy, toggle by adding/removing `dark` on `<html>`. The bundled `color-mode` Stimulus controller handles toggling; Plutonium ships a switcher.
194
194
 
195
195
  ```javascript
196
196
  // Manual toggle if needed
@@ -199,6 +199,6 @@ document.documentElement.classList.toggle('dark')
199
199
 
200
200
  ## Related
201
201
 
202
- - [Assets](./assets) — Tailwind config, design tokens, `.pu-*` classes
203
- - [Components](./components) — custom components used in layout hooks (`AnnouncementBanner`, etc.)
204
- - [Pages](./pages) — page-level hooks (a lighter alternative for per-page chrome)
202
+ - [Assets](./assets): Tailwind config, design tokens, `.pu-*` classes
203
+ - [Components](./components): custom components used in layout hooks (`AnnouncementBanner`, etc.)
204
+ - [Pages](./pages): page-level hooks (a lighter alternative for per-page chrome)
@@ -32,12 +32,10 @@ end
32
32
 
33
33
  ```ruby
34
34
  class PostDefinition < ResourceDefinition
35
- index_page_title "Blog Posts"
36
- index_page_description "Manage all published articles"
37
- show_page_title "Article Details"
38
- show_page_title -> { current_record!.title } # dynamic
39
- new_page_title "Create Post"
40
- edit_page_title -> { "Edit: #{current_record!.title}" }
35
+ index_page_title t("blog.posts.index.title") # lazy: resolved per request, in its locale
36
+ index_page_description t("blog.posts.index.description")
37
+ show_page_title "Article Details" # a literal also works (fixed, untranslated)
38
+ new_page_title t("blog.posts.new.title")
41
39
 
42
40
  breadcrumbs true # global default
43
41
  index_page_breadcrumbs false # per-page override
@@ -48,9 +46,11 @@ class PostDefinition < ResourceDefinition
48
46
  end
49
47
  ```
50
48
 
49
+ The class-level `t` in a definition returns a lazy proc; calling `I18n.t` directly in the class body would resolve once, at load time. A title that depends on the record belongs in a `page_title` override on the nested page class (see the hooks example below), not in a lambda here: the setter's proc is called with no record context.
50
+
51
51
  ## Page hooks (preferred over `view_template`)
52
52
 
53
- Every page inherits these — use them instead of overriding `view_template` to preserve breadcrumbs, header, and DynaFrame behavior:
53
+ Every page inherits these, use them instead of overriding `view_template` to preserve breadcrumbs, header, and DynaFrame behavior:
54
54
 
55
55
  | Hook | Position |
56
56
  |---|---|
@@ -68,12 +68,12 @@ class ShowPage < ShowPage
68
68
  private
69
69
 
70
70
  def page_title
71
- "#{object.title} — #{object.author.name}"
71
+ "#{object.title}, #{object.author.name}"
72
72
  end
73
73
 
74
74
  def render_before_content
75
- div(class: "alert alert-info") do
76
- plain "This post has #{object.comments.count} comments"
75
+ div(class: "pu-alert pu-alert-info", role: "status") do
76
+ div(class: "pu-alert-message") { t("blog.posts.show.comment_count", count: object.comments.count) }
77
77
  end
78
78
  end
79
79
 
@@ -105,7 +105,7 @@ The default view simply renders the page class:
105
105
  <%= render current_definition.show_page_class.new %>
106
106
  ```
107
107
 
108
- Mix — keep the default and add chrome around it:
108
+ Mix, keep the default and add chrome around it:
109
109
 
110
110
  ```erb
111
111
  <div class="announcement-banner">Special announcement</div>
@@ -125,7 +125,7 @@ Use to pin action strips, omit nav chrome, or swap layouts.
125
125
 
126
126
  ### Stacked modals (secondary frame)
127
127
 
128
- Association inputs include an inline `+` button. When the parent form is itself rendered in a modal, the `+` opens a **second stacked modal** in `Plutonium::REMOTE_MODAL_SECONDARY_FRAME` instead of replacing the primary modal. On successful create, the secondary closes and the primary frame reloads so the new record appears in the select — no developer wiring.
128
+ Association inputs include an inline `+` button. When the parent form is itself rendered in a modal, the `+` opens a **second stacked modal** in `Plutonium::REMOTE_MODAL_SECONDARY_FRAME` instead of replacing the primary modal. On successful create, the secondary closes and the primary frame reloads so the new record appears in the select, no developer wiring.
129
129
 
130
130
  For custom flows: `helpers.turbo_stream_close_frame(frame_id)` and `helpers.turbo_stream_reload_frame(frame_id)` are available.
131
131
 
@@ -133,11 +133,11 @@ See [Forms › Association inputs](./forms#association-inputs).
133
133
 
134
134
  ## Modals & slideovers
135
135
 
136
- The framework's `:new` / `:edit` actions and any interactive action render inline inside a modal. Choose the chrome (and optional width) per-resource via the definition — interactive actions inherit the same default:
136
+ The framework's `:new` / `:edit` actions and any interactive action render inline inside a modal. Choose the chrome (and optional width) per-resource via the definition, interactive actions inherit the same default:
137
137
 
138
138
  ```ruby
139
139
  class PostDefinition < ResourceDefinition
140
- modal :slideover # default — slide-in panel from the right
140
+ modal :slideover # default, slide-in panel from the right
141
141
  # modal :centered # centered dialog
142
142
  # modal :centered, size: :lg # centered, wider container
143
143
  # modal false # full standalone pages (no modal)
@@ -148,7 +148,7 @@ end
148
148
 
149
149
  ## Tabs on the show page
150
150
 
151
- Show pages with `permitted_associations` (see [Behavior › Policy](/reference/behavior/policies#association-permissions)) render a tablist: **Details** tab first, then one tab per association. The active tab is reflected in the URL hash (`#products`, `#refund-requests`) so the page deep-links and the active state survives reload / back navigation. Tab rows scroll horizontally on narrow viewports — they don't wrap.
151
+ Show pages with `permitted_associations` (see [Behavior › Policy](/reference/behavior/policies#association-permissions)) render a tablist: **Details** tab first, then one tab per association. The active tab is reflected in the URL hash (`#products`, `#refund-requests`) so the page deep-links and the active state survives reload / back navigation. Tab rows scroll horizontally on narrow viewports, they don't wrap.
152
152
 
153
153
  If the policy permits **no fields**, the empty Details tab is dropped and the first association tab leads instead.
154
154
 
@@ -169,7 +169,7 @@ end
169
169
 
170
170
  ## Available context
171
171
 
172
- Inside any page / form / display / Phlex component, the same set of helpers is available — model accessors, definition/policy methods, URL helpers, `current_user`. For the full list, see [Behavior › Controllers › Key methods](/reference/behavior/controllers#key-methods) — pages inherit the same surface.
172
+ Inside any page / form / display / Phlex component, the same set of helpers is available (model accessors, definition/policy methods, URL helpers, `current_user`; for the full list, see [Behavior › Controllers › Key methods](/reference/behavior/controllers#key-methods)). Pages inherit the same surface.
173
173
 
174
174
  In Phlex components, Rails helpers are accessed via the `helpers` proxy:
175
175
 
@@ -184,9 +184,9 @@ end
184
184
 
185
185
  ## Related
186
186
 
187
- - [Forms](./forms) — Form class, field builder, themes
188
- - [Displays](./displays) — show-page Display class
189
- - [Tables](./tables) — index-page Table class
190
- - [Components](./components) — built-in component kit, custom Phlex components, DynaFrame
191
- - [Layouts](./layouts) — overall shell, eject, ResourceLayout
192
- - [Resource › Definition](/reference/resource/definition) — page titles, breadcrumbs, modal mode, metadata panel
187
+ - [Forms](./forms): Form class, field builder, themes
188
+ - [Displays](./displays): show-page Display class
189
+ - [Tables](./tables): index-page Table class
190
+ - [Components](./components): built-in component kit, custom Phlex components, DynaFrame
191
+ - [Layouts](./layouts): overall shell, eject, ResourceLayout
192
+ - [Resource › Definition](/reference/resource/definition): page titles, breadcrumbs, modal mode, metadata panel
@@ -52,11 +52,11 @@ class PostDefinition < ResourceDefinition
52
52
  column :status, align: :center
53
53
  column :amount, align: :end
54
54
 
55
- # formatter — receives just the value
55
+ # formatter, receives just the value
56
56
  column :description, formatter: ->(value) { value&.truncate(30) }
57
57
  column :price, formatter: ->(value) { "$%.2f" % value if value }
58
58
 
59
- # block — receives the full record
59
+ # block, receives the full record
60
60
  column :full_name do |record|
61
61
  "#{record.first_name} #{record.last_name}"
62
62
  end
@@ -67,7 +67,7 @@ See [Resource › Definition › Column options](/reference/resource/definition#
67
67
 
68
68
  ## Grid view
69
69
 
70
- For card-based layouts as a switchable alternative to the table, use the built-in Grid view — declare `grid_fields` in the definition:
70
+ For card-based layouts as a switchable alternative to the table, use the built-in Grid view, declare `grid_fields` in the definition:
71
71
 
72
72
  ```ruby
73
73
  class UserDefinition < ResourceDefinition
@@ -115,7 +115,7 @@ end
115
115
 
116
116
  ## Related
117
117
 
118
- - [Pages](./pages) — `IndexPage` render hooks (a lighter alternative for top/bottom chrome)
119
- - [Components](./components) — `PostCardComponent` and other reusable Phlex pieces
120
- - [Resource › Definition](/reference/resource/definition) — column configuration, grid view
121
- - [Resource › Query](/reference/resource/query) — search, filters, scopes, sort
118
+ - [Pages](./pages): `IndexPage` render hooks (a lighter alternative for top/bottom chrome)
119
+ - [Components](./components): `PostCardComponent` and other reusable Phlex pieces
120
+ - [Resource › Definition](/reference/resource/definition): column configuration, grid view
121
+ - [Resource › Query](/reference/resource/query): search, filters, scopes, sort