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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +1 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +1 -1
  4. data/.claude/skills/plutonium-auth/SKILL.md +1 -1
  5. data/.claude/skills/plutonium-resource/SKILL.md +56 -3
  6. data/.claude/skills/plutonium-ui/SKILL.md +15 -2
  7. data/CHANGELOG.md +44 -0
  8. data/CONTRIBUTING.md +1 -1
  9. data/README.md +38 -16
  10. data/app/assets/plutonium.css +1 -1
  11. data/app/assets/plutonium.js +94 -26
  12. data/app/assets/plutonium.js.map +2 -2
  13. data/app/assets/plutonium.min.js +9 -9
  14. data/app/assets/plutonium.min.js.map +3 -3
  15. data/config/initializers/rabl.rb +16 -0
  16. data/docs/.vitepress/config.ts +1 -0
  17. data/docs/getting-started/installation.md +2 -2
  18. data/docs/getting-started/tutorial/02-first-resource.md +1 -1
  19. data/docs/getting-started/tutorial/03-authentication.md +3 -3
  20. data/docs/guides/adding-resources.md +1 -1
  21. data/docs/guides/authentication.md +1 -1
  22. data/docs/guides/creating-packages.md +1 -1
  23. data/docs/guides/multi-tenancy.md +1 -1
  24. data/docs/guides/nested-resources.md +1 -1
  25. data/docs/guides/user-invites.md +1 -1
  26. data/docs/guides/user-profile.md +1 -1
  27. data/docs/public/templates/lite.rb +10 -0
  28. data/docs/reference/app/generators.md +3 -3
  29. data/docs/reference/app/index.md +1 -1
  30. data/docs/reference/app/portals.md +1 -1
  31. data/docs/reference/auth/profile.md +1 -1
  32. data/docs/reference/generators/lite.md +65 -0
  33. data/docs/reference/resource/actions.md +55 -0
  34. data/docs/reference/resource/definition.md +18 -2
  35. data/docs/reference/resource/index.md +1 -1
  36. data/docs/reference/tenancy/invites.md +1 -1
  37. data/docs/reference/ui/assets.md +14 -0
  38. data/docs/reference/ui/displays.md +27 -1
  39. data/docs/reference/ui/forms.md +2 -1
  40. data/docs/reference/ui/layouts.md +33 -0
  41. data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md +857 -0
  42. data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md.tasks.json +45 -0
  43. data/docs/superpowers/specs/2026-06-04-sqlite-tune-maintenance-generators-design.md +238 -0
  44. data/gemfiles/rails_7.gemfile.lock +1 -1
  45. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  46. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  47. data/lib/generators/pu/core/typespec/typespec_generator.rb +1 -1
  48. data/lib/generators/pu/core/update/update_generator.rb +4 -1
  49. data/lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb +89 -0
  50. data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +45 -0
  51. data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +60 -0
  52. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +4 -51
  53. data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +1 -1
  54. data/lib/generators/pu/lite/tune/tune_generator.rb +105 -0
  55. data/lib/generators/pu/saas/welcome_generator.rb +1 -1
  56. data/lib/plutonium/action/base.rb +19 -2
  57. data/lib/plutonium/action/condition_context.rb +33 -0
  58. data/lib/plutonium/models/has_cents.rb +10 -0
  59. data/lib/plutonium/resource/controllers/interactive_actions.rb +19 -2
  60. data/lib/plutonium/routing/mapper_extensions.rb +5 -0
  61. data/lib/plutonium/ui/display/base.rb +9 -0
  62. data/lib/plutonium/ui/display/components/badge.rb +83 -0
  63. data/lib/plutonium/ui/display/components/boolean.rb +28 -6
  64. data/lib/plutonium/ui/display/components/currency.rb +50 -0
  65. data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
  66. data/lib/plutonium/ui/display/theme.rb +5 -0
  67. data/lib/plutonium/ui/form/base.rb +5 -0
  68. data/lib/plutonium/ui/form/components/toggle.rb +14 -0
  69. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +14 -25
  70. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +67 -0
  71. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -38
  72. data/lib/plutonium/ui/form/interaction.rb +7 -2
  73. data/lib/plutonium/ui/form/options/inferred_types.rb +2 -0
  74. data/lib/plutonium/ui/form/resource.rb +1 -0
  75. data/lib/plutonium/ui/form/theme.rb +12 -0
  76. data/lib/plutonium/ui/grid/card.rb +61 -23
  77. data/lib/plutonium/ui/layout/icon_rail.rb +29 -9
  78. data/lib/plutonium/ui/page/index.rb +1 -1
  79. data/lib/plutonium/ui/page/show.rb +1 -1
  80. data/lib/plutonium/ui/sidebar_menu.rb +29 -0
  81. data/lib/plutonium/ui/table/resource.rb +2 -2
  82. data/lib/plutonium/version.rb +1 -1
  83. data/package.json +1 -1
  84. data/plutonium.gemspec +5 -4
  85. data/src/css/components.css +126 -0
  86. data/src/js/controllers/dirty_form_guard_controller.js +55 -4
  87. data/src/js/controllers/nested_resource_form_fields_controller.js +35 -12
  88. data/src/js/controllers/resource_drop_down_controller.js +49 -14
  89. metadata +20 -6
