plutonium 0.65.0 → 0.66.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 (104) 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 +144 -133
  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 +151 -96
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +21 -0
  15. data/README.md +9 -9
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/docs/.vitepress/sync-skills.mjs +6 -3
  19. data/docs/blog/introducing-plutonium-dashboards.md +4 -5
  20. data/docs/blog/introducing-plutonium-i18n.md +4 -5
  21. data/docs/getting-started/installation.md +5 -5
  22. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  23. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  24. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  25. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  26. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  27. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  28. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  29. data/docs/getting-started/tutorial/index.md +1 -1
  30. data/docs/guides/adding-resources.md +10 -7
  31. data/docs/guides/authentication.md +25 -25
  32. data/docs/guides/authorization.md +24 -24
  33. data/docs/guides/creating-packages.md +17 -17
  34. data/docs/guides/custom-actions.md +32 -32
  35. data/docs/guides/customizing-ui.md +29 -26
  36. data/docs/guides/dashboards.md +1 -1
  37. data/docs/guides/index.md +3 -3
  38. data/docs/guides/kanban.md +55 -55
  39. data/docs/guides/multi-tenancy.md +35 -22
  40. data/docs/guides/nested-resources.md +21 -21
  41. data/docs/guides/performance.md +3 -3
  42. data/docs/guides/search-filtering.md +13 -13
  43. data/docs/guides/testing.md +16 -12
  44. data/docs/guides/theming.md +32 -17
  45. data/docs/guides/troubleshooting.md +2 -2
  46. data/docs/guides/user-invites.md +17 -17
  47. data/docs/guides/user-profile.md +51 -24
  48. data/docs/guides/wizards.md +55 -55
  49. data/docs/reference/app/generators.md +22 -22
  50. data/docs/reference/app/index.md +15 -18
  51. data/docs/reference/app/packages.md +8 -8
  52. data/docs/reference/app/portals.md +75 -29
  53. data/docs/reference/auth/accounts.md +15 -15
  54. data/docs/reference/auth/index.md +12 -12
  55. data/docs/reference/auth/profile.md +67 -29
  56. data/docs/reference/behavior/async-interactions.md +24 -24
  57. data/docs/reference/behavior/controllers.md +28 -28
  58. data/docs/reference/behavior/index.md +5 -5
  59. data/docs/reference/behavior/interactions.md +44 -44
  60. data/docs/reference/behavior/policies.md +48 -28
  61. data/docs/reference/configuration.md +6 -6
  62. data/docs/reference/dashboard/dsl.md +2 -2
  63. data/docs/reference/dashboard/index.md +1 -1
  64. data/docs/reference/generators/lite.md +7 -7
  65. data/docs/reference/i18n.md +23 -0
  66. data/docs/reference/index.md +1 -1
  67. data/docs/reference/kanban/authorization.md +9 -9
  68. data/docs/reference/kanban/dsl.md +32 -32
  69. data/docs/reference/kanban/index.md +1 -1
  70. data/docs/reference/kanban/positioning.md +17 -15
  71. data/docs/reference/resource/actions.md +51 -51
  72. data/docs/reference/resource/definition.md +73 -73
  73. data/docs/reference/resource/export.md +6 -6
  74. data/docs/reference/resource/index.md +16 -16
  75. data/docs/reference/resource/model.md +24 -24
  76. data/docs/reference/resource/positioning.md +78 -76
  77. data/docs/reference/resource/query.md +13 -13
  78. data/docs/reference/tenancy/entity-scoping.md +65 -35
  79. data/docs/reference/tenancy/index.md +11 -11
  80. data/docs/reference/tenancy/invites.md +20 -20
  81. data/docs/reference/tenancy/nested-resources.md +13 -13
  82. data/docs/reference/testing/index.md +116 -22
  83. data/docs/reference/ui/assets.md +57 -25
  84. data/docs/reference/ui/components.md +20 -20
  85. data/docs/reference/ui/displays.md +14 -14
  86. data/docs/reference/ui/forms.md +35 -35
  87. data/docs/reference/ui/index.md +17 -15
  88. data/docs/reference/ui/layouts.md +21 -21
  89. data/docs/reference/ui/pages.md +22 -22
  90. data/docs/reference/ui/tables.md +7 -7
  91. data/docs/reference/wizard/anchoring-resume.md +33 -32
  92. data/docs/reference/wizard/dsl.md +44 -44
  93. data/docs/reference/wizard/index.md +6 -6
  94. data/docs/reference/wizard/one-time.md +18 -18
  95. data/docs/reference/wizard/registration-launch.md +32 -32
  96. data/docs/reference/wizard/storage-config.md +23 -23
  97. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  98. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  99. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  100. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  101. data/lib/plutonium/version.rb +1 -1
  102. data/package.json +1 -1
  103. data/src/css/components.css +10 -10
  104. metadata +2 -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
