plutonium 0.55.0 → 0.56.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-resource/SKILL.md +21 -2
- data/.claude/skills/plutonium-ui/SKILL.md +15 -2
- data/CHANGELOG.md +31 -0
- 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/public/templates/lite.rb +10 -0
- data/docs/reference/generators/lite.md +65 -0
- data/docs/reference/resource/definition.md +18 -2
- 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/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/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 +58 -21
- data/lib/plutonium/ui/layout/icon_rail.rb +29 -9
- data/lib/plutonium/ui/sidebar_menu.rb +29 -0
- 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 +19 -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
|
{
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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
|
|
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:
|