@@ -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
@@ -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:migrate
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:migrate
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).
@@ -113,7 +113,7 @@ end
113
113
  ## Running the Migration
114
114
 
115
115
  ```bash
116
- rails db:migrate
116
+ rails db:prepare
117
117
  ```
118
118
 
119
119
  ## Creating a Portal
@@ -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:migrate
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:migrate
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:migrate
180
+ rails db:prepare
181
181
  ```
182
182
 
183
183
  Update the Post model to include the association:
@@ -29,7 +29,7 @@ Plutonium generates a basic migration. Before running it, edit `db/migrate/<time
29
29
  ### 3. Run the migration
30
30
 
31
31
  ```bash
32
- rails db:migrate
32
+ rails db:prepare
33
33
  ```
34
34
 
35
35
  ### 4. Connect to a portal
@@ -16,7 +16,7 @@ rails generate pu:rodauth:install
16
16
  rails generate pu:rodauth:account user
17
17
 
18
18
  # 3. Run migrations
19
- rails db:migrate
19
+ rails db:prepare
20
20
 
21
21
  # 4. Wire auth into a portal
22
22
  # (when you run `pu:pkg:portal admin --auth=user`, this happens automatically)
@@ -40,7 +40,7 @@ packages/blogging/
40
40
 
41
41
  ```bash
42
42
  rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
43
- rails db:migrate
43
+ rails db:prepare
44
44
  ```
45
45
 
46
46
  ### 4. Expose it via a portal
@@ -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:migrate
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:migrate
23
+ rails db:prepare
24
24
  ```
25
25
 
26
26
  ### 2. Connect both to the portal
@@ -49,7 +49,7 @@ rails g pu:invites:install \
49
49
  ### 2. Migrate
50
50
 
51
51
  ```bash
52
- rails db:migrate
52
+ rails db:prepare
53
53
  ```
54
54
 
55
55
  ### 3. Connect to your portal
@@ -55,7 +55,7 @@ By default the model is `{UserModel}Profile` (`UserProfile`, `StaffUserProfile`,
55
55
  ### 2. Migrate
56
56
 
57
57
  ```bash
58
- rails db:migrate
58
+ rails db:prepare
59
59
  ```
60
60
 
61
61
  ### 3. Connect to a portal
@@ -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:migrate
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:migrate
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:migrate
477
+ rails db:prepare
478
478
  ```
479
479
 
480
480
  ## Undoing generators
@@ -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:migrate
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:migrate` first.
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:migrate
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 &lt; 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:migrate
17
+ rails db:prepare
18
18
  rails g pu:res:conn Post --dest=admin_portal
19
19
  ```
20
20
 
@@ -56,7 +56,7 @@ rails g pu:invites:install \
56
56
  After install:
57
57
 
58
58
  ```bash
59
- rails db:migrate
59
+ rails db:prepare
60
60
  ```
61
61
 
62
62
  ## What gets created
@@ -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
 
@@ -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: