plutonium 0.55.0 → 0.56.1
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 +1 -1
- data/.claude/skills/plutonium-app/SKILL.md +1 -1
- data/.claude/skills/plutonium-auth/SKILL.md +1 -1
- data/.claude/skills/plutonium-resource/SKILL.md +56 -3
- data/.claude/skills/plutonium-ui/SKILL.md +15 -2
- data/CHANGELOG.md +44 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +38 -16
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +94 -26
- data/app/assets/plutonium.js.map +2 -2
- data/app/assets/plutonium.min.js +9 -9
- data/app/assets/plutonium.min.js.map +3 -3
- data/config/initializers/rabl.rb +16 -0
- data/docs/.vitepress/config.ts +1 -0
- data/docs/getting-started/installation.md +2 -2
- data/docs/getting-started/tutorial/02-first-resource.md +1 -1
- data/docs/getting-started/tutorial/03-authentication.md +3 -3
- data/docs/guides/adding-resources.md +1 -1
- data/docs/guides/authentication.md +1 -1
- data/docs/guides/creating-packages.md +1 -1
- data/docs/guides/multi-tenancy.md +1 -1
- data/docs/guides/nested-resources.md +1 -1
- data/docs/guides/user-invites.md +1 -1
- data/docs/guides/user-profile.md +1 -1
- data/docs/public/templates/lite.rb +10 -0
- data/docs/reference/app/generators.md +3 -3
- data/docs/reference/app/index.md +1 -1
- data/docs/reference/app/portals.md +1 -1
- data/docs/reference/auth/profile.md +1 -1
- data/docs/reference/generators/lite.md +65 -0
- data/docs/reference/resource/actions.md +55 -0
- data/docs/reference/resource/definition.md +18 -2
- data/docs/reference/resource/index.md +1 -1
- data/docs/reference/tenancy/invites.md +1 -1
- data/docs/reference/ui/assets.md +14 -0
- data/docs/reference/ui/displays.md +27 -1
- data/docs/reference/ui/forms.md +2 -1
- data/docs/reference/ui/layouts.md +33 -0
- data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md +857 -0
- data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md.tasks.json +45 -0
- data/docs/superpowers/specs/2026-06-04-sqlite-tune-maintenance-generators-design.md +238 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/typespec/typespec_generator.rb +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +4 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb +89 -0
- data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +45 -0
- data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +60 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +4 -51
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +1 -1
- data/lib/generators/pu/lite/tune/tune_generator.rb +105 -0
- data/lib/generators/pu/saas/welcome_generator.rb +1 -1
- data/lib/plutonium/action/base.rb +19 -2
- data/lib/plutonium/action/condition_context.rb +33 -0
- data/lib/plutonium/models/has_cents.rb +10 -0
- data/lib/plutonium/resource/controllers/interactive_actions.rb +19 -2
- data/lib/plutonium/routing/mapper_extensions.rb +5 -0
- data/lib/plutonium/ui/display/base.rb +9 -0
- data/lib/plutonium/ui/display/components/badge.rb +83 -0
- data/lib/plutonium/ui/display/components/boolean.rb +28 -6
- data/lib/plutonium/ui/display/components/currency.rb +50 -0
- data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
- data/lib/plutonium/ui/display/theme.rb +5 -0
- data/lib/plutonium/ui/form/base.rb +5 -0
- data/lib/plutonium/ui/form/components/toggle.rb +14 -0
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +14 -25
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +67 -0
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -38
- data/lib/plutonium/ui/form/interaction.rb +7 -2
- data/lib/plutonium/ui/form/options/inferred_types.rb +2 -0
- data/lib/plutonium/ui/form/resource.rb +1 -0
- data/lib/plutonium/ui/form/theme.rb +12 -0
- data/lib/plutonium/ui/grid/card.rb +61 -23
- data/lib/plutonium/ui/layout/icon_rail.rb +29 -9
- data/lib/plutonium/ui/page/index.rb +1 -1
- data/lib/plutonium/ui/page/show.rb +1 -1
- data/lib/plutonium/ui/sidebar_menu.rb +29 -0
- data/lib/plutonium/ui/table/resource.rb +2 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +126 -0
- data/src/js/controllers/dirty_form_guard_controller.js +55 -4
- data/src/js/controllers/nested_resource_form_fields_controller.js +35 -12
- data/src/js/controllers/resource_drop_down_controller.js +49 -14
- metadata +20 -6
data/config/initializers/rabl.rb
CHANGED
|
@@ -2,9 +2,25 @@ require "rabl"
|
|
|
2
2
|
|
|
3
3
|
# https://github.com/nesquena/rabl#configuration
|
|
4
4
|
|
|
5
|
+
# RABL encodes its result hash with stdlib JSON by default, which serializes
|
|
6
|
+
# Time/Date/BigDecimal via #to_s — e.g. a datetime becomes
|
|
7
|
+
# "2026-06-04 13:46:18 UTC" instead of the ISO 8601 "2026-06-04T13:46:18.000Z"
|
|
8
|
+
# that JSON clients expect. Routing values through ActiveSupport's #as_json
|
|
9
|
+
# first yields JSON-optimized values (ISO 8601 datetimes, "YYYY-MM-DD" dates,
|
|
10
|
+
# numeric-safe decimals, true/false booleans), then stdlib JSON.generate
|
|
11
|
+
# encodes the now-primitive hash without escaping HTML entities in strings.
|
|
12
|
+
module Plutonium
|
|
13
|
+
module RablJsonEngine
|
|
14
|
+
def self.dump(object)
|
|
15
|
+
JSON.generate(object.as_json)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
5
20
|
Rabl.configure do |config|
|
|
6
21
|
config.cache_sources = !Rails.env.development? # Defaults to false
|
|
7
22
|
config.raise_on_missing_attribute = !Rails.env.production? # Defaults to false
|
|
23
|
+
config.json_engine = Plutonium::RablJsonEngine
|
|
8
24
|
|
|
9
25
|
# config.cache_all_output = false
|
|
10
26
|
# config.cache_engine = Rabl::CacheEngine.new # Defaults to Rails cache
|
data/docs/.vitepress/config.ts
CHANGED
|
@@ -119,6 +119,7 @@ export default defineConfig(withMermaid({
|
|
|
119
119
|
{ text: "Packages", link: "/reference/app/packages" },
|
|
120
120
|
{ text: "Portals", link: "/reference/app/portals" },
|
|
121
121
|
{ text: "Generators", link: "/reference/app/generators" },
|
|
122
|
+
{ text: "Lite (SQLite) Generators", link: "/reference/generators/lite" },
|
|
122
123
|
]
|
|
123
124
|
},
|
|
124
125
|
{
|
|
@@ -15,7 +15,7 @@ After the template completes:
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
cd myapp
|
|
18
|
-
rails db:
|
|
18
|
+
rails db:prepare
|
|
19
19
|
bin/dev
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -51,7 +51,7 @@ rails generate pu:core:install
|
|
|
51
51
|
```bash
|
|
52
52
|
rails generate pu:rodauth:install
|
|
53
53
|
rails generate pu:rodauth:account user
|
|
54
|
-
rails db:
|
|
54
|
+
rails db:prepare
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
For account options and customization, see [Reference › Auth](/reference/auth/) and [Guides › Authentication](/guides/authentication).
|
|
@@ -28,7 +28,7 @@ Plutonium supports multiple account types. For admins, use the dedicated `pu:rod
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
rails generate pu:rodauth:admin admin
|
|
31
|
-
rails db:
|
|
31
|
+
rails db:prepare
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
For self-service user accounts, the corresponding command is `rails generate pu:rodauth:account user`.
|
|
@@ -151,7 +151,7 @@ Our blog needs users (authors) who can create posts. Let's create a User account
|
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
153
|
rails generate pu:rodauth:account user
|
|
154
|
-
rails db:
|
|
154
|
+
rails db:prepare
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
This creates a `User` model similar to `Admin`, but with public registration enabled.
|
|
@@ -177,7 +177,7 @@ end
|
|
|
177
177
|
Run the migration:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
rails db:
|
|
180
|
+
rails db:prepare
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
Update the Post model to include the association:
|
|
@@ -42,7 +42,7 @@ rails g pu:res:scaffold Organization name:string:uniq slug:string:uniq --dest=ma
|
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
rails g pu:res:scaffold Post organization:belongs_to title:string content:text --dest=main_app
|
|
45
|
-
rails db:
|
|
45
|
+
rails db:prepare
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
### 3. Scope the portal to the entity
|
|
@@ -20,7 +20,7 @@ All of this happens with no manual route wiring — Plutonium generates it from
|
|
|
20
20
|
```bash
|
|
21
21
|
rails g pu:res:scaffold Company name:string --dest=main_app
|
|
22
22
|
rails g pu:res:scaffold Property company:belongs_to name:string --dest=main_app
|
|
23
|
-
rails db:
|
|
23
|
+
rails db:prepare
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### 2. Connect both to the portal
|
data/docs/guides/user-invites.md
CHANGED
data/docs/guides/user-profile.md
CHANGED
|
@@ -4,6 +4,10 @@ after_bundle do
|
|
|
4
4
|
git add: "."
|
|
5
5
|
git commit: %( -m 'setup sqlite') if `git status --porcelain`.present?
|
|
6
6
|
|
|
7
|
+
generate "pu:lite:tune"
|
|
8
|
+
git add: "."
|
|
9
|
+
git commit: %( -m 'tune sqlite pragmas') if `git status --porcelain`.present?
|
|
10
|
+
|
|
7
11
|
unless ENV["SKIP_SOLID_QUEUE"]
|
|
8
12
|
generate "pu:lite:solid_queue"
|
|
9
13
|
git add: "."
|
|
@@ -39,4 +43,10 @@ after_bundle do
|
|
|
39
43
|
git add: "."
|
|
40
44
|
git commit: %( -m 'add rails_pulse') if `git status --porcelain`.present?
|
|
41
45
|
end
|
|
46
|
+
|
|
47
|
+
unless ENV["SKIP_SQLITE_MAINTENANCE"]
|
|
48
|
+
generate "pu:lite:maintenance"
|
|
49
|
+
git add: "."
|
|
50
|
+
git commit: %( -m 'add sqlite maintenance job') if `git status --porcelain`.present?
|
|
51
|
+
end
|
|
42
52
|
end
|
|
@@ -455,7 +455,7 @@ rails generate pu:pkg:portal admin --auth=admin
|
|
|
455
455
|
rails generate pu:res:conn Post Comment --dest=admin_portal
|
|
456
456
|
|
|
457
457
|
# 6. Migrate
|
|
458
|
-
rails db:
|
|
458
|
+
rails db:prepare
|
|
459
459
|
|
|
460
460
|
# 7. Create the first admin
|
|
461
461
|
rails rodauth_admin:create[admin@example.com,password123]
|
|
@@ -465,7 +465,7 @@ rails rodauth_admin:create[admin@example.com,password123]
|
|
|
465
465
|
|
|
466
466
|
```bash
|
|
467
467
|
rails g pu:res:scaffold Product name:string price_cents:integer --dest=main_app
|
|
468
|
-
rails db:
|
|
468
|
+
rails db:prepare
|
|
469
469
|
rails g pu:res:conn Product --dest=admin_portal
|
|
470
470
|
```
|
|
471
471
|
|
|
@@ -474,7 +474,7 @@ rails g pu:res:conn Product --dest=admin_portal
|
|
|
474
474
|
```bash
|
|
475
475
|
rails g pu:pkg:portal customer --auth=user --scope=Organization
|
|
476
476
|
rails g pu:res:conn Order --dest=customer_portal
|
|
477
|
-
rails db:
|
|
477
|
+
rails db:prepare
|
|
478
478
|
```
|
|
479
479
|
|
|
480
480
|
## Undoing generators
|
data/docs/reference/app/index.md
CHANGED
|
@@ -50,7 +50,7 @@ rails generate pu:pkg:portal admin --auth=user
|
|
|
50
50
|
|
|
51
51
|
# 4. First resource
|
|
52
52
|
rails generate pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
53
|
-
rails db:
|
|
53
|
+
rails db:prepare
|
|
54
54
|
|
|
55
55
|
# 5. Connect resource to portal
|
|
56
56
|
rails generate pu:res:conn Post --dest=admin_portal
|
|
@@ -195,7 +195,7 @@ rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
::: tip Run after migrations
|
|
198
|
-
The generator reads model columns to seed the policy's `permitted_attributes_for_*`. Run `rails db:
|
|
198
|
+
The generator reads model columns to seed the policy's `permitted_attributes_for_*`. Run `rails db:prepare` first.
|
|
199
199
|
:::
|
|
200
200
|
|
|
201
201
|
### What gets generated
|
|
@@ -24,7 +24,7 @@ Meta-generator: runs `pu:profile:install` + `pu:profile:conn` in one shot.
|
|
|
24
24
|
rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
|
|
25
25
|
--dest=customer
|
|
26
26
|
|
|
27
|
-
rails db:
|
|
27
|
+
rails db:prepare
|
|
28
28
|
|
|
29
29
|
rails generate pu:profile:conn --dest=customer_portal
|
|
30
30
|
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Lite (SQLite) Generators
|
|
2
|
+
|
|
3
|
+
The `pu:lite:*` generators configure a SQLite-first production stack. This page
|
|
4
|
+
covers the two tuning/maintenance generators; the solid_queue / solid_cache /
|
|
5
|
+
solid_cable / solid_errors / litestream / rails_pulse generators are run the
|
|
6
|
+
same way (`rails g pu:lite:<name>`).
|
|
7
|
+
|
|
8
|
+
## `pu:lite:tune`
|
|
9
|
+
|
|
10
|
+
Adds tuned performance pragmas to the `default: &default` block of
|
|
11
|
+
`config/database.yml`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
rails g pu:lite:tune
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
It writes a `pragmas:` mapping:
|
|
18
|
+
|
|
19
|
+
- `cache_size: -64000` — 64 MB page cache (the ~2 MB default is too small).
|
|
20
|
+
- `temp_store: 2` — MEMORY; sorts and temp indexes stay off disk.
|
|
21
|
+
- `mmap_size: 536870912` — 512 MB memory-mapped I/O.
|
|
22
|
+
- `wal_autocheckpoint: 10000` — checkpoint roughly every 40 MB of WAL.
|
|
23
|
+
|
|
24
|
+
On Rails < 8.1 it also writes the baseline pragmas (`journal_mode: WAL`,
|
|
25
|
+
`synchronous: NORMAL`, `foreign_keys: true`, `journal_size_limit`) that Rails 8.1+
|
|
26
|
+
already sets by default.
|
|
27
|
+
|
|
28
|
+
**Why no `busy_timeout`?** Rails routes the `timeout:` key to the sqlite3 gem's
|
|
29
|
+
constant-poll busy handler (`busy_handler_timeout`), which has better tail-latency
|
|
30
|
+
than SQLite's internal exponential backoff. Setting a busy-timeout pragma would
|
|
31
|
+
replace the better handler with the worse one, so this generator never emits it.
|
|
32
|
+
|
|
33
|
+
The generator is idempotent — re-running it detects the existing pragmas and skips.
|
|
34
|
+
It only ever touches the `default:` block, so a `pragmas:` mapping nested under
|
|
35
|
+
another environment is left untouched.
|
|
36
|
+
|
|
37
|
+
## `pu:lite:maintenance`
|
|
38
|
+
|
|
39
|
+
Installs `app/jobs/sqlite_maintenance_job.rb` and (when `solid_queue` is present)
|
|
40
|
+
schedules it in `config/recurring.yml`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rails g pu:lite:maintenance
|
|
44
|
+
# custom schedule:
|
|
45
|
+
rails g pu:lite:maintenance --schedule="every day at 4am"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The job runs `PRAGMA optimize` on every configured SQLite database and `VACUUM`
|
|
49
|
+
only on databases without live 24/7 writers (`primary`, `errors`, `rails_pulse`
|
|
50
|
+
by default — edit `VACUUM_DBS` in the generated job to suit your app).
|
|
51
|
+
|
|
52
|
+
**Why VACUUM only some databases?** SolidQueue, Solid Cache and Solid Cable write
|
|
53
|
+
to their databases constantly. `VACUUM` takes a global *exclusive* lock for its
|
|
54
|
+
whole duration, which stalls and errors those processes (e.g. SolidQueue process
|
|
55
|
+
deregistration failing with "database is locked"). They also barely benefit: in
|
|
56
|
+
WAL mode, freed pages land on the freelist and are reused, so a churning database
|
|
57
|
+
stays at a steady-state size without nightly reclamation. `PRAGMA optimize`, which
|
|
58
|
+
only takes a brief shared lock, still runs everywhere.
|
|
59
|
+
|
|
60
|
+
Databases listed in the job that don't exist in `config/database.yml` are skipped
|
|
61
|
+
at runtime, so the same job is safe regardless of which `pu:lite:*` generators you
|
|
62
|
+
have run.
|
|
63
|
+
|
|
64
|
+
If `solid_queue` is not installed, the job file is still created but not scheduled —
|
|
65
|
+
add a `sqlite_maintenance` entry to whatever scheduler you use.
|
|
@@ -61,6 +61,9 @@ action :name,
|
|
|
61
61
|
collection_record_action: true,
|
|
62
62
|
bulk_action: true,
|
|
63
63
|
|
|
64
|
+
# Conditional visibility — display-only proc, NOT authorization (see below)
|
|
65
|
+
condition: -> { params[:beta] == "1" },
|
|
66
|
+
|
|
64
67
|
# Grouping
|
|
65
68
|
category: :primary, # :primary, :secondary, :danger
|
|
66
69
|
position: 50, # display order (lower = first)
|
|
@@ -84,6 +87,58 @@ def customize_actions
|
|
|
84
87
|
end
|
|
85
88
|
```
|
|
86
89
|
|
|
90
|
+
## Conditional visibility — `condition:`
|
|
91
|
+
|
|
92
|
+
Like the `condition:` proc on [inputs/displays/columns](/reference/resource/definition), an action can be **defined but only rendered when a runtime proc is truthy**. It's purely a toggle on whether the **button is shown** — the action (and its route) stays fully live either way.
|
|
93
|
+
|
|
94
|
+
The headline use case: **expose an action's endpoint without surfacing it in the UI** — e.g. one you call from the API, a webhook, or another service. Hide the button with an always-falsy condition; the route still works:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Defined and callable (API / programmatic), but no button anywhere in the UI:
|
|
98
|
+
action :sync_inventory, interaction: SyncInventoryInteraction, condition: -> { false }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It also works as a dynamic toggle driven by the **record** or the **view/request** context:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# object → the row/shown record (record & collection-record actions):
|
|
105
|
+
action :reopen, interaction: ReopenInteraction, condition: -> { object.closed? }
|
|
106
|
+
# view/request state — feature flag, preview/beta mode:
|
|
107
|
+
action :preview, interaction: PreviewInteraction, condition: -> { params[:beta] == "1" }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Inside the proc, `object`/`record` is the contextual record, and every other call delegates to the **view context**:
|
|
111
|
+
|
|
112
|
+
| Available | Notes |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `object` / `record` | The row/shown record for **record** and **collection-record** actions; **`nil`** for resource and bulk actions (no single record). Guard with `object&.…` if a condition is shared across action kinds. |
|
|
115
|
+
| `params`, `request` | Current request. |
|
|
116
|
+
| `current_user`, `current_parent` | The signed-in user and (nested) parent. |
|
|
117
|
+
| `resource_record!` | The shown record on the show page; raises on index/table — prefer `object`. |
|
|
118
|
+
| `allowed_to?`, `policy_for`, other helpers | The usual view helpers. |
|
|
119
|
+
|
|
120
|
+
`object` is evaluated **per row** in tables and grids, so per-record show/hide works there too.
|
|
121
|
+
|
|
122
|
+
::: danger `condition:` is NOT authorization — it only hides the button
|
|
123
|
+
A hidden action still has a **live route**: anyone who knows the URL can still trigger it. `condition:` decides whether the *button renders*, never whether the *request is allowed*.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# 🚫 WRONG — this does NOT stop non-admins. The route is live; they can POST to it.
|
|
127
|
+
action :wipe, interaction: WipeInteraction, condition: -> { current_user.admin? }
|
|
128
|
+
|
|
129
|
+
# ✅ RIGHT — authorization belongs in the policy. The action only runs if this returns true.
|
|
130
|
+
class WidgetPolicy < ResourcePolicy
|
|
131
|
+
def wipe? = current_user.admin?
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Rule of thumb:** "who may run this" → **policy** (`def action_name?`). "is this UI relevant right now" → `condition:`. Authorization is enforced regardless of `condition:`; the two compose — an action appears only when the policy permits **and** the condition is truthy.
|
|
136
|
+
:::
|
|
137
|
+
|
|
138
|
+
::: tip Per-record display vs. per-record authorization
|
|
139
|
+
`condition: -> { object.draft? }` is fine for **showing/hiding** a per-record button. But if the rule is about **who may run it** ("only while draft *and* nobody else has it locked"), put it in the policy — `def publish? = record.draft?` is also evaluated per record (per row), and unlike `condition:` it actually gates execution.
|
|
140
|
+
:::
|
|
141
|
+
|
|
87
142
|
## Simple actions (navigation)
|
|
88
143
|
|
|
89
144
|
Link to an existing route. The target route MUST exist.
|
|
@@ -102,7 +102,7 @@ end
|
|
|
102
102
|
| Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
|
|
103
103
|
| Rich text | `:markdown` (EasyMDE editor) |
|
|
104
104
|
| Numeric | `:number`, `:integer`, `:decimal`, `:range` |
|
|
105
|
-
| Boolean | `:boolean` |
|
|
105
|
+
| Boolean | `:toggle` / `:switch` (switch — **default** for boolean columns), `:boolean` (plain checkbox) |
|
|
106
106
|
| Date/Time | `:date`, `:time`, `:datetime` |
|
|
107
107
|
| Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
|
|
108
108
|
| Files | `:file`, `:uppy`, `:attachment` |
|
|
@@ -111,7 +111,23 @@ end
|
|
|
111
111
|
|
|
112
112
|
### Display types (show / index)
|
|
113
113
|
|
|
114
|
-
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`
|
|
114
|
+
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`
|
|
115
|
+
|
|
116
|
+
#### Auto-inferred display formatting
|
|
117
|
+
|
|
118
|
+
These render automatically — declare an `as:` only to override or pass options:
|
|
119
|
+
|
|
120
|
+
| Column | Renders as | Notes |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `boolean` | Yes/No pill (`:boolean`) | green "Yes" / neutral "No"; override with `true_label:` / `false_label:` |
|
|
123
|
+
| `enum` | status badge (`:badge`) | known statuses auto-colored; unknown values get a stable decorative color; override per-value with `colors:` |
|
|
124
|
+
| `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals, **no symbol** unless you pass `unit:` (a literal `"£"` or a Symbol read off the record) |
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
display :status, as: :badge, colors: {archived: :neutral, vip: :accent}
|
|
128
|
+
display :price, as: :currency, unit: "£"
|
|
129
|
+
display :active, as: :boolean, true_label: "Live", false_label: "Off"
|
|
130
|
+
```
|
|
115
131
|
|
|
116
132
|
## Field options
|
|
117
133
|
|
|
@@ -14,7 +14,7 @@ A **resource** is the unit Plutonium gives you full CRUD for — list, show, cre
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
rails g pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
17
|
-
rails db:
|
|
17
|
+
rails db:prepare
|
|
18
18
|
rails g pu:res:conn Post --dest=admin_portal
|
|
19
19
|
```
|
|
20
20
|
|
data/docs/reference/ui/assets.md
CHANGED
|
@@ -290,8 +290,22 @@ Ready-to-use styled components in `src/css/components.css`. **Prefer these over
|
|
|
290
290
|
.pu-label / -required
|
|
291
291
|
.pu-hint / .pu-error
|
|
292
292
|
.pu-checkbox
|
|
293
|
+
.pu-toggle (switch-styled checkbox)
|
|
293
294
|
```
|
|
294
295
|
|
|
296
|
+
### Badges (status pills)
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
.pu-badge (base)
|
|
300
|
+
.pu-badge-neutral / -primary / -secondary / -success / -danger / -warning / -info / -accent
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```erb
|
|
304
|
+
<span class="pu-badge pu-badge-success">Active</span>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Rendered automatically by the `:badge` display (enums) and `:boolean` display (Yes/No pills). See [Displays](./displays#built-in-display-components).
|
|
308
|
+
|
|
295
309
|
### Cards, panels, tables, toolbars, empty states
|
|
296
310
|
|
|
297
311
|
```
|
|
@@ -67,6 +67,30 @@ end
|
|
|
67
67
|
|
|
68
68
|
See [Resource › Definition › Custom rendering](/reference/resource/definition#custom-rendering) for the full per-field rendering surface.
|
|
69
69
|
|
|
70
|
+
## Built-in display components
|
|
71
|
+
|
|
72
|
+
Some types render with richer components automatically — you only declare an `as:` to override or pass options.
|
|
73
|
+
|
|
74
|
+
| `as:` | Renders | Auto-inferred for | Options |
|
|
75
|
+
|-------|---------|-------------------|---------|
|
|
76
|
+
| `:boolean` | green "Yes" / neutral "No" pill | `boolean` columns | `true_label:`, `false_label:` |
|
|
77
|
+
| `:badge` | colored status pill | `enum` columns | `colors:` (per-value override) |
|
|
78
|
+
| `:currency` | delimited, 2-decimal money | `has_cents` decimal accessors | `unit:`, `options:` |
|
|
79
|
+
| `:color` | swatch + value | — | — |
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class OrderDefinition < ResourceDefinition
|
|
83
|
+
display :status, as: :badge, colors: {refunded: :neutral, vip: :accent}
|
|
84
|
+
display :total, as: :currency, unit: "£"
|
|
85
|
+
display :total, as: :currency, unit: :currency_symbol # Symbol → read off each record
|
|
86
|
+
display :shipped, as: :boolean, true_label: "Sent", false_label: "Pending"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Badge colors.** Known statuses (`active`, `pending`, `failed`, …) are auto-colored by meaning. Unknown values get a stable decorative color (same value → same color). Override per-value with `colors:`; valid variants: `:neutral`, `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:accent`.
|
|
91
|
+
|
|
92
|
+
**Currency.** No symbol is shown unless you pass `unit:` — a literal string (`"£"`) or a Symbol read off the record for per-row currencies. `has_cents` decimal accessors infer `:currency` automatically (still symbol-less until you set `unit:`).
|
|
93
|
+
|
|
70
94
|
## Theming
|
|
71
95
|
|
|
72
96
|
Override the theme via a nested `Theme` class:
|
|
@@ -90,7 +114,9 @@ end
|
|
|
90
114
|
|
|
91
115
|
### Theme keys
|
|
92
116
|
|
|
93
|
-
`fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`.
|
|
117
|
+
`fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`, `boolean`, `badge`, `currency`, `color`.
|
|
118
|
+
|
|
119
|
+
(`boolean` and `badge` apply their pill variant in the component, so their theme value stays empty — restyle the pills via the `.pu-badge*` classes instead.)
|
|
94
120
|
|
|
95
121
|
## Metadata panel
|
|
96
122
|
|
data/docs/reference/ui/forms.md
CHANGED
|
@@ -122,6 +122,7 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
122
122
|
| `input_tag` | text (auto-detected type) |
|
|
123
123
|
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
|
|
124
124
|
| `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
|
|
125
|
+
| `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`) — the **default** for boolean columns; same behavior as a checkbox. Use `checkbox_tag` (`as: :boolean`) for a plain checkbox. |
|
|
125
126
|
|
|
126
127
|
### Plutonium-enhanced tags
|
|
127
128
|
|
|
@@ -239,7 +240,7 @@ Don't replace the theme wholesale — Plutonium's defaults handle invalid states
|
|
|
239
240
|
|
|
240
241
|
### Theme keys
|
|
241
242
|
|
|
242
|
-
`base`, `fields_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`.
|
|
243
|
+
`base`, `fields_wrapper`, `actions_wrapper`, `wrapper`, `inner_wrapper`, `label`, `invalid_label`, `valid_label`, `neutral_label`, `input`, `invalid_input`, `valid_input`, `neutral_input`, `hint`, `error`, `button`, `checkbox`, `toggle`, `select`.
|
|
243
244
|
|
|
244
245
|
See [Assets › Phlexi component themes](./assets#phlexi-component-themes) for the underlying theme system.
|
|
245
246
|
|
|
@@ -26,6 +26,39 @@ rails generate pu:eject:layout
|
|
|
26
26
|
|
|
27
27
|
`pu:eject:layout` copies `layouts/resource.html.erb` for layout-level edits.
|
|
28
28
|
|
|
29
|
+
## Navigation menu
|
|
30
|
+
|
|
31
|
+
The sidebar/icon-rail navigation is built with `Phlexi::Menu::Builder` in the ejected `_resource_sidebar.html.erb`. Each `item` takes a `label`, plus `url:`, `icon:`, and optional `leading_badge:` / `trailing_badge:`:
|
|
32
|
+
|
|
33
|
+
```erb
|
|
34
|
+
<%= render Plutonium::UI::Layout::IconRail.new(
|
|
35
|
+
menu: Phlexi::Menu::Builder.new do |m|
|
|
36
|
+
m.item "Dashboard", url: root_path, icon: Phlex::TablerIcons::Home
|
|
37
|
+
|
|
38
|
+
m.item "Resources", icon: Phlex::TablerIcons::GridDots do |n|
|
|
39
|
+
registered_resources.each do |resource|
|
|
40
|
+
n.item resource_label(resource), url: resource_url_for(resource, parent: nil)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
) %>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Per-item link attributes
|
|
48
|
+
|
|
49
|
+
Any extra options you pass to `item` are spread straight onto the rendered `<a>` — so a menu entry can opt into `target`, `rel`, `data-*`, `aria-*`, etc. Useful for items that open in their own tab or drive a Stimulus/Turbo behavior:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
m.item "Inbox",
|
|
53
|
+
url: inbox_path,
|
|
54
|
+
icon: Phlex::TablerIcons::Mail,
|
|
55
|
+
target: "_blank",
|
|
56
|
+
rel: "noopener",
|
|
57
|
+
data: {turbo_frame: "_top"}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This works across both shells — the `:modern` icon-rail (leaf items, parent flyout triggers, and flyout children) and the `:classic` sidebar. Framework attributes always win on conflict: a custom `class:` is **merged** with the component's base classes, and on a parent trigger your `data:` / `aria:` merge with the flyout's own wiring (so you can't accidentally break the toggle). The `:active` key is reserved by Phlexi for [custom active-state logic](https://github.com/radioactive-labs/phlexi-menu) and is never emitted as an attribute.
|
|
61
|
+
|
|
29
62
|
## Custom layout class
|
|
30
63
|
|
|
31
64
|
For full Phlex-level control over the layout:
|