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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Testing Reference
|
|
2
2
|
|
|
3
|
-
`Plutonium::Testing` provides scaffolded integration tests that assert a resource × portal pairing
|
|
3
|
+
`Plutonium::Testing` provides scaffolded integration tests that assert a resource × portal pairing: CRUD, policy matrix, definition smoke tests, model concerns (associated_with, SGID, has_cents), nested-resource scope boundaries, cross-portal access, and interaction outcomes. All optional, all opt-in.
|
|
4
4
|
|
|
5
5
|
## 🚨 Critical
|
|
6
6
|
|
|
7
7
|
- **Use the generators.** `pu:test:install` once per app, then `pu:test:scaffold ResourceClass --portals=...` per resource × portal. Hand-written test files drift from conventions.
|
|
8
|
-
- **Tests are opt-in.** `Plutonium::Testing` is only loaded when `require "plutonium/testing"` runs
|
|
8
|
+
- **Tests are opt-in.** `Plutonium::Testing` is only loaded when `require "plutonium/testing"` runs; it's never autoloaded, never present in production.
|
|
9
9
|
- **One file per (resource × portal).** Same model in admin and org portals = two test files. Each portal has different auth, scoping, and allowed actions.
|
|
10
|
-
- **Stub methods are required.** Concerns ship with `NotImplementedError` stubs
|
|
10
|
+
- **Stub methods are required.** Concerns ship with `NotImplementedError` stubs: your test class supplies the test data via `create_resource!`, `valid_create_params`, `policy_roles`, etc.
|
|
11
11
|
|
|
12
12
|
## Quick start
|
|
13
13
|
|
|
@@ -88,6 +88,11 @@ class AdminPortal::BloggingPostsTest < ActionDispatch::IntegrationTest
|
|
|
88
88
|
end
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
**`valid_update_params` is also the assertion.** After the PATCH, the update test runs `assert_equal value, record.reload.public_send(attr)` for every key, so:
|
|
92
|
+
|
|
93
|
+
- Use values that read back identically. Enums go in as strings (`status: "published"`): the enum reader returns a String, so `:published` fails.
|
|
94
|
+
- Keep association SGIDs out of it. The loop only skips values starting with `gid://`, and `to_sgid.to_s` is a signed token, so the token gets compared to the associated record. Test reassigning an association in its own `test` block that PATCHes the SGID and asserts `record.reload.user == other_user`. `valid_create_params` has no such check, so SGIDs are fine there.
|
|
95
|
+
|
|
91
96
|
### `Plutonium::Testing::ResourcePolicy`
|
|
92
97
|
|
|
93
98
|
Asserts the `permit?` matrix across action × role and verifies `relation_scope` returns an `ActiveRecord::Relation`.
|
|
@@ -133,18 +138,31 @@ Outcome-assertion helpers for `Plutonium::Resource::Interaction` subclasses.
|
|
|
133
138
|
|
|
134
139
|
- `assert_interaction_success(klass, **input)` → returns the success outcome
|
|
135
140
|
- `assert_interaction_failure(klass, **input)` → returns the failure outcome
|
|
136
|
-
- `interaction_view_context` (overridable) →
|
|
141
|
+
- `interaction_view_context` (overridable) → the view context both helpers pass in (a mock by default); override it only when the interaction reads from the view context
|
|
142
|
+
|
|
143
|
+
Use the helpers for both outcomes; they build the interaction and call it for you.
|
|
137
144
|
|
|
138
145
|
```ruby
|
|
139
|
-
test "
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
test "PublishProduct moves a draft to active" do
|
|
147
|
+
product = create_product!(status: :draft)
|
|
148
|
+
assert_interaction_success(Catalog::PublishProduct, resource: product)
|
|
149
|
+
assert product.reload.active?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
test "PublishProduct fails for a product that isn't a draft" do
|
|
153
|
+
product = create_product!(status: :active)
|
|
154
|
+
assert_interaction_failure(Catalog::PublishProduct, resource: product)
|
|
155
|
+
assert product.reload.active? # state unchanged
|
|
142
156
|
end
|
|
143
157
|
```
|
|
144
158
|
|
|
159
|
+
The failure outcome carries no validation errors (they live on the interaction instance), so assert the unchanged state rather than building the interaction by hand to read `errors`.
|
|
160
|
+
|
|
161
|
+
`ResourceInteraction` includes neither the DSL nor `AuthHelpers`. A file scaffolded with only `--concerns=interaction` still has the template's `resource_tests_for` and `login_as(@account)`, which raise `NoMethodError`: delete both (the helpers need no portal and no login).
|
|
162
|
+
|
|
145
163
|
### `Plutonium::Testing::ResourceModel`
|
|
146
164
|
|
|
147
|
-
Tests `associated_with` scope, SGID routing, and `has_cents` accessors
|
|
165
|
+
Tests `associated_with` scope, SGID routing, and `has_cents` accessors, gated by DSL flags.
|
|
148
166
|
|
|
149
167
|
**Stubs:**
|
|
150
168
|
|
|
@@ -167,13 +185,77 @@ Asserts CRUD under a parent + scope-boundary tests (sibling tenants invisible).
|
|
|
167
185
|
|
|
168
186
|
**Stubs:**
|
|
169
187
|
|
|
170
|
-
- `parent_record!` → current tenant
|
|
188
|
+
- `parent_record!` → current tenant (called several times per test, so return the same record each time, e.g. `@org`)
|
|
171
189
|
- `other_parent_record!` → sibling tenant
|
|
172
190
|
- `create_resource!(parent:)` → persisted record under given parent
|
|
173
191
|
|
|
192
|
+
The concern builds its URLs as `"#{current_path_prefix}/#{parent.id}/#{collection}"`, so it expects the bare portal mount as the prefix and inserts the parent id itself.
|
|
193
|
+
|
|
194
|
+
#### Entity-scoped portals: CRUD + tenant isolation
|
|
195
|
+
|
|
196
|
+
In a portal that calls `scope_to_entity Model, strategy: :path` (e.g. `/org/:id/...`), "records from another tenant aren't reachable" is the `NestedResource` concern with the entity as the parent. The resolved prefix there is the bare mount (`/org`), and the two concerns need different prefixes and different `create_resource!` signatures:
|
|
197
|
+
|
|
198
|
+
| | `ResourceCrud` | `NestedResource` |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| URL built | `prefix/collection` | `prefix/parent.id/collection` |
|
|
201
|
+
| Prefix needed | `/org/#{@org.to_param}` (override `current_path_prefix`) | `/org` (the resolved default) |
|
|
202
|
+
| Calls | `create_resource!` | `create_resource!(parent:)` |
|
|
203
|
+
|
|
204
|
+
One class can't satisfy both, so split the scaffolded file into two classes:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
rails g pu:test:scaffold Catalog::Variant --portals=org --concerns=crud,nested --parent=organization
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class OrgPortal::CatalogVariantTest < ActionDispatch::IntegrationTest
|
|
212
|
+
include IntegrationTestHelper
|
|
213
|
+
include Plutonium::Testing::ResourceCrud
|
|
214
|
+
|
|
215
|
+
resource_tests_for Catalog::Variant, portal: :org
|
|
216
|
+
|
|
217
|
+
setup do
|
|
218
|
+
@org = create_organization!
|
|
219
|
+
@user = create_user!
|
|
220
|
+
create_membership!(organization: @org, user: @user)
|
|
221
|
+
@product = create_product!(user: @user, organization: @org)
|
|
222
|
+
login_as(@user) # :org logs in through /users/login
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def current_path_prefix = "/org/#{@org.to_param}"
|
|
226
|
+
def create_resource! = create_variant!(product: @product)
|
|
227
|
+
def valid_create_params = {name: "Red", sku: "RED-1", stock_count: 5, product: @product.to_sgid.to_s}
|
|
228
|
+
def valid_update_params = {name: "Red / Large"}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
class OrgPortal::CatalogVariantNestedTest < ActionDispatch::IntegrationTest
|
|
232
|
+
include IntegrationTestHelper
|
|
233
|
+
include Plutonium::Testing::NestedResource
|
|
234
|
+
|
|
235
|
+
resource_tests_for Catalog::Variant, portal: :org, parent: :organization
|
|
236
|
+
|
|
237
|
+
setup do
|
|
238
|
+
@org = create_organization!
|
|
239
|
+
@other_org = create_organization!
|
|
240
|
+
@user = create_user!
|
|
241
|
+
create_membership!(organization: @org, user: @user)
|
|
242
|
+
login_as(@user)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def parent_record! = @org
|
|
246
|
+
def other_parent_record! = @other_org
|
|
247
|
+
|
|
248
|
+
def create_resource!(parent:)
|
|
249
|
+
create_variant!(product: create_product!(organization: parent))
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`create_resource!(parent:)` must create under `parent`, not always under `@org`: the isolation test passes `other_parent_record!` and expects a 404 (or redirect) for that record.
|
|
255
|
+
|
|
174
256
|
### `Plutonium::Testing::PortalAccess`
|
|
175
257
|
|
|
176
|
-
Cross-portal access boundaries. Uses its own DSL
|
|
258
|
+
Cross-portal access boundaries. Uses its own DSL (NOT `resource_tests_for`).
|
|
177
259
|
|
|
178
260
|
```ruby
|
|
179
261
|
class PortalAccessTest < ActionDispatch::IntegrationTest
|
|
@@ -210,7 +292,16 @@ Generates one test per (role × portal). Allowed = `200 | 302`; blocked = `302 |
|
|
|
210
292
|
|
|
211
293
|
## Auth helpers
|
|
212
294
|
|
|
213
|
-
`Plutonium::Testing::AuthHelpers
|
|
295
|
+
`login_as` and friends come from `Plutonium::Testing::AuthHelpers`, which only some concerns pull in. The default portal comes from the DSL (`resource_tests_for`), which is a separate include:
|
|
296
|
+
|
|
297
|
+
| Concern | `login_as` available | Bare `login_as(account)` works |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `ResourceCrud`, `NestedResource` | yes | yes (portal from `resource_tests_for`) |
|
|
300
|
+
| `PortalAccess` | yes | no: pass `portal:` every time |
|
|
301
|
+
| `ResourcePolicy`, `ResourceDefinition`, `ResourceModel` | no | no |
|
|
302
|
+
| `ResourceInteraction` | no | no |
|
|
303
|
+
|
|
304
|
+
A hand-written integration test that only includes the app's own helpers (e.g. an `IntegrationTestHelper`) has no `login_as` at all. Add `include Plutonium::Testing::AuthHelpers` (and `require "plutonium/testing"` if `test_helper.rb` doesn't) and pass `portal:` explicitly. Likewise, delete the scaffold's `login_as(@account)` from a file whose concerns don't provide it.
|
|
214
305
|
|
|
215
306
|
```ruby
|
|
216
307
|
login_as(account) # uses portal from the DSL
|
|
@@ -255,16 +346,18 @@ rails g pu:test:scaffold Blogging::Post --portals=org --parent=organization --de
|
|
|
255
346
|
|---|---|---|
|
|
256
347
|
| `--portals=admin,org` | required | Emit one file per portal |
|
|
257
348
|
| `--concerns=...` | `crud,policy,definition` | Concerns to include (`crud`, `policy`, `definition`, `nested`, `model`, `interaction`, `portal_access`) |
|
|
258
|
-
| `--parent=organization` | |
|
|
349
|
+
| `--parent=organization` | | Adds `parent:` to `resource_tests_for` and, only together with `nested` in `--concerns`, the `parent_record!`/`other_parent_record!` stubs |
|
|
259
350
|
| `--dest=main_app\|<package>` | `main_app` | Output destination |
|
|
260
351
|
|
|
261
352
|
Output path: `test/integration/<portal>_portal/<resource_underscored>_test.rb`.
|
|
262
353
|
|
|
354
|
+
`--parent` alone does not add the `NestedResource` include; pass `--concerns=crud,nested --parent=organization`. The template is one class with every requested include, a `setup` that calls `login_as(@account)`, and a no-argument `create_resource!`. Treat it as a starting point: drop `login_as`/`resource_tests_for` where the concerns don't provide them (see [Auth helpers](#auth-helpers)), and split `crud` and `nested` into two classes for [entity-scoped portals](#entity-scoped-portals-crud-tenant-isolation).
|
|
355
|
+
|
|
263
356
|
## Customization & escape hatches
|
|
264
357
|
|
|
265
358
|
- **Skip individual tests:** `resource_tests_for Klass, portal: :admin, skip: %i[destroy]`
|
|
266
359
|
- **Restrict action set:** `resource_tests_for Klass, portal: :admin, actions: %i[index show]`
|
|
267
|
-
- **Custom assertions:** add regular `test "..."` blocks alongside the generated matrix
|
|
360
|
+
- **Custom assertions:** add regular `test "..."` blocks alongside the generated matrix; they coexist.
|
|
268
361
|
- **Non-Rodauth auth:** override `sign_in_for_tests`. See [AuthHelpers](#auth-helpers).
|
|
269
362
|
- **Custom path prefix:** `path_prefix: "/v2/admin"` overrides portal resolution.
|
|
270
363
|
|
|
@@ -273,15 +366,16 @@ Output path: `test/integration/<portal>_portal/<resource_underscored>_test.rb`.
|
|
|
273
366
|
- **Forgotten stubs raise `NotImplementedError`** with the stub name. Look for the missing method in your test class.
|
|
274
367
|
- **Portal mismatch:** `:admin` portal expects `AdminPortal::Engine` constant. If your portal is named differently, pass `path_prefix:` explicitly.
|
|
275
368
|
- **Tenant leakage in stubs:** `create_resource!` for an org portal must return a record bound to the test's `@org`. Otherwise scope filtering tests pass for the wrong reason.
|
|
276
|
-
- **`policy_record` for tenant-scoped resources** must belong to a tenant the role has access to
|
|
277
|
-
- **Nested
|
|
278
|
-
-
|
|
369
|
+
- **`policy_record` for tenant-scoped resources** must belong to a tenant the role has access to; otherwise even allowed roles will see `false`.
|
|
370
|
+
- **Nested paths come from `parent_record!.id`**, so it must return a real, persisted tenant the logged-in account belongs to. `parent: :foo` in the DSL documents the relationship; the concern doesn't read it.
|
|
371
|
+
- **Entity-scoped CRUD hitting `/org/<collection>`** (404 or routing error): a `:path` portal resolves to the bare mount, so override `current_path_prefix` to include the tenant. Don't do this in a `NestedResource` class, which adds the id itself.
|
|
372
|
+
- **`PortalAccess` doesn't use `resource_tests_for`**: use `portal_access_for` instead. Mixing them on the same class is undefined behavior.
|
|
279
373
|
|
|
280
374
|
## Related
|
|
281
375
|
|
|
282
|
-
- [Behavior › Policy](/reference/behavior/policies)
|
|
283
|
-
- [Behavior › Interaction](/reference/behavior/interactions)
|
|
284
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
285
|
-
- [Tenancy](/reference/tenancy/)
|
|
286
|
-
- [Auth](/reference/auth/)
|
|
287
|
-
- [Guides › Testing](/guides/testing)
|
|
376
|
+
- [Behavior › Policy](/reference/behavior/policies): the policy methods `ResourcePolicy` verifies
|
|
377
|
+
- [Behavior › Interaction](/reference/behavior/interactions): interaction outcomes asserted by `ResourceInteraction`
|
|
378
|
+
- [Resource › Definition](/reference/resource/definition): definition props the smoke test introspects
|
|
379
|
+
- [Tenancy](/reference/tenancy/): parent scoping (`NestedResource`), entity strategies (drive auth/scoping)
|
|
380
|
+
- [Auth](/reference/auth/): Rodauth setup behind the default `sign_in_for_tests`
|
|
381
|
+
- [Guides › Testing](/guides/testing): task-oriented walkthrough
|
data/docs/reference/ui/assets.md
CHANGED
|
@@ -4,11 +4,12 @@ TailwindCSS 4 + Stimulus toolchain. CSS design tokens for theming, `.pu-*` compo
|
|
|
4
4
|
|
|
5
5
|
## 🚨 Critical
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
7
|
+
- **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.
|
|
8
|
+
- **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 controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead.
|
|
9
|
+
- **Use `plutoniumTailwindConfig.merge`** when overriding the theme, plain object spread drops Plutonium's defaults.
|
|
10
|
+
- **Tokens are CSS variables**, not Tailwind keys, `bg-[var(--pu-surface)]`, NOT `bg-pu-surface`.
|
|
11
|
+
- **Dark mode uses `selector`** strategy, toggle `dark` on `<html>`. The bundled `color-mode` controller does this.
|
|
12
|
+
- **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. See [Component classes](#component-classes-pu).
|
|
12
13
|
|
|
13
14
|
## Asset configuration
|
|
14
15
|
|
|
@@ -30,13 +31,19 @@ end
|
|
|
30
31
|
rails generate pu:core:assets
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
Until this runs, the app serves the gem's prebuilt assets (`config.assets.stylesheet` defaults to `plutonium.css`, `script` to `plutonium.min.js`). Those are compiled from the gem's own sources, so app-side Tailwind classes, a new `primary` palette, token overrides and custom Stimulus controllers have nowhere to go. The generator:
|
|
34
35
|
|
|
35
|
-
1. Installs
|
|
36
|
-
2.
|
|
37
|
-
3.
|
|
38
|
-
4.
|
|
39
|
-
5.
|
|
36
|
+
1. Installs `@radioactive-labs/plutonium` (pinned to the gem version), Tailwind 4 and the PostCSS plugins.
|
|
37
|
+
2. Writes `tailwind.config.js` (through `plutoniumTailwindConfig.merge`) and `postcss.config.js`.
|
|
38
|
+
3. Prepends `@import "gem:plutonium/src/css/plutonium.css";` to `application.tailwind.css` and adds `@config` after `@import "tailwindcss";`.
|
|
39
|
+
4. Appends `registerControllers(application)` to `app/javascript/controllers/index.js`.
|
|
40
|
+
5. Sets `config.assets.stylesheet = "application"` and `config.assets.script = "application"`, and writes the `build` / `build:css` scripts in `package.json`.
|
|
41
|
+
|
|
42
|
+
Step 5 is why `registerControllers` is not optional: the gem's `plutonium.min.js` calls it itself, and your `application.js` replaces that bundle.
|
|
43
|
+
|
|
44
|
+
### Prerequisites
|
|
45
|
+
|
|
46
|
+
The generator aborts unless `app/assets/stylesheets/application.tailwind.css` and `app/javascript/controllers/index.js` exist, i.e. an app created with `-j esbuild -c tailwind` plus Stimulus. For an app without them, install the bundlers first (`bin/rails javascript:install:esbuild`, `css:install:tailwind`, `stimulus:install` from jsbundling-rails, cssbundling-rails and stimulus-rails), then run the generator. Don't hand-write `tailwind.config.js` / `postcss.config.js` instead: the generated ones resolve the gem path (`bundle show plutonium`) and load its `postcss-gem-import.cjs` so the `gem:` import works.
|
|
40
47
|
|
|
41
48
|
### Package managers
|
|
42
49
|
|
|
@@ -86,6 +93,8 @@ theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
|
86
93
|
})
|
|
87
94
|
```
|
|
88
95
|
|
|
96
|
+
These are Tailwind palette colors, compiled into the CSS at build time (`.pu-btn-primary` is `@apply bg-primary-600 ...`; `--pu-input-focus-ring` is `theme(colors.primary.500)`). Recoloring `primary` therefore means the `merge` above plus a rebuild, not a `--pu-*` override.
|
|
97
|
+
|
|
89
98
|
### Default color palette
|
|
90
99
|
|
|
91
100
|
| Color | Usage |
|
|
@@ -128,15 +137,15 @@ application.register("custom", CustomController)
|
|
|
128
137
|
|
|
129
138
|
### Bundled controllers
|
|
130
139
|
|
|
131
|
-
- `color-mode
|
|
132
|
-
- `form
|
|
133
|
-
- `nested-resource-form-fields
|
|
134
|
-
- `slim-select
|
|
135
|
-
- `flatpickr
|
|
136
|
-
- `easymde
|
|
140
|
+
- `color-mode`: dark/light mode toggle
|
|
141
|
+
- `form`: form handling (pre-submit, etc.)
|
|
142
|
+
- `nested-resource-form-fields`: nested form management
|
|
143
|
+
- `slim-select`: enhanced select boxes
|
|
144
|
+
- `flatpickr`: date/time pickers
|
|
145
|
+
- `easymde`: markdown editor
|
|
137
146
|
- Various internal UI controllers
|
|
138
147
|
|
|
139
|
-
### Custom Stimulus controller
|
|
148
|
+
### Custom Stimulus controller: standard pattern
|
|
140
149
|
|
|
141
150
|
```javascript
|
|
142
151
|
// app/javascript/controllers/custom_controller.js
|
|
@@ -249,9 +258,13 @@ Plutonium uses a comprehensive CSS custom-property system for consistent, themea
|
|
|
249
258
|
```
|
|
250
259
|
|
|
251
260
|
::: warning Mirror every `:root` override in `.dark`
|
|
252
|
-
Your stylesheet loads after Plutonium's, and `:root` and `.dark` have equal specificity
|
|
261
|
+
Your stylesheet loads after Plutonium's, and `:root` and `.dark` have equal specificity, so a token you override in `:root` beats Plutonium's `.dark` value even when dark mode is active. Any color token you customize in `:root` without re-asserting in `.dark` ships your light value into dark mode, where it's typically unreadable (e.g. a translucent dark `--pu-text-subtle` becomes invisible on a dark surface).
|
|
262
|
+
|
|
263
|
+
That includes the shadows: `src/css/tokens.css` redefines `--pu-shadow-sm/md/lg` (and every surface, text, border, table, input, card and chart token) under `.dark`, so a tinted light shadow left out of your `.dark` block replaces the dark one.
|
|
253
264
|
:::
|
|
254
265
|
|
|
266
|
+
Put dark values in a `.dark { ... }` block, not `@media (prefers-color-scheme: dark)`. Dark mode is the `dark` class on `<html>` (set by the `color-mode` controller), so a media query ignores the user's toggle. Overrides need the app's own stylesheet after the Plutonium import (`pu:core:assets`); never edit the gem's `tokens.css` / `components.css`.
|
|
267
|
+
|
|
255
268
|
### Using tokens in templates
|
|
256
269
|
|
|
257
270
|
```erb
|
|
@@ -279,7 +292,11 @@ end
|
|
|
279
292
|
|
|
280
293
|
## Component classes (`.pu-*`)
|
|
281
294
|
|
|
282
|
-
Ready-to-use styled components in `src/css/components.css`. **Prefer these over hardcoded `gray-X/dark:gray-Y`
|
|
295
|
+
Ready-to-use styled components in `src/css/components.css`. **Prefer these over hardcoded `gray-X/dark:gray-Y` (or `warning-50 dark:warning-950`) pairs.** In order of preference:
|
|
296
|
+
|
|
297
|
+
1. **A `.pu-*` class** (`pu-alert-warning`, `pu-badge-warning`, `pu-card`, `pu-btn-soft-danger`). Each ships with its own `.dark` rule and is always in the CSS, because `components.css` is part of `plutonium.css` whether the app uses the prebuilt file or imports it.
|
|
298
|
+
2. **A `var(--pu-*)` token** (`text-[var(--pu-text-muted)]`, `border-[var(--pu-border)]`) for layout around them. The token switches value under `.dark` and follows any theme override.
|
|
299
|
+
3. **Raw palette utilities** only for what neither covers. On the prebuilt `plutonium.css` they exist only if the gem's own sources happen to use them (its Tailwind `content` scans the gem, not your app); with your own build they compile, but each needs a hand-picked `dark:` twin that won't follow a rebrand.
|
|
283
300
|
|
|
284
301
|
### Buttons
|
|
285
302
|
|
|
@@ -318,6 +335,21 @@ Ready-to-use styled components in `src/css/components.css`. **Prefer these over
|
|
|
318
335
|
|
|
319
336
|
Rendered automatically by the `:badge` display (enums) and `:boolean` display (Yes/No pills). See [Displays](./displays#built-in-display-components).
|
|
320
337
|
|
|
338
|
+
### Alerts (inline banners)
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
.pu-alert / -success / -warning / -danger / -info
|
|
342
|
+
.pu-alert-message / .pu-alert-close
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
div(class: "pu-alert pu-alert-warning", role: "alert") do
|
|
347
|
+
div(class: "pu-alert-message") { t("blog.posts.flagged_comments", count: flagged) }
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The same banner the flash messages use (`app/views/plutonium/_flash_alerts.html.erb`), so it already has its dark-mode colors.
|
|
352
|
+
|
|
321
353
|
### Cards, panels, tables, toolbars, empty states
|
|
322
354
|
|
|
323
355
|
```
|
|
@@ -408,12 +440,12 @@ end
|
|
|
408
440
|
**Theme keys:** `wrapper`, `base`, `header`, `header_cell`, `body_row`, `body_cell`, `sort_icon`.
|
|
409
441
|
|
|
410
442
|
::: warning Always `super.merge(...)`
|
|
411
|
-
Don't replace the theme wholesale. Plutonium's defaults handle invalid states, focus rings, and dark mode
|
|
443
|
+
Don't replace the theme wholesale. Plutonium's defaults handle invalid states, focus rings, and dark mode, `super.merge` keeps them.
|
|
412
444
|
:::
|
|
413
445
|
|
|
414
446
|
## Gotchas
|
|
415
447
|
|
|
416
|
-
- **Stimulus controllers register silently fails.**
|
|
448
|
+
- **Stimulus controllers register silently fails.** Once `config.assets.script` points at the app's JS, if `registerControllers(application)` isn't called the entire UI's interactive layer is dead (color-mode toggle, slim-select, flatpickr, easymde, pre-submit). No error: just no behavior.
|
|
417
449
|
- **`plutoniumTailwindConfig.merge` is mandatory.** Plain spread drops defaults silently.
|
|
418
450
|
- **Tokens are CSS variables, not Tailwind keys.** Use `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
419
451
|
- **Dark mode is `selector`, not `class`.** Toggle via `document.documentElement.classList.toggle('dark')`.
|
|
@@ -421,6 +453,6 @@ Don't replace the theme wholesale. Plutonium's defaults handle invalid states, f
|
|
|
421
453
|
|
|
422
454
|
## Related
|
|
423
455
|
|
|
424
|
-
- [Forms › Theming](./forms#theming)
|
|
425
|
-
- [Components](./components)
|
|
426
|
-
- [Layouts](./layouts)
|
|
456
|
+
- [Forms › Theming](./forms#theming): Form theme keys + override pattern
|
|
457
|
+
- [Components](./components): `tokens` and `classes` helpers for conditional class composition
|
|
458
|
+
- [Layouts](./layouts): fonts, dark-mode toggle, body attributes
|
|
@@ -22,13 +22,13 @@ TablePagination(pagy)
|
|
|
22
22
|
Breadcrumbs()
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
These are shorthand for `render Plutonium::UI::PageHeader.new(...)` etc
|
|
25
|
+
These are shorthand for `render Plutonium::UI::PageHeader.new(...)` etc.; they work because every component class is exposed as a method on `Plutonium::UI::Component::Base`.
|
|
26
26
|
|
|
27
27
|
## Avatar
|
|
28
28
|
|
|
29
29
|
`Plutonium::UI::Avatar` renders a profile image for a subject. It resolves an optional image source and falls back to a deterministic avatar from the hosted [Navii](https://navii.dev) service, then to a generic user icon when there's nothing to show.
|
|
30
30
|
|
|
31
|
-

|
|
32
32
|
|
|
33
33
|
```ruby
|
|
34
34
|
Avatar(user) # Navii fallback seeded from the record
|
|
@@ -45,7 +45,7 @@ Avatar(src: "https://.../p.png") # a bare image, no subject/fallback
|
|
|
45
45
|
| `src:` | `nil` | The image. A **Symbol** names a method on the subject (`:avatar` → `subject.avatar`); otherwise an ActiveStorage attachment, [active_shrine](https://github.com/radioactive-labs/active_shrine)/Shrine uploader, or URL string. |
|
|
46
46
|
| `size:` | `:md` | Semantic `:xs 24 / :sm 32 / :md 40 / :lg 48 / :xl 64`, or a raw Integer (px). |
|
|
47
47
|
| `alt:` | derived | Defaults to the String subject, or the record's display name. |
|
|
48
|
-
| `class:` |
|
|
48
|
+
| `class:` | - | Merged over the default `rounded-full` classes. |
|
|
49
49
|
|
|
50
50
|
### How the source resolves
|
|
51
51
|
|
|
@@ -58,7 +58,7 @@ Avatar(src: "https://.../p.png") # a bare image, no subject/fallback
|
|
|
58
58
|
When `src` is absent or unattached, a Navii avatar is rendered from the subject; with no subject either, a generic user icon is shown.
|
|
59
59
|
|
|
60
60
|
::: tip Symbol `src` is a contract
|
|
61
|
-
`Avatar(user, src: :avatar)` calls `user.avatar
|
|
61
|
+
`Avatar(user, src: :avatar)` calls `user.avatar`, the subject **must** respond to it (a `NoMethodError` is raised otherwise). Use a Symbol `src` only with a record subject, not a value that might be a plain string (e.g. a guest `current_user`).
|
|
62
62
|
:::
|
|
63
63
|
|
|
64
64
|
### Privacy
|
|
@@ -94,7 +94,7 @@ class PostCardComponent < Plutonium::UI::Component::Base
|
|
|
94
94
|
span(class: "text-sm text-[var(--pu-text-subtle)]") {
|
|
95
95
|
@post.published_at&.strftime("%B %d, %Y")
|
|
96
96
|
}
|
|
97
|
-
a(href: resource_url_for(@post), class: "text-primary-600") { "
|
|
97
|
+
a(href: resource_url_for(@post), class: "text-primary-600") { t("blog.posts.card.read_more") }
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
end
|
|
@@ -104,18 +104,18 @@ end
|
|
|
104
104
|
::: tip Inherit `Plutonium::UI::Component::Base`
|
|
105
105
|
It gives you:
|
|
106
106
|
- The component kit (`PageHeader`, `Panel`, `Block`, …)
|
|
107
|
-
- Resource helpers (`resource_url_for`, `current_user`, `
|
|
107
|
+
- Resource helpers (`resource_url_for`, `current_user`, `resource_record!`, `current_definition`)
|
|
108
108
|
- A `helpers` proxy for Rails helpers (`helpers.link_to`, `helpers.number_to_currency`)
|
|
109
109
|
- Token / class helpers (`tokens`, `classes`)
|
|
110
110
|
|
|
111
|
-
A **field** component (one you pass to `as:`) inherits its Phlexi base instead
|
|
111
|
+
A **field** component (one you pass to `as:`) inherits its Phlexi base instead:
|
|
112
112
|
`include Plutonium::UI::Component::Behaviour` there to get the same helpers.
|
|
113
113
|
:::
|
|
114
114
|
|
|
115
115
|
### Use in a definition
|
|
116
116
|
|
|
117
117
|
A component like `PostCardComponent` above has its own constructor, so it reaches
|
|
118
|
-
a field through the **block form
|
|
118
|
+
a field through the **block form**, you build it yourself:
|
|
119
119
|
|
|
120
120
|
```ruby
|
|
121
121
|
class PostDefinition < ResourceDefinition
|
|
@@ -129,13 +129,13 @@ class PostDefinition < ResourceDefinition
|
|
|
129
129
|
end
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
`as: SomeComponent` is the other route, and it expects a **field component
|
|
132
|
+
`as: SomeComponent` is the other route, and it expects a **field component**.
|
|
133
133
|
Plutonium instantiates it with the field builder, not with your keyword
|
|
134
134
|
arguments. See [field components](#field-components) below.
|
|
135
135
|
|
|
136
136
|
::: warning `as:` does not take a keyword-argument component
|
|
137
137
|
`display :card, as: PostCardComponent` raises `ArgumentError: wrong number of
|
|
138
|
-
arguments
|
|
138
|
+
arguments`; the component is constructed as `PostCardComponent.new(field,
|
|
139
139
|
**attributes)`. Use the block form for those.
|
|
140
140
|
:::
|
|
141
141
|
|
|
@@ -143,7 +143,7 @@ arguments` — the component is constructed as `PostCardComponent.new(field,
|
|
|
143
143
|
|
|
144
144
|
A field component subclasses the Phlexi component base for its surface and reads
|
|
145
145
|
everything off `field` (`field.value`, `field.object`, `field.dom`, plus
|
|
146
|
-
`attributes
|
|
146
|
+
`attributes`, the themed id/name/class Plutonium already computed):
|
|
147
147
|
|
|
148
148
|
```ruby
|
|
149
149
|
# app/components/color_picker_component.rb
|
|
@@ -172,7 +172,7 @@ class PostDefinition < ResourceDefinition
|
|
|
172
172
|
end
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
An `as:` component works in every surface that renders the field
|
|
175
|
+
An `as:` component works in every surface that renders the field: form, show
|
|
176
176
|
page, index column, filter panel and wizard summary alike.
|
|
177
177
|
|
|
178
178
|
### Use in a page / form / display
|
|
@@ -187,7 +187,7 @@ end
|
|
|
187
187
|
|
|
188
188
|
## `DynaFrameContent` pattern
|
|
189
189
|
|
|
190
|
-
Enables frame-aware rendering
|
|
190
|
+
Enables frame-aware rendering, regular requests get the full page (header + content + footer); turbo-frame requests get only the content inside the frame.
|
|
191
191
|
|
|
192
192
|
```ruby
|
|
193
193
|
def view_template(&block)
|
|
@@ -205,7 +205,7 @@ All pages inherit this automatically. Modals and frame navigation work without s
|
|
|
205
205
|
|
|
206
206
|
Rarely. Use it when writing a custom non-resource page that needs the same frame-aware rendering as the built-in pages.
|
|
207
207
|
|
|
208
|
-
For typical custom pages, just inherit `Plutonium::UI::Page::Base` and override hooks like `render_content
|
|
208
|
+
For typical custom pages, just inherit `Plutonium::UI::Page::Base` and override hooks like `render_content`, the DynaFrame wrapping happens in `view_template` automatically.
|
|
209
209
|
|
|
210
210
|
## Conditional class helpers
|
|
211
211
|
|
|
@@ -257,15 +257,15 @@ class MyComponent < Plutonium::UI::Component::Base
|
|
|
257
257
|
end
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
The `helpers` proxy gives you everything `ApplicationController#helpers` exposes
|
|
260
|
+
The `helpers` proxy gives you everything `ApplicationController#helpers` exposes, including any custom helpers in `app/helpers/`.
|
|
261
261
|
|
|
262
262
|
## Available context
|
|
263
263
|
|
|
264
|
-
Inside any custom component, the same set of helpers as pages/forms/displays
|
|
264
|
+
Inside any custom component, the same set of helpers as pages/forms/displays, see [Pages › Available context](./pages#available-context).
|
|
265
265
|
|
|
266
266
|
## Related
|
|
267
267
|
|
|
268
|
-
- [Pages](./pages)
|
|
269
|
-
- [Forms](./forms)
|
|
270
|
-
- [Displays](./displays)
|
|
271
|
-
- [Assets](./assets)
|
|
268
|
+
- [Pages](./pages): `render_*` hooks call your components
|
|
269
|
+
- [Forms](./forms): the built-in input tags and their `as:` aliases
|
|
270
|
+
- [Displays](./displays): using custom display components
|
|
271
|
+
- [Assets](./assets): design tokens (`var(--pu-*)`) and `.pu-*` component classes
|
|
@@ -14,7 +14,7 @@ class PostDefinition < ResourceDefinition
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# `fields_wrapper` is ALREADY a card (it renders a Block internally),
|
|
17
|
-
# so do not wrap it in another one
|
|
17
|
+
# so do not wrap it in another one, that stacks two cards and doubles
|
|
18
18
|
# the border and shadow.
|
|
19
19
|
fields_wrapper do
|
|
20
20
|
render_resource_field :author
|
|
@@ -38,7 +38,7 @@ end
|
|
|
38
38
|
|---|---|
|
|
39
39
|
| `render_fields` | All permitted fields |
|
|
40
40
|
| `render_resource_field(name)` | One field |
|
|
41
|
-
| `render_associations` | Association tabs (driven by `permitted_associations
|
|
41
|
+
| `render_associations` | Association tabs (driven by `permitted_associations`, see [Behavior › Policy](/reference/behavior/policies#association-permissions)) |
|
|
42
42
|
| `object` | The record |
|
|
43
43
|
| `resource_fields`, `resource_associations` | Permitted lists |
|
|
44
44
|
|
|
@@ -48,7 +48,7 @@ For per-field custom rendering, prefer declaring it in the **definition** rather
|
|
|
48
48
|
|
|
49
49
|
```ruby
|
|
50
50
|
class PostDefinition < ResourceDefinition
|
|
51
|
-
# Block
|
|
51
|
+
# Block, returns any Phlex component
|
|
52
52
|
display :status do |field|
|
|
53
53
|
StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
|
|
54
54
|
end
|
|
@@ -59,7 +59,7 @@ class PostDefinition < ResourceDefinition
|
|
|
59
59
|
span(class: "pu-badge pu-badge-#{variant}") { field.value.to_s.humanize }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# Field component class
|
|
62
|
+
# Field component class, built as ChartComponent.new(field, **attributes),
|
|
63
63
|
# so it subclasses Phlexi::Display::Components::Base and reads `field`.
|
|
64
64
|
# (A component with its own constructor uses the block form above.)
|
|
65
65
|
display :chart, as: ChartComponent
|
|
@@ -70,14 +70,14 @@ See [Resource › Definition › Custom rendering](/reference/resource/definitio
|
|
|
70
70
|
|
|
71
71
|
## Built-in display components
|
|
72
72
|
|
|
73
|
-
Some types render with richer components automatically
|
|
73
|
+
Some types render with richer components automatically; you only declare an `as:` to override or pass options.
|
|
74
74
|
|
|
75
75
|
| `as:` | Renders | Auto-inferred for | Options |
|
|
76
76
|
|-------|---------|-------------------|---------|
|
|
77
77
|
| `:boolean` | green "Yes" / neutral "No" pill | `boolean` columns | `true_label:`, `false_label:` |
|
|
78
78
|
| `:badge` | colored status pill | `enum` columns | `colors:` (per-value override) |
|
|
79
79
|
| `:currency` | delimited, 2-decimal money | `has_cents` decimal accessors | `unit:`, `options:` |
|
|
80
|
-
| `:color` | swatch + value |
|
|
80
|
+
| `:color` | swatch + value | - | - |
|
|
81
81
|
|
|
82
82
|
```ruby
|
|
83
83
|
class OrderDefinition < ResourceDefinition
|
|
@@ -90,7 +90,7 @@ end
|
|
|
90
90
|
|
|
91
91
|
**Badge colors.** Known statuses (`active`, `pending`, `failed`, …) are auto-colored by meaning. Unknown values get a stable decorative color (same value → same color). Override per-value with `colors:`; valid variants: `:neutral`, `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:accent`.
|
|
92
92
|
|
|
93
|
-
**Currency.** No symbol is shown unless you pass `unit
|
|
93
|
+
**Currency.** No symbol is shown unless you pass `unit:`, a literal string (`"£"`) or a Symbol read off the record for per-row currencies. `has_cents` decimal accessors infer `:currency` automatically (still symbol-less until you set `unit:`).
|
|
94
94
|
|
|
95
95
|
## Theming
|
|
96
96
|
|
|
@@ -118,20 +118,20 @@ end
|
|
|
118
118
|
`fields_wrapper`, `fields_inner`, `sections_wrapper`, `section_grid`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`, `boolean`, `badge`, `currency`, `color`, plus the shared section-chrome keys (`section_wrapper`, `section_header`, `section_summary`, `section_accent`, `section_heading`, `section_description`, `section_caret`, `section_body`).
|
|
119
119
|
|
|
120
120
|
::: warning `fields_wrapper` is the card, `fields_inner` is the grid
|
|
121
|
-
`fields_wrapper` is merged into a `Plutonium::UI::Block`, which supplies `pu-card` itself
|
|
121
|
+
`fields_wrapper` is merged into a `Plutonium::UI::Block`, which supplies `pu-card` itself, grid classes put there style the card, not the fields. Override **`fields_inner`** for the unsectioned grid, and **`section_grid`** for the grid inside a [`display_layout`](/reference/resource/definition#display-layout) section.
|
|
122
122
|
:::
|
|
123
123
|
|
|
124
124
|
Section chrome comes from `Plutonium::UI::Component::Section::DEFAULT_THEME`, merged into **both** `Form::Theme` and `Display::Theme`, so form sections and show-page sections read identically by default while staying independently overridable.
|
|
125
125
|
|
|
126
|
-
(`boolean` and `badge` apply their pill variant in the component, so their theme value stays empty
|
|
126
|
+
(`boolean` and `badge` apply their pill variant in the component, so their theme value stays empty; restyle the pills via the `.pu-badge*` classes instead.)
|
|
127
127
|
|
|
128
128
|
## Metadata panel
|
|
129
129
|
|
|
130
|
-
A right-side aside on the show page. Configured at the definition level, not the Display class
|
|
130
|
+
A right-side aside on the show page. Configured at the definition level, not the Display class, see [Resource › Definition › Metadata panel](/reference/resource/definition#metadata-panel-show-page).
|
|
131
131
|
|
|
132
132
|
## Related
|
|
133
133
|
|
|
134
|
-
- [Pages](./pages)
|
|
135
|
-
- [Components](./components)
|
|
136
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
137
|
-
- [Behavior › Policy](/reference/behavior/policies)
|
|
134
|
+
- [Pages](./pages): `ShowPage` render hooks (often a lighter alternative to overriding `Display`)
|
|
135
|
+
- [Components](./components): building reusable Phlex display components
|
|
136
|
+
- [Resource › Definition](/reference/resource/definition): field-level display configuration (`as:`, `condition:`, blocks)
|
|
137
|
+
- [Behavior › Policy](/reference/behavior/policies): `permitted_associations` drives the show-page tablist
|