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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-resource/SKILL.md +21 -2
  3. data/.claude/skills/plutonium-ui/SKILL.md +15 -2
  4. data/CHANGELOG.md +31 -0
  5. data/app/assets/plutonium.css +1 -1
  6. data/app/assets/plutonium.js +94 -26
  7. data/app/assets/plutonium.js.map +2 -2
  8. data/app/assets/plutonium.min.js +9 -9
  9. data/app/assets/plutonium.min.js.map +3 -3
  10. data/config/initializers/rabl.rb +16 -0
  11. data/docs/.vitepress/config.ts +1 -0
  12. data/docs/public/templates/lite.rb +10 -0
  13. data/docs/reference/generators/lite.md +65 -0
  14. data/docs/reference/resource/definition.md +18 -2
  15. data/docs/reference/ui/assets.md +14 -0
  16. data/docs/reference/ui/displays.md +27 -1
  17. data/docs/reference/ui/forms.md +2 -1
  18. data/docs/reference/ui/layouts.md +33 -0
  19. data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md +857 -0
  20. data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md.tasks.json +45 -0
  21. data/docs/superpowers/specs/2026-06-04-sqlite-tune-maintenance-generators-design.md +238 -0
  22. data/gemfiles/rails_7.gemfile.lock +1 -1
  23. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  24. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  25. data/lib/generators/pu/core/update/update_generator.rb +4 -1
  26. data/lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb +89 -0
  27. data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +45 -0
  28. data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +60 -0
  29. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +4 -51
  30. data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +1 -1
  31. data/lib/generators/pu/lite/tune/tune_generator.rb +105 -0
  32. data/lib/plutonium/models/has_cents.rb +10 -0
  33. data/lib/plutonium/resource/controllers/interactive_actions.rb +19 -2
  34. data/lib/plutonium/routing/mapper_extensions.rb +5 -0
  35. data/lib/plutonium/ui/display/base.rb +9 -0
  36. data/lib/plutonium/ui/display/components/badge.rb +83 -0
  37. data/lib/plutonium/ui/display/components/boolean.rb +28 -6
  38. data/lib/plutonium/ui/display/components/currency.rb +50 -0
  39. data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
  40. data/lib/plutonium/ui/display/theme.rb +5 -0
  41. data/lib/plutonium/ui/form/base.rb +5 -0
  42. data/lib/plutonium/ui/form/components/toggle.rb +14 -0
  43. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +14 -25
  44. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +67 -0
  45. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -38
  46. data/lib/plutonium/ui/form/interaction.rb +7 -2
  47. data/lib/plutonium/ui/form/options/inferred_types.rb +2 -0
  48. data/lib/plutonium/ui/form/resource.rb +1 -0
  49. data/lib/plutonium/ui/form/theme.rb +12 -0
  50. data/lib/plutonium/ui/grid/card.rb +58 -21
  51. data/lib/plutonium/ui/layout/icon_rail.rb +29 -9
  52. data/lib/plutonium/ui/sidebar_menu.rb +29 -0
  53. data/lib/plutonium/version.rb +1 -1
  54. data/package.json +1 -1
  55. data/plutonium.gemspec +5 -4
  56. data/src/css/components.css +126 -0
  57. data/src/js/controllers/dirty_form_guard_controller.js +55 -4
  58. data/src/js/controllers/nested_resource_form_fields_controller.js +35 -12
  59. data/src/js/controllers/resource_drop_down_controller.js +49 -14
  60. metadata +19 -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
  {
@@ -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 &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.
@@ -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
 
@@ -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: