plutonium 0.65.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +158 -141
- 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 +161 -98
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +43 -0
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +56 -3
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +4 -4
- data/config/brakeman.ignore +1 -1
- data/config/locales/en/js.yml +4 -0
- data/config/locales/en/ui.yml +16 -0
- 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 +85 -80
- 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 +24 -14
- 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 +32 -15
- data/docs/reference/ui/forms.md +89 -39
- 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_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +13 -3
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +10 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +10 -1
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/plutonium/auth/rodauth.rb +7 -0
- data/lib/plutonium/core/controller.rb +0 -2
- data/lib/plutonium/helpers/content_helper.rb +0 -3
- data/lib/plutonium/query/filter.rb +2 -1
- data/lib/plutonium/query/filters/tags.rb +36 -0
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
- data/lib/plutonium/ui/display/base.rb +50 -2
- data/lib/plutonium/ui/display/components/badge.rb +1 -1
- data/lib/plutonium/ui/display/components/binary.rb +20 -0
- data/lib/plutonium/ui/display/components/boolean.rb +1 -1
- data/lib/plutonium/ui/display/components/code.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +14 -6
- data/lib/plutonium/ui/display/components/duration.rb +31 -0
- data/lib/plutonium/ui/display/components/file_size.rb +22 -0
- data/lib/plutonium/ui/display/components/key_value.rb +37 -0
- data/lib/plutonium/ui/display/components/list.rb +33 -0
- data/lib/plutonium/ui/display/components/number.rb +32 -0
- data/lib/plutonium/ui/display/components/progress.rb +36 -0
- data/lib/plutonium/ui/display/components/rating.rb +36 -0
- data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
- data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
- data/lib/plutonium/ui/display/components/tags.rb +18 -0
- data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
- data/lib/plutonium/ui/display/theme.rb +24 -2
- data/lib/plutonium/ui/form/base.rb +30 -1
- data/lib/plutonium/ui/form/components/binary.rb +40 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
- data/lib/plutonium/ui/form/components/list.rb +46 -0
- data/lib/plutonium/ui/form/components/rating.rb +51 -0
- data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
- data/lib/plutonium/ui/form/components/tags.rb +45 -0
- data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
- data/lib/plutonium/ui/form/components/uppy.rb +1 -1
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
- data/lib/plutonium/ui/form/theme.rb +20 -4
- data/lib/plutonium/ui/grid/card.rb +13 -1
- data/lib/plutonium/ui/layout/base.rb +10 -1
- data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
- data/lib/plutonium/ui/options/secret_field.rb +32 -0
- data/lib/plutonium/ui/options/tags_field.rb +23 -0
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
- data/lib/plutonium/ui/table/display_theme.rb +30 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +53 -10
- data/src/css/flatpickr.css +91 -0
- data/src/js/controllers/easymde_controller.js +1 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/slim_select_controller.js +32 -3
- data/src/js/controllers/timeago_controller.js +40 -0
- metadata +25 -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
|
|
|
@@ -306,9 +309,9 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
306
309
|
| Tag | Input |
|
|
307
310
|
|---|---|
|
|
308
311
|
| `input_tag` | text (auto-detected type) |
|
|
309
|
-
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag
|
|
312
|
+
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag` / `secret_tag`, `url_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
|
|
|
@@ -322,6 +325,13 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
322
325
|
| `secure_association_tag` | Association with policy-checked options |
|
|
323
326
|
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
324
327
|
| `key_value_store_tag` | Key/value pairs editor |
|
|
328
|
+
| `currency_tag` | Money input (number field + unit prefix) |
|
|
329
|
+
| `rich_text_tag` | Action Text editor (Trix, or Lexxy once the app installs it); default for `has_rich_text` |
|
|
330
|
+
| `list_tag` | Chip input for array values (`suggestions:`, `limit:`, `addable: false`) |
|
|
331
|
+
| `tags_tag` | Chip input for a no_fly_list `has_tags` context; default for those contexts |
|
|
332
|
+
| `rating_tag` | Star picker (`max:`, default 5); a Clear option unless required |
|
|
333
|
+
| `binary_tag` | Plain file input whose bytes go into a `binary` column |
|
|
334
|
+
| `json_tag` | JSON editor; default for `json`/`jsonb` columns |
|
|
325
335
|
|
|
326
336
|
```ruby
|
|
327
337
|
render field(:published_at).wrapped { |f| f.flatpickr_tag(min_date: Date.today, enable_time: true) }
|
|
@@ -330,13 +340,14 @@ render field(:avatar).wrapped { |f| f.uppy_tag(allowed_file_types: %w[.jpg
|
|
|
330
340
|
|
|
331
341
|
### Password & secret fields
|
|
332
342
|
|
|
333
|
-
`password_tag` masks the stored value
|
|
343
|
+
`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
344
|
|
|
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
|
|
345
|
+
Auto-detected by name: `password`/`token`/`salt`, `encrypted_*`, `*_password`/`*_digest`/`*_hash`/`*_token`/`*_key`/`*_salt`, or any name containing `secret`. The same names render `••••••••` on the show page, table, wizard summary and grid/kanban cards; `field`/`display` `as:` overrides it there too. A convenience, **not** a guarantee: odd-named secrets (`recovery_phrase`, `pin`) still leak unless masked explicitly.
|
|
336
346
|
|
|
337
347
|
```ruby
|
|
338
|
-
field :api_token, as: :string # opt OUT
|
|
339
|
-
field :recovery_phrase, as: :password # opt IN
|
|
348
|
+
field :api_token, as: :string # opt OUT: show a readable value (token to copy, checksum)
|
|
349
|
+
field :recovery_phrase, as: :password # opt IN: mask a secret the heuristic misses
|
|
350
|
+
field :pin, as: :secret # :secret is an alias of :password
|
|
340
351
|
```
|
|
341
352
|
|
|
342
353
|
## Submit buttons
|
|
@@ -347,7 +358,7 @@ Control the secondary button via the definition:
|
|
|
347
358
|
|
|
348
359
|
```ruby
|
|
349
360
|
class PostDefinition < ResourceDefinition
|
|
350
|
-
submit_and_continue false # nil (default
|
|
361
|
+
submit_and_continue false # nil (default, auto), true (always show), false (always hide)
|
|
351
362
|
end
|
|
352
363
|
```
|
|
353
364
|
|
|
@@ -369,14 +380,14 @@ end
|
|
|
369
380
|
|
|
370
381
|
These all live in the definition layer:
|
|
371
382
|
|
|
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
|
|
383
|
+
- **Pre-submit / dynamic forms**: see [[plutonium-resource]] › Dynamic Forms.
|
|
384
|
+
- **Nested inputs** (`nested_input :variants`): association-backed inline forms; see [[plutonium-resource]] › Nested Inputs.
|
|
385
|
+
- **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.
|
|
386
|
+
- **Interaction forms**: interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [[plutonium-behavior]] › Interactions.
|
|
376
387
|
|
|
377
388
|
---
|
|
378
389
|
|
|
379
|
-
# Part 3
|
|
390
|
+
# Part 3: Display & Table
|
|
380
391
|
|
|
381
392
|
## Custom Display
|
|
382
393
|
|
|
@@ -390,7 +401,7 @@ class PostDefinition < ResourceDefinition
|
|
|
390
401
|
end
|
|
391
402
|
|
|
392
403
|
# `fields_wrapper` is ALREADY a card (it renders a Block internally),
|
|
393
|
-
# so do not wrap it in another one
|
|
404
|
+
# so do not wrap it in another one: that stacks two cards and doubles
|
|
394
405
|
# the border and shadow.
|
|
395
406
|
fields_wrapper do
|
|
396
407
|
render_resource_field :author
|
|
@@ -412,14 +423,14 @@ end
|
|
|
412
423
|
|---|---|
|
|
413
424
|
| `render_fields` | All permitted fields |
|
|
414
425
|
| `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
|
|
426
|
+
| `render_associations` | Association tabs (driven by `permitted_associations`, see [[plutonium-behavior]]) |
|
|
427
|
+
| `render_before_fields` / `render_after_fields` | Hooks around the fields, **Details tab only** |
|
|
417
428
|
| `object` | The record |
|
|
418
429
|
| `resource_fields`, `resource_associations` | Permitted lists |
|
|
419
430
|
|
|
420
431
|
### Details-tab-only content
|
|
421
432
|
|
|
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
|
|
433
|
+
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
434
|
|
|
424
435
|
```ruby
|
|
425
436
|
class PostDefinition < ResourceDefinition
|
|
@@ -427,13 +438,28 @@ class PostDefinition < ResourceDefinition
|
|
|
427
438
|
private
|
|
428
439
|
|
|
429
440
|
def render_before_fields
|
|
430
|
-
|
|
441
|
+
flagged = object.comments.where(flagged: true).count
|
|
442
|
+
return if flagged.zero?
|
|
443
|
+
|
|
444
|
+
div(class: "pu-alert pu-alert-warning", role: "alert") do
|
|
445
|
+
div(class: "pu-alert-message") { t("blog.posts.flagged_comments", count: flagged) }
|
|
446
|
+
end
|
|
431
447
|
end
|
|
432
448
|
end
|
|
433
449
|
end
|
|
434
450
|
```
|
|
435
451
|
|
|
436
|
-
|
|
452
|
+
```yaml
|
|
453
|
+
# config/locales/en.yml
|
|
454
|
+
en:
|
|
455
|
+
blog:
|
|
456
|
+
posts:
|
|
457
|
+
flagged_comments:
|
|
458
|
+
one: "1 comment is waiting for moderation."
|
|
459
|
+
other: "%{count} comments are waiting for moderation."
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`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
463
|
|
|
438
464
|
## Custom Table
|
|
439
465
|
|
|
@@ -472,7 +498,7 @@ end
|
|
|
472
498
|
|
|
473
499
|
## Drag-to-Reorder Affordance (`position_on`)
|
|
474
500
|
|
|
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
|
|
501
|
+
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
502
|
|
|
477
503
|
**What renders where:**
|
|
478
504
|
|
|
@@ -480,11 +506,11 @@ When a definition declares `position_on` (see [[plutonium-resource]]) the index
|
|
|
480
506
|
|---|---|---|---|
|
|
481
507
|
| Index / nested table | the **grip only**, never the `<tr>` | inside the first cell's existing left padding (content does not shift) | vertical |
|
|
482
508
|
| Card grid | the **grip only** | floated over the card's top-left corner | horizontal, wrap-aware |
|
|
483
|
-
| Kanban board | the **whole card** |
|
|
509
|
+
| Kanban board | the **whole card** | - | both (cross-column) |
|
|
484
510
|
|
|
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
|
|
511
|
+
🚨 **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
512
|
|
|
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
|
|
513
|
+
**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
514
|
|
|
489
515
|
**DOM contract** (relevant if you eject a table/grid or write a custom collection component):
|
|
490
516
|
|
|
@@ -493,18 +519,18 @@ wrapper data-controller="positioned"
|
|
|
493
519
|
data-positioned-url-template-value="/things/__ID__/reposition"
|
|
494
520
|
data-positioned-axis-value="horizontal" # grid only
|
|
495
521
|
row/card data-positioned-row-id="<id>" # single source of truth for the record id
|
|
496
|
-
grip data-positioned-grip # a real <button
|
|
522
|
+
grip data-positioned-grip # a real <button>, tabbable
|
|
497
523
|
```
|
|
498
524
|
|
|
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
|
|
525
|
+
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
526
|
|
|
501
|
-
**Accessibility.** Focus the grip and use <kbd>↑</kbd>/<kbd>↓</kbd>
|
|
527
|
+
**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
528
|
|
|
503
529
|
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
530
|
|
|
505
531
|
---
|
|
506
532
|
|
|
507
|
-
# Part 4
|
|
533
|
+
# Part 4: Component Kit & Custom Components
|
|
508
534
|
|
|
509
535
|
## Built-in shorthand kit
|
|
510
536
|
|
|
@@ -528,7 +554,7 @@ Breadcrumbs()
|
|
|
528
554
|
|
|
529
555
|
## Avatar
|
|
530
556
|
|
|
531
|
-
`Avatar(subject = nil, src: nil, size: :md, alt: nil, **attrs)
|
|
557
|
+
`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
558
|
|
|
533
559
|
```ruby
|
|
534
560
|
Avatar(user) # Navii fallback seeded from the record
|
|
@@ -540,17 +566,24 @@ Avatar(src: avatar_url) # bare image, no subject/fallback
|
|
|
540
566
|
```
|
|
541
567
|
|
|
542
568
|
- **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
|
|
569
|
+
- **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
570
|
- **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
|
|
571
|
+
- **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
572
|
- **Resolution order**: resolved `src` → Navii (from subject) → generic user icon.
|
|
547
573
|
- **Config**: `config.navii_host_url` (default `https://api.navii.dev`); the component appends `/avatar/:seed`.
|
|
548
574
|
|
|
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
|
|
575
|
+
🚨 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
576
|
|
|
551
|
-
## Translating
|
|
577
|
+
## Translating text
|
|
552
578
|
|
|
553
|
-
|
|
579
|
+
Put new user-facing text in a locale file, never a literal. Which `t` you get depends on where the code runs:
|
|
580
|
+
|
|
581
|
+
| Where | Call | Notes |
|
|
582
|
+
|---|---|---|
|
|
583
|
+
| 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 |
|
|
584
|
+
| `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` |
|
|
585
|
+
| 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 |
|
|
586
|
+
| A bare Phlexi field component without `Behaviour` | `Plutonium::Translation.t("full.key")` | |
|
|
554
587
|
|
|
555
588
|
```ruby
|
|
556
589
|
def view_template
|
|
@@ -559,6 +592,8 @@ def view_template
|
|
|
559
592
|
end
|
|
560
593
|
```
|
|
561
594
|
|
|
595
|
+
Never call `I18n.t` directly in a definition class body: it runs once at load time, in whatever locale is active then.
|
|
596
|
+
|
|
562
597
|
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
598
|
|
|
564
599
|
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 +610,14 @@ class PostCardComponent < Plutonium::UI::Component::Base
|
|
|
575
610
|
div(class: "bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)] p-4") do
|
|
576
611
|
h3(class: "font-bold text-[var(--pu-text)]") { @post.title }
|
|
577
612
|
p(class: "text-[var(--pu-text-muted)] mt-2") { @post.excerpt }
|
|
578
|
-
a(href: resource_url_for(@post), class: "text-primary-600") { "
|
|
613
|
+
a(href: resource_url_for(@post), class: "text-primary-600") { t("blog.posts.card.read_more") }
|
|
579
614
|
end
|
|
580
615
|
end
|
|
581
616
|
end
|
|
582
617
|
```
|
|
583
618
|
|
|
584
619
|
Use in a definition. A component with its **own constructor** (like the one above)
|
|
585
|
-
must use the **block form
|
|
620
|
+
must use the **block form**; you build it:
|
|
586
621
|
|
|
587
622
|
```ruby
|
|
588
623
|
display :card do |field|
|
|
@@ -640,13 +675,13 @@ All pages inherit this. Modals and frame navigation work without special handlin
|
|
|
640
675
|
|
|
641
676
|
---
|
|
642
677
|
|
|
643
|
-
# Part 5
|
|
678
|
+
# Part 5: Modals, Slideovers, Tabs
|
|
644
679
|
|
|
645
680
|
## Modal/slideover for `:new` / `:edit` + interactive actions
|
|
646
681
|
|
|
647
682
|
```ruby
|
|
648
683
|
class PostDefinition < ResourceDefinition
|
|
649
|
-
modal :slideover # default
|
|
684
|
+
modal :slideover # default, slide-in panel from the right
|
|
650
685
|
# modal :centered # centered dialog
|
|
651
686
|
# modal :centered, size: :lg # centered, wider container
|
|
652
687
|
# modal false # full standalone page
|
|
@@ -657,19 +692,19 @@ Drives both framework `:new` / `:edit` and every interactive action on the defin
|
|
|
657
692
|
|
|
658
693
|
## Tabs on the show page
|
|
659
694
|
|
|
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
|
|
695
|
+
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
696
|
|
|
662
697
|
If the policy permits **no fields**, the empty Details tab is dropped and the first association tab leads instead.
|
|
663
698
|
|
|
664
699
|
---
|
|
665
700
|
|
|
666
|
-
# Part 6
|
|
701
|
+
# Part 6: Layout (Chrome) & Eject
|
|
667
702
|
|
|
668
703
|
## Shell
|
|
669
704
|
|
|
670
705
|
```ruby
|
|
671
706
|
Plutonium.configure do |config|
|
|
672
|
-
config.shell = :modern # default
|
|
707
|
+
config.shell = :modern # default, topbar + icon rail
|
|
673
708
|
# config.shell = :plain # topbar, no icon rail (whole app rail-less)
|
|
674
709
|
# config.shell = :classic # legacy header + sidebar (only when upgrading)
|
|
675
710
|
end
|
|
@@ -679,16 +714,16 @@ end
|
|
|
679
714
|
|
|
680
715
|
```ruby
|
|
681
716
|
config.shell = :plain # 1. global default
|
|
682
|
-
# 2. per-engine
|
|
717
|
+
# 2. per-engine: inside the engine's config.after_initialize (with scope_to_entity)
|
|
683
718
|
class CustomerPortal::Engine
|
|
684
719
|
config.after_initialize { shell :plain }
|
|
685
720
|
end
|
|
686
721
|
class DashboardController; shell :modern; end # 3. per-controller (overrides engine/global)
|
|
687
722
|
```
|
|
688
723
|
|
|
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`)
|
|
724
|
+
`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
725
|
|
|
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)
|
|
726
|
+
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
727
|
|
|
693
728
|
```ruby
|
|
694
729
|
module CustomerPortal::Concerns::Controller
|
|
@@ -716,7 +751,7 @@ The sidebar/icon-rail menu is built with `Phlexi::Menu::Builder` in `_resource_s
|
|
|
716
751
|
m.item "Inbox", url: inbox_path, icon: Icon, target: "_blank", rel: "noopener", data: {turbo_frame: "_top"}
|
|
717
752
|
```
|
|
718
753
|
|
|
719
|
-
Applies to both shells (icon-rail leaf, parent flyout trigger, and flyout children; classic sidebar). Framework `class`/`data`/`aria` win on conflict
|
|
754
|
+
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
755
|
|
|
721
756
|
## Custom layout class (Phlex)
|
|
722
757
|
|
|
@@ -751,7 +786,7 @@ end
|
|
|
751
786
|
|
|
752
787
|
---
|
|
753
788
|
|
|
754
|
-
# Part 7
|
|
789
|
+
# Part 7: Assets, Tailwind, Stimulus
|
|
755
790
|
|
|
756
791
|
## Asset configuration
|
|
757
792
|
|
|
@@ -772,7 +807,17 @@ end
|
|
|
772
807
|
rails generate pu:core:assets
|
|
773
808
|
```
|
|
774
809
|
|
|
775
|
-
|
|
810
|
+
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:
|
|
811
|
+
|
|
812
|
+
- installs `@radioactive-labs/plutonium` (pinned to the gem version), Tailwind 4 and the PostCSS plugins
|
|
813
|
+
- writes `tailwind.config.js` (through `plutoniumTailwindConfig.merge`) and `postcss.config.js`
|
|
814
|
+
- prepends `@import "gem:plutonium/src/css/plutonium.css";` to `application.tailwind.css` and adds `@config` after `@import "tailwindcss";`
|
|
815
|
+
- appends `registerControllers(application)` to `app/javascript/controllers/index.js`
|
|
816
|
+
- sets `config.assets.stylesheet = "application"` and `config.assets.script = "application"`, and writes the `build` / `build:css` scripts in `package.json`
|
|
817
|
+
|
|
818
|
+
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.
|
|
819
|
+
|
|
820
|
+
**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
821
|
|
|
777
822
|
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
823
|
|
|
@@ -803,6 +848,8 @@ module.exports = {
|
|
|
803
848
|
|
|
804
849
|
## Default color palette
|
|
805
850
|
|
|
851
|
+
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.
|
|
852
|
+
|
|
806
853
|
| Color | Use |
|
|
807
854
|
|---|---|
|
|
808
855
|
| `primary` | Brand primary (turquoise default) |
|
|
@@ -853,7 +900,7 @@ application.register("custom", CustomController)
|
|
|
853
900
|
|
|
854
901
|
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
902
|
|
|
856
|
-
Custom controller
|
|
903
|
+
Custom controller, standard Stimulus:
|
|
857
904
|
|
|
858
905
|
```javascript
|
|
859
906
|
import { Controller } from "@hotwired/stimulus"
|
|
@@ -881,11 +928,11 @@ theme: { fontFamily: { body: ['Inter', 'sans-serif'], sans: ['Inter', 'sans-seri
|
|
|
881
928
|
|
|
882
929
|
## Dark mode
|
|
883
930
|
|
|
884
|
-
`selector` strategy
|
|
931
|
+
`selector` strategy: toggle by adding/removing `dark` on `<html>`. The `color-mode` Stimulus controller handles it; Plutonium ships a switcher.
|
|
885
932
|
|
|
886
933
|
---
|
|
887
934
|
|
|
888
|
-
# Part 8
|
|
935
|
+
# Part 8: Design Tokens & `.pu-*` Component Classes
|
|
889
936
|
|
|
890
937
|
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
938
|
|
|
@@ -898,12 +945,15 @@ Plutonium uses CSS custom properties for surfaces, text, borders, forms, cards,
|
|
|
898
945
|
| `--pu-border`, `--pu-border-muted`, `--pu-border-strong` | Borders |
|
|
899
946
|
| `--pu-input-bg`, `--pu-input-border`, `--pu-input-focus-ring`, `--pu-input-placeholder` | Form inputs |
|
|
900
947
|
| `--pu-card-bg`, `--pu-card-border` | Cards |
|
|
948
|
+
| `--pu-table-header-bg`, `--pu-table-header-text`, `--pu-table-row-bg`, `--pu-table-row-hover`, `--pu-table-row-selected`, `--pu-table-border` | Tables |
|
|
949
|
+
| `--pu-text-danger` | Error text |
|
|
950
|
+
| `--pu-chart-1` … `--pu-chart-8` | Dashboard chart series |
|
|
901
951
|
| `--pu-shadow-sm/md/lg` | Shadows |
|
|
902
952
|
| `--pu-radius-sm/md/lg/xl/full` | Border radius |
|
|
903
953
|
| `--pu-space-xs/sm/md/lg/xl` | Spacing |
|
|
904
954
|
| `--pu-transition-fast/normal/slow` | Transitions |
|
|
905
955
|
|
|
906
|
-
🚨 Tokens are CSS variables
|
|
956
|
+
🚨 Tokens are CSS variables: use `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
907
957
|
|
|
908
958
|
## Customizing tokens
|
|
909
959
|
|
|
@@ -919,11 +969,23 @@ Plutonium uses CSS custom properties for surfaces, text, borders, forms, cards,
|
|
|
919
969
|
}
|
|
920
970
|
```
|
|
921
971
|
|
|
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
|
|
972
|
+
🚨 **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.
|
|
973
|
+
|
|
974
|
+
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.
|
|
975
|
+
|
|
976
|
+
Overrides need the app's own stylesheet, after the Plutonium import (`pu:core:assets`). Never edit the gem's `tokens.css` / `components.css`.
|
|
923
977
|
|
|
924
978
|
## `.pu-*` component classes
|
|
925
979
|
|
|
926
|
-
Ready-to-use styled components in `src/css/components.css`. **Prefer these over hardcoded `gray-X/dark:gray-Y` pairs.**
|
|
980
|
+
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.**
|
|
981
|
+
|
|
982
|
+
Why, in order of preference:
|
|
983
|
+
|
|
984
|
+
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.
|
|
985
|
+
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.
|
|
986
|
+
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.
|
|
987
|
+
|
|
988
|
+
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
989
|
|
|
928
990
|
### Buttons
|
|
929
991
|
|
|
@@ -944,6 +1006,7 @@ Ready-to-use styled components in `src/css/components.css`. **Prefer these over
|
|
|
944
1006
|
```
|
|
945
1007
|
.pu-input / -invalid / -valid .pu-label / -required .pu-hint / .pu-error .pu-checkbox / .pu-toggle
|
|
946
1008
|
.pu-badge / -neutral / -primary / -secondary / -success / -danger / -warning / -info / -accent
|
|
1009
|
+
.pu-alert / -success / -warning / -danger / -info .pu-alert-message / .pu-alert-close
|
|
947
1010
|
.pu-card / .pu-card-body
|
|
948
1011
|
.pu-panel-header / -title / -description
|
|
949
1012
|
.pu-table-wrapper / .pu-table / -header / -header-cell / -body-row / -body-row-selected / -body-cell / .pu-selection-cell
|
|
@@ -1006,9 +1069,9 @@ tokens("base", condition?: {then: "if-true", else: "if-false"})
|
|
|
1006
1069
|
|
|
1007
1070
|
---
|
|
1008
1071
|
|
|
1009
|
-
# Part 9
|
|
1072
|
+
# Part 9: Phlexi Component Themes
|
|
1010
1073
|
|
|
1011
|
-
Themes are Ruby classes nested under a Form/Display/Table override. They merge into Plutonium's defaults
|
|
1074
|
+
Themes are Ruby classes nested under a Form/Display/Table override. They merge into Plutonium's defaults, never replace wholesale: always `super.merge(...)`.
|
|
1012
1075
|
|
|
1013
1076
|
## Form theme
|
|
1014
1077
|
|
|
@@ -1036,7 +1099,7 @@ end
|
|
|
1036
1099
|
|
|
1037
1100
|
`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
1101
|
|
|
1039
|
-
`sectioned_base` replaces `base` when the definition declares a `form_layout
|
|
1102
|
+
`sectioned_base` replaces `base` when the definition declares a `form_layout`, because the sections are cards, so the form itself stops being one.
|
|
1040
1103
|
|
|
1041
1104
|
⚠️ **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
1105
|
|
|
@@ -1059,7 +1122,7 @@ end
|
|
|
1059
1122
|
|
|
1060
1123
|
### Display theme keys
|
|
1061
1124
|
|
|
1062
|
-
`fields_wrapper`, `fields_inner`, `sections_wrapper`, `section_grid`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`, `boolean`, `badge`, `currency`, `color`.
|
|
1125
|
+
`fields_wrapper`, `fields_inner`, `sections_wrapper`, `section_grid`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `rich_text`, `json`, `key_value`, `list` / `list_item`, `boolean`, `badge`, `currency`, `color`, `binary`, `relative_time`, `code`, `duration`, `file_size`, `progress`, `rating`. Table cells use `Plutonium::UI::Table::DisplayTheme`, which has its own (more compact) entry for each.
|
|
1063
1126
|
|
|
1064
1127
|
⚠️ **`fields_wrapper` is the CARD, `fields_inner` is the grid.** `fields_wrapper` is merged into a `Plutonium::UI::Block` (which supplies `pu-card` itself), so putting grid classes there styles the card, not the fields. Override `fields_inner` to change the unsectioned grid, and `section_grid` to change the grid inside a `display_layout` section.
|
|
1065
1128
|
|
|
@@ -1067,7 +1130,7 @@ end
|
|
|
1067
1130
|
|
|
1068
1131
|
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
1132
|
|
|
1070
|
-
`section_wrapper` (merged into the section's Block
|
|
1133
|
+
`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
1134
|
|
|
1072
1135
|
## Table theme
|
|
1073
1136
|
|
|
@@ -1096,7 +1159,7 @@ end
|
|
|
1096
1159
|
|
|
1097
1160
|
## Available context
|
|
1098
1161
|
|
|
1099
|
-
Inside any page / form / display / Phlex component, the same set of helpers is available
|
|
1162
|
+
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
1163
|
|
|
1101
1164
|
In Phlex components, Rails helpers are accessed via the `helpers` proxy:
|
|
1102
1165
|
|
|
@@ -1130,12 +1193,12 @@ end
|
|
|
1130
1193
|
|
|
1131
1194
|
## Gotchas
|
|
1132
1195
|
|
|
1133
|
-
- **Don't override `view_template` in pages** when a render hook fits
|
|
1134
|
-
- **Always register Stimulus controllers.**
|
|
1135
|
-
- **Use `plutoniumTailwindConfig.merge
|
|
1196
|
+
- **Don't override `view_template` in pages** when a render hook fits; you lose breadcrumbs / header / DynaFrame behavior.
|
|
1197
|
+
- **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.
|
|
1198
|
+
- **Use `plutoniumTailwindConfig.merge`**: plain object merge drops Plutonium's defaults.
|
|
1136
1199
|
- **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
|
|
1200
|
+
- **Tokens are CSS variables, not Tailwind keys**: `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
|
|
1201
|
+
- **`render_actions` is mandatory in custom `form_template`**: otherwise no submit button.
|
|
1139
1202
|
- **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
1203
|
- **`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
1204
|
- **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 +1209,7 @@ end
|
|
|
1146
1209
|
|
|
1147
1210
|
## Related skills
|
|
1148
1211
|
|
|
1149
|
-
- [[plutonium-resource]]
|
|
1150
|
-
- [[plutonium-behavior]]
|
|
1151
|
-
- [[plutonium-app]]
|
|
1152
|
-
- [[plutonium-tenancy]]
|
|
1212
|
+
- [[plutonium-resource]]: field/input/display config (`as:`, `condition:`, blocks); modal options for actions.
|
|
1213
|
+
- [[plutonium-behavior]]: controller presentation hooks (`present_parent?`), available helpers (`resource_record!`, `current_scoped_entity`).
|
|
1214
|
+
- [[plutonium-app]]: `pu:eject:layout`, `pu:eject:shell`, portal package overrides.
|
|
1215
|
+
- [[plutonium-tenancy]]: `permitted_associations` drives the show-page tablist.
|