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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +43 -43
- data/.claude/skills/plutonium-app/SKILL.md +101 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +144 -133
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +151 -96
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +21 -0
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/blog/introducing-plutonium-dashboards.md +4 -5
- data/docs/blog/introducing-plutonium-i18n.md +4 -5
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +1 -1
- data/docs/guides/index.md +3 -3
- data/docs/guides/kanban.md +55 -55
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- data/docs/reference/app/generators.md +22 -22
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +75 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +6 -6
- data/docs/reference/dashboard/dsl.md +2 -2
- data/docs/reference/dashboard/index.md +1 -1
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +23 -0
- data/docs/reference/index.md +1 -1
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +32 -32
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +17 -15
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +73 -73
- data/docs/reference/resource/export.md +6 -6
- data/docs/reference/resource/index.md +16 -16
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/resource/positioning.md +78 -76
- data/docs/reference/resource/query.md +13 -13
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +57 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +14 -14
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +44 -44
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +10 -10
- metadata +2 -2
data/docs/reference/ui/forms.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
50
|
+
### Sectioned form (declarative: preferred)
|
|
51
51
|
|
|
52
|
-
Declare sections in the **definition** using `form_layout`. The form picks up the layout automatically
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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
|
|
184
|
-
- `intl_options
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
228
|
-
| emptied | explicit clear (clear-by-blank)
|
|
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
|
|
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
|
|
258
|
-
- Bare tag
|
|
259
|
-
- `wrapped(class: "...")
|
|
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
|
|
266
|
-
- **Typeahead
|
|
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
|
|
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
|
|
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
|
|
317
|
-
- **Nested inputs** (`nested_input :variants`)
|
|
318
|
-
- **Interaction forms
|
|
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
|
|
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)
|
|
388
|
-
- [Components](./components)
|
|
389
|
-
- [Assets](./assets)
|
|
390
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
391
|
-
- [Behavior › Interactions](/reference/behavior/interactions)
|
|
392
|
-
- [Tenancy › Nested resources](/reference/tenancy/nested-resources)
|
|
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
|
data/docs/reference/ui/index.md
CHANGED
|
@@ -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)
|
|
8
|
-
- [Forms](./forms)
|
|
9
|
-
- [Displays](./displays)
|
|
10
|
-
- [Tables](./tables)
|
|
11
|
-
- [Components](./components)
|
|
12
|
-
- [Layouts](./layouts)
|
|
13
|
-
- [Assets](./assets)
|
|
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
|
|
20
|
-
- **Custom components inherit `Plutonium::UI::Component::Base
|
|
21
|
-
- **`render_actions` is mandatory in custom `form_template
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
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)
|
|
30
|
-
- [Behavior › Controllers](/reference/behavior/controllers)
|
|
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
|
|
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
|
|
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)
|
|
24
|
-
- `:plain
|
|
25
|
-
- `:classic
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78
|
-
- `pu-sticky-footer
|
|
79
|
-
- `html.pu-no-rail
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
203
|
-
- [Components](./components)
|
|
204
|
-
- [Pages](./pages)
|
|
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)
|
data/docs/reference/ui/pages.md
CHANGED
|
@@ -32,12 +32,10 @@ end
|
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
34
34
|
class PostDefinition < ResourceDefinition
|
|
35
|
-
index_page_title "
|
|
36
|
-
index_page_description "
|
|
37
|
-
show_page_title "Article Details"
|
|
38
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
188
|
-
- [Displays](./displays)
|
|
189
|
-
- [Tables](./tables)
|
|
190
|
-
- [Components](./components)
|
|
191
|
-
- [Layouts](./layouts)
|
|
192
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
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
|
data/docs/reference/ui/tables.md
CHANGED
|
@@ -52,11 +52,11 @@ class PostDefinition < ResourceDefinition
|
|
|
52
52
|
column :status, align: :center
|
|
53
53
|
column :amount, align: :end
|
|
54
54
|
|
|
55
|
-
# formatter
|
|
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
|
|
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
|
|
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)
|
|
119
|
-
- [Components](./components)
|
|
120
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
121
|
-
- [Resource › Query](/reference/resource/query)
|
|
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
|