@@ -152,7 +152,7 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
152
152
  | `input_tag` | text (auto-detected type) |
153
153
  | `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
154
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. |
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
 
@@ -180,8 +180,8 @@ end
180
180
 
181
181
  `as: :phone` renders an [intl-tel-input](https://github.com/jackocnr/intl-tel-input) field. Forward library options two ways:
182
182
 
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.
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.
185
185
 
186
186
  ```ruby
187
187
  input :phone, as: :phone, initial_country: "gh"
@@ -207,14 +207,14 @@ The field defaults to `strictMode: true`; override it via `intl_options: {strict
207
207
  ```ruby
208
208
  input :price, as: :currency # unit from has_cents / config / i18n
209
209
  input :price, as: :currency, unit: "£" # a literal symbol
210
- input :price, as: :currency, unit: false # no prefix — a plain number input
210
+ input :price, as: :currency, unit: false # no prefix, a plain number input
211
211
  ```
212
212
 
213
213
  When nothing resolves (or `unit: false`), the prefix is omitted and it's an ordinary number input.
214
214
 
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.
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.
216
216
 
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.
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.
218
218
 
219
219
  ### Password & secret fields {#password-fields}
220
220
 
@@ -222,10 +222,10 @@ In a **wizard** step the data snapshot has no `has_cents` reflection, so there's
222
222
 
223
223
  | field state | result |
224
224
  |---|---|
225
- | untouched (sentinel) | kept — the stored secret is left unchanged |
225
+ | untouched (sentinel) | kept, the stored secret is left unchanged |
226
226
  | 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 |
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 |
229
229
  | typed | set as the new value |
230
230
 
231
231
  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,7 +237,7 @@ The sentinel is guarded client-side by the `password-sentinel` Stimulus controll
237
237
  - ends with `_password`, `_digest`, `_hash`, `_token`, `_key`, or `_salt`;
238
238
  - contains `secret`.
239
239
 
240
- This is a naming convenience, **not** a security guarantee — tune it per field:
240
+ This is a naming convenience, **not** a security guarantee, tune it per field:
241
241
 
242
242
  ```ruby
243
243
  # Opt OUT: render the value as a normal, readable text input
@@ -254,16 +254,16 @@ field :recovery_phrase, as: :password
254
254
 
255
255
  ### Wrapped vs unwrapped
256
256
 
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.
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.
260
260
 
261
261
  ## Association inputs (`secure_association_tag`) {#association-inputs}
262
262
 
263
263
  Association inputs render with two affordances out of the box:
264
264
 
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.
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.
267
267
 
268
268
  ```ruby
269
269
  # Opt out of the + button
@@ -280,7 +280,7 @@ input :author, label_method: :email
280
280
  ```
281
281
 
282
282
  ::: 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.
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.
284
284
  :::
285
285
 
286
286
  ## Submit buttons
@@ -291,7 +291,7 @@ Control the secondary button via the definition:
291
291
 
292
292
  ```ruby
293
293
  class PostDefinition < ResourceDefinition
294
- submit_and_continue false # nil (default — auto), true (always show), false (always hide)
294
+ submit_and_continue false # nil (default, auto), true (always show), false (always hide)
295
295
  end
296
296
  ```
297
297
 
@@ -313,9 +313,9 @@ end
313
313
 
314
314
  These all live in the definition layer:
315
315
 
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)
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)
319
319
 
320
320
  ## Theming
321
321
 
@@ -342,7 +342,7 @@ end
342
342
  ```
343
343
 
344
344
  ::: 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.
345
+ Don't replace the theme wholesale, Plutonium's defaults handle invalid states, focus rings, and dark mode. `super.merge` keeps them.
346
346
  :::
347
347
 
348
348
  ### Theme keys
@@ -384,9 +384,9 @@ end
384
384
 
385
385
  ## Related
386
386
 
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
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
@@ -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