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,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plutonium-ui
|
|
3
|
-
description: Use BEFORE building or customizing any Plutonium UI
|
|
3
|
+
description: 'Use BEFORE building or customizing any Plutonium UI: page classes, forms, displays, tables, custom Phlex components, layouts, Stimulus controllers, Tailwind config, design tokens, themes, or component classes. Covers the full view + asset toolchain.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Plutonium UI
|
|
6
|
+
# Plutonium UI: Pages, Forms, Components, Assets
|
|
7
7
|
|
|
8
8
|
Plutonium uses Phlex for all view components and TailwindCSS 4 + Stimulus for the frontend. This skill covers everything from overriding a single page to writing custom Phlex components, configuring Tailwind, and theming via design tokens.
|
|
9
9
|
|
|
@@ -13,19 +13,21 @@ For field-level rendering (`field :foo, as: :markdown`, `display :status do |f|
|
|
|
13
13
|
|
|
14
14
|
- **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end`. Don't replace the entire view layer.
|
|
15
15
|
- **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.
|
|
16
|
-
- **All pages inherit `DynaFrameContent
|
|
17
|
-
- **Custom components inherit `Plutonium::UI::Component::Base
|
|
18
|
-
- **`render_actions` is mandatory in custom `form_template
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
16
|
+
- **All pages inherit `DynaFrameContent`**: turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work".
|
|
17
|
+
- **Custom components inherit `Plutonium::UI::Component::Base`**: it gives you the component kit (`PageHeader`, `Panel`, `Block`), resource helpers, and the `helpers` proxy for Rails helpers.
|
|
18
|
+
- **`render_actions` is mandatory in custom `form_template`**: without it, the form has no submit button.
|
|
19
|
+
- **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.
|
|
20
|
+
- **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.
|
|
21
|
+
- **Use `plutoniumTailwindConfig.merge`** when extending Tailwind theme; plain object merge drops Plutonium's defaults.
|
|
22
|
+
- **Style with `.pu-*` classes first, `var(--pu-*)` tokens second, raw palette pairs never.** 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 (see Part 8).
|
|
23
|
+
- **User-facing copy goes through `t(...)` with a locale key**, in components, pages, displays and definitions alike. See Part 4 › Translating text.
|
|
22
24
|
- **Configure inputs in the definition; render them with `render_resource_field` in the form.** Don't reimplement field widgets from scratch.
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
26
|
-
## 🛑 Before you customize UI: pick the lightest seam (ASK
|
|
28
|
+
## 🛑 Before you customize UI: pick the lightest seam (ASK: don't infer)
|
|
27
29
|
|
|
28
|
-
Plutonium gives you escalating levels of customization. Reach for the **lightest that fits
|
|
30
|
+
Plutonium gives you escalating levels of customization. Reach for the **lightest that fits**: jumping to `view_template` or an eject loses breadcrumbs/header/DynaFrame behavior and saddles you with maintaining copied markup forever.
|
|
29
31
|
|
|
30
32
|
| You want to… | Reach for | **NOT** |
|
|
31
33
|
|---|---|---|
|
|
@@ -33,35 +35,35 @@ Plutonium gives you escalating levels of customization. Reach for the **lightest
|
|
|
33
35
|
| Re-arrange the record's fields | a custom `Display` (`display_template`) | a hand-rolled `Form`/`view_template` |
|
|
34
36
|
| Group form fields into sections | the `form_layout` DSL in the definition | a `Form` subclass |
|
|
35
37
|
| Recolor / rebrand | `plutoniumTailwindConfig.merge(...)` in `tailwind.config.js` | a plain object spread (drops Plutonium's defaults) |
|
|
36
|
-
| Replace whole chrome per-portal | `pu:eject:shell` / `pu:eject:layout
|
|
38
|
+
| Replace whole chrome per-portal | `pu:eject:shell` / `pu:eject:layout`, **last resort; you own it after** | ejecting when a hook/class/theme would do |
|
|
37
39
|
|
|
38
|
-
Then: is the change **global** (base `PostDefinition`) or **per-portal** (`AdminPortal::PostDefinition`)? And does it touch CSS/JS (⇒ the asset toolchain must be set up)? Don't guess field names or the banner copy
|
|
40
|
+
Then: is the change **global** (base `PostDefinition`) or **per-portal** (`AdminPortal::PostDefinition`)? And does it touch CSS/JS (⇒ the asset toolchain must be set up)? Don't guess field names or the banner copy; read the definition.
|
|
39
41
|
|
|
40
|
-
## ✅ Before you edit: verify the ground truth (CHECK
|
|
42
|
+
## ✅ Before you edit: verify the ground truth (CHECK: read it, don't ask for it)
|
|
41
43
|
|
|
42
|
-
You have file access
|
|
44
|
+
You have file access, **inspect**; don't ask the user to describe their app.
|
|
43
45
|
|
|
44
46
|
| Check | How | Why it matters |
|
|
45
47
|
|---|---|---|
|
|
46
48
|
| Custom page/Display already exists | Read the definition for nested `ShowPage`/`Display`/`Form` | Re-declaring clobbers an existing override |
|
|
47
49
|
| Global vs per-portal | Is it `::PostDefinition` or `AdminPortal::PostDefinition`? | Override the right one |
|
|
48
50
|
| Real field names | Read the model/definition | Don't invent fields in `display_template` |
|
|
49
|
-
| Asset toolchain wired | `ls tailwind.config.js`; the
|
|
50
|
-
| Stimulus registered | grep `app/javascript/controllers/index.js` for `registerControllers` |
|
|
51
|
+
| Asset toolchain wired | `ls tailwind.config.js`; `config.assets.stylesheet` in the Plutonium initializer; has `pu:core:assets` run? | Still on the gem's prebuilt `plutonium.css` ⇒ brand/CSS edits won't compile |
|
|
52
|
+
| Stimulus registered | grep `app/javascript/controllers/index.js` for `registerControllers` | Once the app serves its own JS bundle, the interactive layer is dead without it |
|
|
51
53
|
| Build watcher | Is `yarn dev` running? (`PLUTONIUM_DEV=1` when working on the gem) | CSS/JS changes need the rebuild |
|
|
52
54
|
|
|
53
55
|
Inspect with your own tools **before** proposing code.
|
|
54
56
|
|
|
55
|
-
## 🛠 Use the generator
|
|
57
|
+
## 🛠 Use the generator, and prefer hooks over ejecting
|
|
56
58
|
|
|
57
59
|
| Task | How | Verify first |
|
|
58
60
|
|---|---|---|
|
|
59
|
-
| Custom Tailwind + Stimulus toolchain | `pu:core:assets` | Not already run |
|
|
61
|
+
| Custom Tailwind + Stimulus toolchain | `pu:core:assets` (see Part 7 for its prerequisites) | Not already run |
|
|
60
62
|
| Eject chrome (header/sidebar/layout) | `pu:eject:shell` / `pu:eject:layout --dest=portal` | A render hook / nested class / theme genuinely can't do it (last resort) |
|
|
61
63
|
|
|
62
64
|
---
|
|
63
65
|
|
|
64
|
-
# Part 1
|
|
66
|
+
# Part 1: Pages
|
|
65
67
|
|
|
66
68
|
Each definition has nested page classes. Override the ones you need to customize:
|
|
67
69
|
|
|
@@ -93,16 +95,17 @@ Definition
|
|
|
93
95
|
|
|
94
96
|
```ruby
|
|
95
97
|
class PostDefinition < ResourceDefinition
|
|
96
|
-
index_page_title "
|
|
97
|
-
index_page_description "
|
|
98
|
-
show_page_title "Article Details"
|
|
99
|
-
show_page_title -> { "#{current_record!.title} — Details" } # dynamic
|
|
98
|
+
index_page_title t("blog.posts.index.title") # lazy: resolved per request, in its locale
|
|
99
|
+
index_page_description t("blog.posts.index.description")
|
|
100
|
+
show_page_title "Article Details" # a literal also works (fixed, untranslated)
|
|
100
101
|
|
|
101
102
|
breadcrumbs true # global default
|
|
102
103
|
index_page_breadcrumbs false # per-page override
|
|
103
104
|
end
|
|
104
105
|
```
|
|
105
106
|
|
|
107
|
+
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.
|
|
108
|
+
|
|
106
109
|
## Page hooks (preferred over `view_template`)
|
|
107
110
|
|
|
108
111
|
Every page inherits these:
|
|
@@ -123,12 +126,12 @@ class ShowPage < ShowPage
|
|
|
123
126
|
private
|
|
124
127
|
|
|
125
128
|
def page_title
|
|
126
|
-
"#{object.title}
|
|
129
|
+
"#{object.title} - #{object.author.name}"
|
|
127
130
|
end
|
|
128
131
|
|
|
129
132
|
def render_before_content
|
|
130
|
-
div(class: "alert alert-info") do
|
|
131
|
-
|
|
133
|
+
div(class: "pu-alert pu-alert-info", role: "status") do
|
|
134
|
+
div(class: "pu-alert-message") { t("blog.posts.show.comment_count", count: object.comments.count) }
|
|
132
135
|
end
|
|
133
136
|
end
|
|
134
137
|
|
|
@@ -179,7 +182,7 @@ Use to pin action strips, omit nav chrome, or swap layouts.
|
|
|
179
182
|
|
|
180
183
|
---
|
|
181
184
|
|
|
182
|
-
# Part 2
|
|
185
|
+
# Part 2: Forms
|
|
183
186
|
|
|
184
187
|
Forms are built on [Phlexi::Form](https://github.com/radioactive-labs/phlexi-form). Hierarchy:
|
|
185
188
|
|
|
@@ -198,7 +201,7 @@ class PostDefinition < ResourceDefinition
|
|
|
198
201
|
class Form < Form
|
|
199
202
|
def form_template
|
|
200
203
|
render_fields # render every permitted field
|
|
201
|
-
render_actions # submit buttons
|
|
204
|
+
render_actions # submit buttons, REQUIRED
|
|
202
205
|
end
|
|
203
206
|
end
|
|
204
207
|
end
|
|
@@ -220,11 +223,11 @@ end
|
|
|
220
223
|
|
|
221
224
|
## Custom layouts
|
|
222
225
|
|
|
223
|
-
### Sectioned
|
|
226
|
+
### Sectioned: prefer the `form_layout` / `display_layout` DSL
|
|
224
227
|
|
|
225
|
-
**For grouping fields into sections, don't hand-roll a `Form` or `Display` subclass
|
|
228
|
+
**For grouping fields into sections, don't hand-roll a `Form` or `Display` subclass; declare `form_layout` (forms) or `display_layout` (show page) in the definition.** They handle headings, descriptions, collapsible `<details>`, `condition:`-based visibility, and **auto-drop sections that resolve to zero fields** (so `+ New` doesn't sprout empty headings). `columns:` is form-only, `display_layout` raises on it. See [[plutonium-resource]] › Form Layout / Display Layout.
|
|
226
229
|
|
|
227
|
-
**Each section renders as its own card** (`Plutonium::UI::Block`), so a sectioned form or show page has **no single outer card
|
|
230
|
+
**Each section renders as its own card** (`Plutonium::UI::Block`), so a sectioned form or show page has **no single outer card**: the form drops its own `pu-card` and the sections supply it. Don't add a card wrapper of your own around them.
|
|
228
231
|
|
|
229
232
|
```ruby
|
|
230
233
|
class PostDefinition < ResourceDefinition
|
|
@@ -265,7 +268,7 @@ class Form < Form
|
|
|
265
268
|
end
|
|
266
269
|
```
|
|
267
270
|
|
|
268
|
-
A hand-rolled `section` like this renders its heading unconditionally
|
|
271
|
+
A hand-rolled `section` like this renders its heading unconditionally, which is exactly the empty-heading problem `form_layout` avoids. If you must hand-roll, guard empty sections yourself.
|
|
269
272
|
|
|
270
273
|
### Two-column
|
|
271
274
|
|
|
@@ -308,7 +311,7 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
308
311
|
| `input_tag` | text (auto-detected type) |
|
|
309
312
|
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
|
|
310
313
|
| `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
|
|
311
|
-
| `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`)
|
|
314
|
+
| `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`), default for boolean columns; `as: :boolean` for a plain checkbox |
|
|
312
315
|
|
|
313
316
|
### Plutonium-enhanced tags
|
|
314
317
|
|
|
@@ -330,13 +333,13 @@ render field(:avatar).wrapped { |f| f.uppy_tag(allowed_file_types: %w[.jpg
|
|
|
330
333
|
|
|
331
334
|
### Password & secret fields
|
|
332
335
|
|
|
333
|
-
`password_tag` masks the stored value
|
|
336
|
+
`password_tag` masks the stored value and **never emits the secret into the DOM**. A stored secret renders a sentinel; an untouched submit keeps it, an edit-to-new-value then failed re-render comes back blank + `required` (re-type; secrets are never echoed back), a *cleared* field comes back blank but **not** `required` (the clear may be intentional), a deliberately emptied field clears it (clear-by-blank), a typed value sets it. The sentinel is guarded by the `password-sentinel` Stimulus controller: the first edit (incl. **backspace**) wipes the whole field so a partial edit can't corrupt it.
|
|
334
337
|
|
|
335
|
-
Auto-detected by name: `password`/`token`/`salt`, `encrypted_*`, `*_password`/`*_digest`/`*_hash`/`*_token`/`*_key`/`*_salt`, or any name containing `secret`. A convenience, **not** a guarantee
|
|
338
|
+
Auto-detected by name: `password`/`token`/`salt`, `encrypted_*`, `*_password`/`*_digest`/`*_hash`/`*_token`/`*_key`/`*_salt`, or any name containing `secret`. A convenience, **not** a guarantee: odd-named secrets (`recovery_phrase`, `pin`) still leak unless masked explicitly.
|
|
336
339
|
|
|
337
340
|
```ruby
|
|
338
|
-
field :api_token, as: :string # opt OUT
|
|
339
|
-
field :recovery_phrase, as: :password # opt IN
|
|
341
|
+
field :api_token, as: :string # opt OUT: show a readable value (token to copy, checksum)
|
|
342
|
+
field :recovery_phrase, as: :password # opt IN: mask a secret the heuristic misses
|
|
340
343
|
```
|
|
341
344
|
|
|
342
345
|
## Submit buttons
|
|
@@ -347,7 +350,7 @@ Control the secondary button via the definition:
|
|
|
347
350
|
|
|
348
351
|
```ruby
|
|
349
352
|
class PostDefinition < ResourceDefinition
|
|
350
|
-
submit_and_continue false # nil (default
|
|
353
|
+
submit_and_continue false # nil (default, auto), true (always show), false (always hide)
|
|
351
354
|
end
|
|
352
355
|
```
|
|
353
356
|
|
|
@@ -369,14 +372,14 @@ end
|
|
|
369
372
|
|
|
370
373
|
These all live in the definition layer:
|
|
371
374
|
|
|
372
|
-
- **Pre-submit / dynamic forms
|
|
373
|
-
- **Nested inputs** (`nested_input :variants`)
|
|
374
|
-
- **Structured inputs** (`structured_input :payload`, `structured_input :rows, repeat: 5`)
|
|
375
|
-
- **Interaction forms
|
|
375
|
+
- **Pre-submit / dynamic forms**: see [[plutonium-resource]] › Dynamic Forms.
|
|
376
|
+
- **Nested inputs** (`nested_input :variants`): association-backed inline forms; see [[plutonium-resource]] › Nested Inputs.
|
|
377
|
+
- **Structured inputs** (`structured_input :payload`, `structured_input :rows, repeat: 5`): classless hash / array-of-hashes into a JSON column (resources) or an attribute (interactions); reuses the repeater chrome. See [[plutonium-resource]] › Structured Inputs.
|
|
378
|
+
- **Interaction forms**: interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [[plutonium-behavior]] › Interactions.
|
|
376
379
|
|
|
377
380
|
---
|
|
378
381
|
|
|
379
|
-
# Part 3
|
|
382
|
+
# Part 3: Display & Table
|
|
380
383
|
|
|
381
384
|
## Custom Display
|
|
382
385
|
|
|
@@ -390,7 +393,7 @@ class PostDefinition < ResourceDefinition
|
|
|
390
393
|
end
|
|
391
394
|
|
|
392
395
|
# `fields_wrapper` is ALREADY a card (it renders a Block internally),
|
|
393
|
-
# so do not wrap it in another one
|
|
396
|
+
# so do not wrap it in another one: that stacks two cards and doubles
|
|
394
397
|
# the border and shadow.
|
|
395
398
|
fields_wrapper do
|
|
396
399
|
render_resource_field :author
|
|
@@ -412,14 +415,14 @@ end
|
|
|
412
415
|
|---|---|
|
|
413
416
|
| `render_fields` | All permitted fields |
|
|
414
417
|
| `render_resource_field(name)` | One field |
|
|
415
|
-
| `render_associations` | Association tabs (driven by `permitted_associations
|
|
416
|
-
| `render_before_fields` / `render_after_fields` | Hooks around the fields
|
|
418
|
+
| `render_associations` | Association tabs (driven by `permitted_associations`, see [[plutonium-behavior]]) |
|
|
419
|
+
| `render_before_fields` / `render_after_fields` | Hooks around the fields, **Details tab only** |
|
|
417
420
|
| `object` | The record |
|
|
418
421
|
| `resource_fields`, `resource_associations` | Permitted lists |
|
|
419
422
|
|
|
420
423
|
### Details-tab-only content
|
|
421
424
|
|
|
422
|
-
To add a banner or extra section that shows on the **Details** tab and not the association tabs, override `render_before_fields` / `render_after_fields` on the **Display
|
|
425
|
+
To add a banner or extra section that shows on the **Details** tab and not the association tabs, override `render_before_fields` / `render_after_fields` on the **Display**, not the ShowPage. The page-level `render_before_content` / `render_after_content` hooks wrap the whole content block, and the tablist lives inside it, so anything added there shows on every tab.
|
|
423
426
|
|
|
424
427
|
```ruby
|
|
425
428
|
class PostDefinition < ResourceDefinition
|
|
@@ -427,13 +430,28 @@ class PostDefinition < ResourceDefinition
|
|
|
427
430
|
private
|
|
428
431
|
|
|
429
432
|
def render_before_fields
|
|
430
|
-
|
|
433
|
+
flagged = object.comments.where(flagged: true).count
|
|
434
|
+
return if flagged.zero?
|
|
435
|
+
|
|
436
|
+
div(class: "pu-alert pu-alert-warning", role: "alert") do
|
|
437
|
+
div(class: "pu-alert-message") { t("blog.posts.flagged_comments", count: flagged) }
|
|
438
|
+
end
|
|
431
439
|
end
|
|
432
440
|
end
|
|
433
441
|
end
|
|
434
442
|
```
|
|
435
443
|
|
|
436
|
-
|
|
444
|
+
```yaml
|
|
445
|
+
# config/locales/en.yml
|
|
446
|
+
en:
|
|
447
|
+
blog:
|
|
448
|
+
posts:
|
|
449
|
+
flagged_comments:
|
|
450
|
+
one: "1 comment is waiting for moderation."
|
|
451
|
+
other: "%{count} comments are waiting for moderation."
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`pu-alert pu-alert-<success|warning|danger|info>` is the same inline banner the flash messages use (`app/views/plutonium/_flash_alerts.html.erb`), so it already has its dark-mode colors. Both hooks are no-ops by default. `render_fields` is the Details tab body when the record has associations and the entire display when it doesn't, so the hooks fire in the Details context either way.
|
|
437
455
|
|
|
438
456
|
## Custom Table
|
|
439
457
|
|
|
@@ -472,7 +490,7 @@ end
|
|
|
472
490
|
|
|
473
491
|
## Drag-to-Reorder Affordance (`position_on`)
|
|
474
492
|
|
|
475
|
-
When a definition declares `position_on` (see [[plutonium-resource]]) the index **table**, the **card grid**, and **nested association tables** render a drag grip. Configuration is entirely in the definition
|
|
493
|
+
When a definition declares `position_on` (see [[plutonium-resource]]) the index **table**, the **card grid**, and **nested association tables** render a drag grip. Configuration is entirely in the definition; there is no UI-layer switch.
|
|
476
494
|
|
|
477
495
|
**What renders where:**
|
|
478
496
|
|
|
@@ -480,11 +498,11 @@ When a definition declares `position_on` (see [[plutonium-resource]]) the index
|
|
|
480
498
|
|---|---|---|---|
|
|
481
499
|
| Index / nested table | the **grip only**, never the `<tr>` | inside the first cell's existing left padding (content does not shift) | vertical |
|
|
482
500
|
| Card grid | the **grip only** | floated over the card's top-left corner | horizontal, wrap-aware |
|
|
483
|
-
| Kanban board | the **whole card** |
|
|
501
|
+
| Kanban board | the **whole card** | - | both (cross-column) |
|
|
484
502
|
|
|
485
|
-
🚨 **Never make a `<tr>` draggable.** Two silent regressions: `draggable="true"` disables text selection inside the element in every major browser (you lose copy-a-cell-value), and it fights `row_click_controller
|
|
503
|
+
🚨 **Never make a `<tr>` draggable.** Two silent regressions: `draggable="true"` disables text selection inside the element in every major browser (you lose copy-a-cell-value), and it fights `row_click_controller`: a drag that starts and ends in place still fires a click and navigates the user away. Kanban keeps whole-card dragging because neither applies to a kanban card; a **grid** card gets a grip because it *does* carry a row-click show affordance.
|
|
486
504
|
|
|
487
|
-
**Enabled state.** The grip is live only while the collection is sorted **ascending, by the position attribute, and nothing else**. Otherwise "drop me between these two rows" describes nothing. Under a foreign sort the Stimulus controller isn't attached at all and the grip renders as a **link that applies the position sort
|
|
505
|
+
**Enabled state.** The grip is live only while the collection is sorted **ascending, by the position attribute, and nothing else**. Otherwise "drop me between these two rows" describes nothing. Under a foreign sort the Stimulus controller isn't attached at all and the grip renders as a **link that applies the position sort**: the disabled state is the way out of the disabled state, which is why `position_on` registers `sort <attr>`. Per record, the grip also requires `reposition?`.
|
|
488
506
|
|
|
489
507
|
**DOM contract** (relevant if you eject a table/grid or write a custom collection component):
|
|
490
508
|
|
|
@@ -493,18 +511,18 @@ wrapper data-controller="positioned"
|
|
|
493
511
|
data-positioned-url-template-value="/things/__ID__/reposition"
|
|
494
512
|
data-positioned-axis-value="horizontal" # grid only
|
|
495
513
|
row/card data-positioned-row-id="<id>" # single source of truth for the record id
|
|
496
|
-
grip data-positioned-grip # a real <button
|
|
514
|
+
grip data-positioned-grip # a real <button>, tabbable
|
|
497
515
|
```
|
|
498
516
|
|
|
499
|
-
The URL template is built off `current_page_path` (not `request.path`) so a post-rebalance re-render doesn't wire subsequent drops to `/things/5/reposition`. The controller POSTs `{prev_id, next_id, to_index}` plus `window.location.search
|
|
517
|
+
The URL template is built off `current_page_path` (not `request.path`) so a post-rebalance re-render doesn't wire subsequent drops to `/things/5/reposition`. The controller POSTs `{prev_id, next_id, to_index}` plus `window.location.search`; the query string is load-bearing, since the endpoint re-renders through the ordinary index pipeline.
|
|
500
518
|
|
|
501
|
-
**Accessibility.** Focus the grip and use <kbd>↑</kbd>/<kbd>↓</kbd>
|
|
519
|
+
**Accessibility.** Focus the grip and use <kbd>↑</kbd>/<kbd>↓</kbd> (deliberately linear even on a wrapped grid, since one position attribute stores a 1-D order). Focus is restored onto the same record's grip after a stream replaces the collection. ⚠️ Native HTML5 drag does **not** fire on touch devices (inherited from kanban); there is no automatic fallback.
|
|
502
520
|
|
|
503
521
|
Components: `lib/plutonium/ui/table/components/drag_handle.rb`, `lib/plutonium/ui/component/positionable.rb`, `src/js/controllers/positioned_controller.js`. Reference: `docs/reference/resource/positioning.md`.
|
|
504
522
|
|
|
505
523
|
---
|
|
506
524
|
|
|
507
|
-
# Part 4
|
|
525
|
+
# Part 4: Component Kit & Custom Components
|
|
508
526
|
|
|
509
527
|
## Built-in shorthand kit
|
|
510
528
|
|
|
@@ -528,7 +546,7 @@ Breadcrumbs()
|
|
|
528
546
|
|
|
529
547
|
## Avatar
|
|
530
548
|
|
|
531
|
-
`Avatar(subject = nil, src: nil, size: :md, alt: nil, **attrs)
|
|
549
|
+
`Avatar(subject = nil, src: nil, size: :md, alt: nil, **attrs)`: profile image with a deterministic [Navii](https://navii.dev) fallback. Registered in the kit.
|
|
532
550
|
|
|
533
551
|
```ruby
|
|
534
552
|
Avatar(user) # Navii fallback seeded from the record
|
|
@@ -540,17 +558,24 @@ Avatar(src: avatar_url) # bare image, no subject/fallback
|
|
|
540
558
|
```
|
|
541
559
|
|
|
542
560
|
- **subject** (positional): record → PII-free hashed seed + default `alt` (display name); String → seed. A URL-shaped String (`http(s)://…` or `/…`) is routed to `src` (shown as the image), not used as a seed.
|
|
543
|
-
- **src**: a Symbol is sent to the subject (`:avatar` → `subject.avatar`, a **contract
|
|
561
|
+
- **src**: a Symbol is sent to the subject (`:avatar` → `subject.avatar`, a **contract**, raises if absent); otherwise an ActiveStorage attachment, active_shrine/Shrine uploader, or URL string. ActiveStorage resolves via `helpers.url_for`; everything else via its own `#url`.
|
|
544
562
|
- **size**: `:xs 24 / :sm 32 / :md 40 / :lg 48 / :xl 64`, or a raw Integer.
|
|
545
|
-
- **Privacy**: the value sent to Navii is **always** a SHA256 hash
|
|
563
|
+
- **Privacy**: the value sent to Navii is **always** a SHA256 hash, so no ids, emails, or seed strings leave the app. Deterministic per subject.
|
|
546
564
|
- **Resolution order**: resolved `src` → Navii (from subject) → generic user icon.
|
|
547
565
|
- **Config**: `config.navii_host_url` (default `https://api.navii.dev`); the component appends `/avatar/:seed`.
|
|
548
566
|
|
|
549
|
-
🚨 Ejected shells: `Avatar` only shows a Navii avatar when `NavUser` is passed `record:`. The gem's `_resource_header.html.erb` passes `record: (current_user if current_user.respond_to?(:id))`; portals that **ejected** the header before this must re-eject (`rails g pu:eject:shell --dest=<portal>`) or add the `record:` line, otherwise they keep the icon fallback. Pass a record only
|
|
567
|
+
🚨 Ejected shells: `Avatar` only shows a Navii avatar when `NavUser` is passed `record:`. The gem's `_resource_header.html.erb` passes `record: (current_user if current_user.respond_to?(:id))`; portals that **ejected** the header before this must re-eject (`rails g pu:eject:shell --dest=<portal>`) or add the `record:` line, otherwise they keep the icon fallback. Pass a record only: a String `current_user` (e.g. a guest) would otherwise be seeded as a literal identity.
|
|
550
568
|
|
|
551
|
-
## Translating
|
|
569
|
+
## Translating text
|
|
552
570
|
|
|
553
|
-
|
|
571
|
+
Put new user-facing text in a locale file, never a literal. Which `t` you get depends on where the code runs:
|
|
572
|
+
|
|
573
|
+
| Where | Call | Notes |
|
|
574
|
+
|---|---|---|
|
|
575
|
+
| Components, pages, and nested `Form` / `Display` / `Table` classes (render hooks included) | `t("full.key", **opts)` | Protected instance method from `Plutonium::UI::Component::Behaviour`; full keys only |
|
|
576
|
+
| `display` / `input` / `column` blocks in a definition | `t("full.key")` | The block is `instance_exec`ed by the page, so it is the page's `t` |
|
|
577
|
+
| Definition class body (`label:`, `hint:`, `placeholder:`, page titles) | `t("full.key")` | Class-level lazy `t` (`Plutonium::Translation::Lazy`), resolved per render in the request's locale |
|
|
578
|
+
| A bare Phlexi field component without `Behaviour` | `Plutonium::Translation.t("full.key")` | |
|
|
554
579
|
|
|
555
580
|
```ruby
|
|
556
581
|
def view_template
|
|
@@ -559,6 +584,8 @@ def view_template
|
|
|
559
584
|
end
|
|
560
585
|
```
|
|
561
586
|
|
|
587
|
+
Never call `I18n.t` directly in a definition class body: it runs once at load time, in whatever locale is active then.
|
|
588
|
+
|
|
562
589
|
Rules: one key per sentence with `%{name}` placeholders (never concatenate fragments around a value), `count:` for plurals, no `.downcase`/`.pluralize` on translated nouns. The gem's own keys live under `plutonium.*` in its `config/locales/en/*.yml` and any can be overridden from the app.
|
|
563
590
|
|
|
564
591
|
Stimulus controllers bundled with Plutonium read `plutonium.js.*` from a `<meta name="pu-i18n">` JSON blob the layout renders; host JS can call `window.Plutonium.t("plutonium.js.turbo_confirm.confirm")`. Library locales (Slim Select, flatpickr, Uppy, intl-tel-input) pass through `plutonium.js.libraries.*`. See `docs/reference/i18n.md`.
|
|
@@ -575,14 +602,14 @@ class PostCardComponent < Plutonium::UI::Component::Base
|
|
|
575
602
|
div(class: "bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)] p-4") do
|
|
576
603
|
h3(class: "font-bold text-[var(--pu-text)]") { @post.title }
|
|
577
604
|
p(class: "text-[var(--pu-text-muted)] mt-2") { @post.excerpt }
|
|
578
|
-
a(href: resource_url_for(@post), class: "text-primary-600") { "
|
|
605
|
+
a(href: resource_url_for(@post), class: "text-primary-600") { t("blog.posts.card.read_more") }
|
|
579
606
|
end
|
|
580
607
|
end
|
|
581
608
|
end
|
|
582
609
|
```
|
|
583
610
|
|
|
584
611
|
Use in a definition. A component with its **own constructor** (like the one above)
|
|
585
|
-
must use the **block form
|
|
612
|
+
must use the **block form**; you build it:
|
|
586
613
|
|
|
587
614
|
```ruby
|
|
588
615
|
display :card do |field|
|
|
@@ -640,13 +667,13 @@ All pages inherit this. Modals and frame navigation work without special handlin
|
|
|
640
667
|
|
|
641
668
|
---
|
|
642
669
|
|
|
643
|
-
# Part 5
|
|
670
|
+
# Part 5: Modals, Slideovers, Tabs
|
|
644
671
|
|
|
645
672
|
## Modal/slideover for `:new` / `:edit` + interactive actions
|
|
646
673
|
|
|
647
674
|
```ruby
|
|
648
675
|
class PostDefinition < ResourceDefinition
|
|
649
|
-
modal :slideover # default
|
|
676
|
+
modal :slideover # default, slide-in panel from the right
|
|
650
677
|
# modal :centered # centered dialog
|
|
651
678
|
# modal :centered, size: :lg # centered, wider container
|
|
652
679
|
# modal false # full standalone page
|
|
@@ -657,19 +684,19 @@ Drives both framework `:new` / `:edit` and every interactive action on the defin
|
|
|
657
684
|
|
|
658
685
|
## Tabs on the show page
|
|
659
686
|
|
|
660
|
-
Show pages with `permitted_associations` (see [[plutonium-behavior]]) 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
|
|
687
|
+
Show pages with `permitted_associations` (see [[plutonium-behavior]]) 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.
|
|
661
688
|
|
|
662
689
|
If the policy permits **no fields**, the empty Details tab is dropped and the first association tab leads instead.
|
|
663
690
|
|
|
664
691
|
---
|
|
665
692
|
|
|
666
|
-
# Part 6
|
|
693
|
+
# Part 6: Layout (Chrome) & Eject
|
|
667
694
|
|
|
668
695
|
## Shell
|
|
669
696
|
|
|
670
697
|
```ruby
|
|
671
698
|
Plutonium.configure do |config|
|
|
672
|
-
config.shell = :modern # default
|
|
699
|
+
config.shell = :modern # default, topbar + icon rail
|
|
673
700
|
# config.shell = :plain # topbar, no icon rail (whole app rail-less)
|
|
674
701
|
# config.shell = :classic # legacy header + sidebar (only when upgrading)
|
|
675
702
|
end
|
|
@@ -679,16 +706,16 @@ end
|
|
|
679
706
|
|
|
680
707
|
```ruby
|
|
681
708
|
config.shell = :plain # 1. global default
|
|
682
|
-
# 2. per-engine
|
|
709
|
+
# 2. per-engine: inside the engine's config.after_initialize (with scope_to_entity)
|
|
683
710
|
class CustomerPortal::Engine
|
|
684
711
|
config.after_initialize { shell :plain }
|
|
685
712
|
end
|
|
686
713
|
class DashboardController; shell :modern; end # 3. per-controller (overrides engine/global)
|
|
687
714
|
```
|
|
688
715
|
|
|
689
|
-
`shell` takes a symbol so the class body works too, but the generated engine already has a `config.after_initialize` block (home of `scope_to_entity`)
|
|
716
|
+
`shell` takes a symbol so the class body works too, but the generated engine already has a `config.after_initialize` block (home of `scope_to_entity`); keep it there for consistency.
|
|
690
717
|
|
|
691
|
-
Alongside `shell`, the controller-only `rail` DSL flips just the rail (inherited `class_attribute`, so a portal opts in/out once in its concern)
|
|
718
|
+
Alongside `shell`, the controller-only `rail` DSL flips just the rail (inherited `class_attribute`, so a portal opts in/out once in its concern), `rail false` / `rail true`; `rail nil` (default) inherits the resolved shell, `rail?` reads the resolved value:
|
|
692
719
|
|
|
693
720
|
```ruby
|
|
694
721
|
module CustomerPortal::Concerns::Controller
|
|
@@ -716,7 +743,7 @@ The sidebar/icon-rail menu is built with `Phlexi::Menu::Builder` in `_resource_s
|
|
|
716
743
|
m.item "Inbox", url: inbox_path, icon: Icon, target: "_blank", rel: "noopener", data: {turbo_frame: "_top"}
|
|
717
744
|
```
|
|
718
745
|
|
|
719
|
-
Applies to both shells (icon-rail leaf, parent flyout trigger, and flyout children; classic sidebar). Framework `class`/`data`/`aria` win on conflict
|
|
746
|
+
Applies to both shells (icon-rail leaf, parent flyout trigger, and flyout children; classic sidebar). Framework `class`/`data`/`aria` win on conflict; `class:` merges with the base classes, and on a parent trigger `data:`/`aria:` merge with the flyout wiring so options can't break the toggle. Phlexi's reserved `:active` key is never emitted as an attribute.
|
|
720
747
|
|
|
721
748
|
## Custom layout class (Phlex)
|
|
722
749
|
|
|
@@ -751,7 +778,7 @@ end
|
|
|
751
778
|
|
|
752
779
|
---
|
|
753
780
|
|
|
754
|
-
# Part 7
|
|
781
|
+
# Part 7: Assets, Tailwind, Stimulus
|
|
755
782
|
|
|
756
783
|
## Asset configuration
|
|
757
784
|
|
|
@@ -772,7 +799,17 @@ end
|
|
|
772
799
|
rails generate pu:core:assets
|
|
773
800
|
```
|
|
774
801
|
|
|
775
|
-
|
|
802
|
+
Until this runs, the app serves the gem's prebuilt assets (`config.assets.stylesheet` defaults to `plutonium.css`, `script` to `plutonium.min.js`, see `lib/plutonium/configuration.rb`). Those are compiled from the gem's own sources, so app-side Tailwind classes, a new `primary` palette, CSS token overrides and custom Stimulus controllers have nowhere to go. The generator:
|
|
803
|
+
|
|
804
|
+
- installs `@radioactive-labs/plutonium` (pinned to the gem version), Tailwind 4 and the PostCSS plugins
|
|
805
|
+
- writes `tailwind.config.js` (through `plutoniumTailwindConfig.merge`) and `postcss.config.js`
|
|
806
|
+
- prepends `@import "gem:plutonium/src/css/plutonium.css";` to `application.tailwind.css` and adds `@config` after `@import "tailwindcss";`
|
|
807
|
+
- appends `registerControllers(application)` to `app/javascript/controllers/index.js`
|
|
808
|
+
- sets `config.assets.stylesheet = "application"` and `config.assets.script = "application"`, and writes the `build` / `build:css` scripts in `package.json`
|
|
809
|
+
|
|
810
|
+
That last step is why `registerControllers` is not optional: the gem's `plutonium.min.js` calls it itself, and your `application.js` replaces that bundle.
|
|
811
|
+
|
|
812
|
+
**Prerequisites.** It 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. An app without them gets the bundlers installed first (`bin/rails javascript:install:esbuild`, `css:install:tailwind`, `stimulus:install` from jsbundling-rails / cssbundling-rails / stimulus-rails), then 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.
|
|
776
813
|
|
|
777
814
|
Packages install with the app's package manager, detected from the lockfile (`bun.lock`/`bun.lockb` → bun, `pnpm-lock.yaml` → pnpm, `package-lock.json` → npm, `yarn.lock` → yarn), else the first of bun, yarn, pnpm, npm on PATH. A yarn app stays yarn even with bun installed. Do not run `yarn add` by hand in a bun app: that leaves two lockfiles. Yarn 2+ needs `nodeLinker: node-modules` in `.yarnrc.yml` (the generator writes it); Tailwind's PostCSS plugin does not load under Plug'n'Play.
|
|
778
815
|
|
|
@@ -803,6 +840,8 @@ module.exports = {
|
|
|
803
840
|
|
|
804
841
|
## Default color palette
|
|
805
842
|
|
|
843
|
+
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` below plus a rebuild, not a `--pu-*` override.
|
|
844
|
+
|
|
806
845
|
| Color | Use |
|
|
807
846
|
|---|---|
|
|
808
847
|
| `primary` | Brand primary (turquoise default) |
|
|
@@ -853,7 +892,7 @@ application.register("custom", CustomController)
|
|
|
853
892
|
|
|
854
893
|
Bundled controllers: `color-mode`, `form` (pre-submit), `nested-resource-form-fields`, `slim-select`, `flatpickr`, `easymde`, `kanban`, `positioned` (drag-to-reorder), `row-click`, plus various internal UI controllers.
|
|
855
894
|
|
|
856
|
-
Custom controller
|
|
895
|
+
Custom controller, standard Stimulus:
|
|
857
896
|
|
|
858
897
|
```javascript
|
|
859
898
|
import { Controller } from "@hotwired/stimulus"
|
|
@@ -881,11 +920,11 @@ theme: { fontFamily: { body: ['Inter', 'sans-serif'], sans: ['Inter', 'sans-seri
|
|
|
881
920
|
|
|
882
921
|
## Dark mode
|
|
883
922
|
|
|
884
|
-
`selector` strategy
|
|
923
|
+
`selector` strategy: toggle by adding/removing `dark` on `<html>`. The `color-mode` Stimulus controller handles it; Plutonium ships a switcher.
|
|
885
924
|
|
|
886
925
|
---
|
|
887
926
|
|
|
888
|
-
# Part 8
|
|
927
|
+
# Part 8: Design Tokens & `.pu-*` Component Classes
|
|
889
928
|
|
|
890
929
|
Plutonium uses CSS custom properties for surfaces, text, borders, forms, cards, shadows, radii, spacing, and transitions. Tokens auto-switch with dark mode. Source: `src/css/tokens.css`.
|
|
891
930
|
|
|
@@ -898,12 +937,15 @@ Plutonium uses CSS custom properties for surfaces, text, borders, forms, cards,
|
|
|
898
937
|
| `--pu-border`, `--pu-border-muted`, `--pu-border-strong` | Borders |
|
|
899
938
|
| `--pu-input-bg`, `--pu-input-border`, `--pu-input-focus-ring`, `--pu-input-placeholder` | Form inputs |
|
|
900
939
|
| `--pu-card-bg`, `--pu-card-border` | Cards |
|
|
940
|
+
| `--pu-table-header-bg`, `--pu-table-header-text`, `--pu-table-row-bg`, `--pu-table-row-hover`, `--pu-table-row-selected`, `--pu-table-border` | Tables |
|
|
941
|
+
| `--pu-text-danger` | Error text |
|
|
942
|
+
| `--pu-chart-1` … `--pu-chart-8` | Dashboard chart series |
|
|
901
943
|
| `--pu-shadow-sm/md/lg` | Shadows |
|
|
902
944
|
| `--pu-radius-sm/md/lg/xl/full` | Border radius |
|
|
903
945
|
| `--pu-space-xs/sm/md/lg/xl` | Spacing |
|
|
904
946
|
| `--pu-transition-fast/normal/slow` | Transitions |
|
|
905
947
|
|
|
906
|
-
🚨 Tokens are CSS variables
|
|
948
|
+
🚨 Tokens are CSS variables: use `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
907
949
|
|
|
908
950
|
## Customizing tokens
|
|
909
951
|
|
|
@@ -919,11 +961,23 @@ Plutonium uses CSS custom properties for surfaces, text, borders, forms, cards,
|
|
|
919
961
|
}
|
|
920
962
|
```
|
|
921
963
|
|
|
922
|
-
🚨 **Mirror every `:root` override in `.dark`.** The app stylesheet loads after Plutonium's and `:root`/`.dark` have equal specificity, so a `:root`-only override beats Plutonium's `.dark` value even in dark mode
|
|
964
|
+
🚨 **Mirror every `:root` override in `.dark`.** The app stylesheet loads after Plutonium's and `:root`/`.dark` have equal specificity, so a `:root`-only override beats Plutonium's `.dark` value even in dark mode, so your light color ships into dark mode, often unreadably (e.g. translucent navy `--pu-text-subtle` is invisible on a dark surface). Every color token customized in `:root` MUST be re-asserted with a dark value in `.dark`. 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.
|
|
965
|
+
|
|
966
|
+
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.
|
|
967
|
+
|
|
968
|
+
Overrides need the app's own stylesheet, after the Plutonium import (`pu:core:assets`). Never edit the gem's `tokens.css` / `components.css`.
|
|
923
969
|
|
|
924
970
|
## `.pu-*` component classes
|
|
925
971
|
|
|
926
|
-
Ready-to-use styled components in `src/css/components.css`. **Prefer these over hardcoded `gray-X/dark:gray-Y` pairs.**
|
|
972
|
+
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.**
|
|
973
|
+
|
|
974
|
+
Why, in order of preference:
|
|
975
|
+
|
|
976
|
+
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.
|
|
977
|
+
2. **A `var(--pu-*)` token** (`text-[var(--pu-text-muted)]`, `border-[var(--pu-border)]`) for layout around them. The token switches value under `.dark`, so one class covers both modes and follows any theme override.
|
|
978
|
+
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 every one needs a hand-picked `dark:` twin that won't follow a rebrand.
|
|
979
|
+
|
|
980
|
+
For a status banner use `pu-alert pu-alert-<variant>` with a `pu-alert-message` child; for an inline status chip, `pu-badge pu-badge-<variant>`.
|
|
927
981
|
|
|
928
982
|
### Buttons
|
|
929
983
|
|
|
@@ -944,6 +998,7 @@ Ready-to-use styled components in `src/css/components.css`. **Prefer these over
|
|
|
944
998
|
```
|
|
945
999
|
.pu-input / -invalid / -valid .pu-label / -required .pu-hint / .pu-error .pu-checkbox / .pu-toggle
|
|
946
1000
|
.pu-badge / -neutral / -primary / -secondary / -success / -danger / -warning / -info / -accent
|
|
1001
|
+
.pu-alert / -success / -warning / -danger / -info .pu-alert-message / .pu-alert-close
|
|
947
1002
|
.pu-card / .pu-card-body
|
|
948
1003
|
.pu-panel-header / -title / -description
|
|
949
1004
|
.pu-table-wrapper / .pu-table / -header / -header-cell / -body-row / -body-row-selected / -body-cell / .pu-selection-cell
|
|
@@ -1006,9 +1061,9 @@ tokens("base", condition?: {then: "if-true", else: "if-false"})
|
|
|
1006
1061
|
|
|
1007
1062
|
---
|
|
1008
1063
|
|
|
1009
|
-
# Part 9
|
|
1064
|
+
# Part 9: Phlexi Component Themes
|
|
1010
1065
|
|
|
1011
|
-
Themes are Ruby classes nested under a Form/Display/Table override. They merge into Plutonium's defaults
|
|
1066
|
+
Themes are Ruby classes nested under a Form/Display/Table override. They merge into Plutonium's defaults, never replace wholesale: always `super.merge(...)`.
|
|
1012
1067
|
|
|
1013
1068
|
## Form theme
|
|
1014
1069
|
|
|
@@ -1036,7 +1091,7 @@ end
|
|
|
1036
1091
|
|
|
1037
1092
|
`base`, `sectioned_base`, `fields_wrapper`, `sections_wrapper`, `actions_wrapper`, `wrapper`, `inner_wrapper`, `label`, `invalid_label`, `valid_label`, `neutral_label`, `input`, `invalid_input`, `valid_input`, `neutral_input`, `hint`, `error`, `button`, `checkbox`, `select`, plus the shared section keys below.
|
|
1038
1093
|
|
|
1039
|
-
`sectioned_base` replaces `base` when the definition declares a `form_layout
|
|
1094
|
+
`sectioned_base` replaces `base` when the definition declares a `form_layout`, because the sections are cards, so the form itself stops being one.
|
|
1040
1095
|
|
|
1041
1096
|
⚠️ **Width is NOT a theme key.** It's configuration (`page_width` / `form_width` on the definition, `Plutonium.configuration.default_page_width` globally) and is appended by `Form::Resource`/`Page::Show`, so overriding `base` or `fields_wrapper` restyles a surface without silently pinning its width. See [[plutonium-resource]] › Page Width.
|
|
1042
1097
|
|
|
@@ -1067,7 +1122,7 @@ end
|
|
|
1067
1122
|
|
|
1068
1123
|
Section chrome is shared: `Plutonium::UI::Component::Section::DEFAULT_THEME` is merged into **both** `Form::Theme` and `Display::Theme`, so the two read identically by default while staying independently overridable.
|
|
1069
1124
|
|
|
1070
|
-
`section_wrapper` (merged into the section's Block
|
|
1125
|
+
`section_wrapper` (merged into the section's Block, Block already supplies `pu-card`), `section_header`, `section_summary` (the collapsible header row), `section_accent`, `section_heading`, `section_description`, `section_caret`, `section_body`, plus `sections_wrapper` (the container that stacks sections).
|
|
1071
1126
|
|
|
1072
1127
|
## Table theme
|
|
1073
1128
|
|
|
@@ -1096,7 +1151,7 @@ end
|
|
|
1096
1151
|
|
|
1097
1152
|
## Available context
|
|
1098
1153
|
|
|
1099
|
-
Inside any page / form / display / Phlex component, the same set of helpers is available
|
|
1154
|
+
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 [[plutonium-behavior]] › Key methods (controllers expose the same surface; pages inherit it).
|
|
1100
1155
|
|
|
1101
1156
|
In Phlex components, Rails helpers are accessed via the `helpers` proxy:
|
|
1102
1157
|
|
|
@@ -1130,12 +1185,12 @@ end
|
|
|
1130
1185
|
|
|
1131
1186
|
## Gotchas
|
|
1132
1187
|
|
|
1133
|
-
- **Don't override `view_template` in pages** when a render hook fits
|
|
1134
|
-
- **Always register Stimulus controllers.**
|
|
1135
|
-
- **Use `plutoniumTailwindConfig.merge
|
|
1188
|
+
- **Don't override `view_template` in pages** when a render hook fits; you lose breadcrumbs / header / DynaFrame behavior.
|
|
1189
|
+
- **Always register Stimulus controllers in your own bundle.** Once `config.assets.script` points at the app's JS, without `registerControllers(application)` the entire UI's interactive layer is dead.
|
|
1190
|
+
- **Use `plutoniumTailwindConfig.merge`**: plain object merge drops Plutonium's defaults.
|
|
1136
1191
|
- **Dark mode is `selector`, not `class`.** Toggle via `document.documentElement.classList.toggle('dark')`.
|
|
1137
|
-
- **Tokens are CSS variables, not Tailwind keys
|
|
1138
|
-
- **`render_actions` is mandatory in custom `form_template
|
|
1192
|
+
- **Tokens are CSS variables, not Tailwind keys**: `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
1193
|
+
- **`render_actions` is mandatory in custom `form_template`**: otherwise no submit button.
|
|
1139
1194
|
- **Dropdowns (`resource-drop-down`) teleport their menu to `<body>` while open.** popper's `fixed` strategy alone is still clipped by a transformed + `overflow:hidden` ancestor (e.g. grid cards, app shells), so the controller reparents the open menu to `<body>` and restores it on close. Don't rely on the menu being a DOM child of its trigger while open.
|
|
1140
1195
|
- **`DisplaysValue` components stringify the value and loop per item.** `render_value` receives `normalize_value(value)`, which is `value.to_s`, and for a `field.multiple?` (has_many) field it is called once per element (`field.value.each`). So a custom component that inherits `DisplaysValue` only ever sees the stringified value, per item, never the record. When you need `f.object` or whole-collection rendering, use the block-form display (`display :x do |f| … end`), which is `instance_exec`ed in Phlex once and can emit markup directly.
|
|
1141
1196
|
- **Blocks run in a Phlex context on every surface.** A `display`, `input`, or `column` block emits `span`/`div` directly, or returns a String or a component. All three are `instance_exec`ed by the resource page rendering them (`self` is the page, not the definition): `display`/`input` blocks receive the field `f`, a `column` block receives the record. Phlex renders the return value too, so a block that emits markup must end with a tag call or `nil`. Most columns need no block at all, because `display :x, as: …` already flows to the table column.
|
|
@@ -1146,7 +1201,7 @@ end
|
|
|
1146
1201
|
|
|
1147
1202
|
## Related skills
|
|
1148
1203
|
|
|
1149
|
-
- [[plutonium-resource]]
|
|
1150
|
-
- [[plutonium-behavior]]
|
|
1151
|
-
- [[plutonium-app]]
|
|
1152
|
-
- [[plutonium-tenancy]]
|
|
1204
|
+
- [[plutonium-resource]]: field/input/display config (`as:`, `condition:`, blocks); modal options for actions.
|
|
1205
|
+
- [[plutonium-behavior]]: controller presentation hooks (`present_parent?`), available helpers (`resource_record!`, `current_scoped_entity`).
|
|
1206
|
+
- [[plutonium-app]]: `pu:eject:layout`, `pu:eject:shell`, portal package overrides.
|
|
1207
|
+
- [[plutonium-tenancy]]: `permitted_associations` drives the show-page tablist.
|