plutonium 0.54.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-behavior/SKILL.md +22 -0
  3. data/.claude/skills/plutonium-resource/SKILL.md +76 -2
  4. data/.claude/skills/plutonium-ui/SKILL.md +17 -3
  5. data/CHANGELOG.md +45 -0
  6. data/app/assets/plutonium.css +1 -1
  7. data/app/assets/plutonium.js +112 -26
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +31 -31
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/config/initializers/rabl.rb +16 -0
  12. data/docs/.vitepress/config.ts +1 -0
  13. data/docs/public/images/reference/structured-inputs-removed.png +0 -0
  14. data/docs/public/images/reference/structured-inputs.png +0 -0
  15. data/docs/public/templates/lite.rb +10 -0
  16. data/docs/reference/generators/lite.md +65 -0
  17. data/docs/reference/resource/definition.md +128 -2
  18. data/docs/reference/ui/assets.md +14 -0
  19. data/docs/reference/ui/displays.md +27 -1
  20. data/docs/reference/ui/forms.md +2 -1
  21. data/docs/reference/ui/layouts.md +33 -0
  22. data/docs/superpowers/plans/2026-06-02-structured-inputs.md +1061 -0
  23. data/docs/superpowers/plans/2026-06-02-structured-inputs.md.tasks.json +60 -0
  24. data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md +857 -0
  25. data/docs/superpowers/plans/2026-06-04-sqlite-tune-maintenance-generators.md.tasks.json +45 -0
  26. data/docs/superpowers/specs/2026-06-01-structured-inputs-design.md +191 -0
  27. data/docs/superpowers/specs/2026-06-04-sqlite-tune-maintenance-generators-design.md +238 -0
  28. data/gemfiles/rails_7.gemfile.lock +1 -1
  29. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  31. data/lib/generators/pu/core/update/update_generator.rb +4 -1
  32. data/lib/generators/pu/lib/plutonium_generators/concerns/configures_recurring.rb +89 -0
  33. data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +45 -0
  34. data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +60 -0
  35. data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +4 -51
  36. data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +1 -1
  37. data/lib/generators/pu/lite/tune/tune_generator.rb +105 -0
  38. data/lib/plutonium/definition/base.rb +1 -0
  39. data/lib/plutonium/definition/structured_inputs.rb +67 -0
  40. data/lib/plutonium/interaction/README.md +24 -78
  41. data/lib/plutonium/interaction/base.rb +10 -2
  42. data/lib/plutonium/models/has_cents.rb +10 -0
  43. data/lib/plutonium/resource/controller.rb +6 -1
  44. data/lib/plutonium/resource/controllers/interactive_actions.rb +27 -6
  45. data/lib/plutonium/routing/mapper_extensions.rb +5 -0
  46. data/lib/plutonium/structured_inputs/param_cleaner.rb +36 -0
  47. data/lib/plutonium/structured_inputs/params_concern.rb +36 -0
  48. data/lib/plutonium/ui/display/base.rb +9 -0
  49. data/lib/plutonium/ui/display/components/badge.rb +83 -0
  50. data/lib/plutonium/ui/display/components/boolean.rb +28 -6
  51. data/lib/plutonium/ui/display/components/currency.rb +50 -0
  52. data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
  53. data/lib/plutonium/ui/display/theme.rb +5 -0
  54. data/lib/plutonium/ui/form/base.rb +5 -0
  55. data/lib/plutonium/ui/form/components/toggle.rb +14 -0
  56. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +17 -28
  57. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +67 -0
  58. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +145 -0
  59. data/lib/plutonium/ui/form/concerns/repeater_field_styles.rb +24 -0
  60. data/lib/plutonium/ui/form/interaction.rb +7 -2
  61. data/lib/plutonium/ui/form/options/inferred_types.rb +2 -0
  62. data/lib/plutonium/ui/form/resource.rb +5 -1
  63. data/lib/plutonium/ui/form/theme.rb +12 -0
  64. data/lib/plutonium/ui/grid/card.rb +58 -21
  65. data/lib/plutonium/ui/layout/icon_rail.rb +29 -9
  66. data/lib/plutonium/ui/modal/slideover.rb +9 -3
  67. data/lib/plutonium/ui/sidebar_menu.rb +29 -0
  68. data/lib/plutonium/version.rb +1 -1
  69. data/package.json +1 -1
  70. data/plutonium.gemspec +5 -4
  71. data/src/css/components.css +136 -5
  72. data/src/js/controllers/dirty_form_guard_controller.js +55 -4
  73. data/src/js/controllers/nested_resource_form_fields_controller.js +35 -12
  74. data/src/js/controllers/register_controllers.js +2 -0
  75. data/src/js/controllers/resource_drop_down_controller.js +49 -14
  76. data/src/js/controllers/structured_input_row_controller.js +26 -0
  77. metadata +30 -8
  78. data/docs/superpowers/specs/2026-06-01-interaction-repeater-inputs-design.md +0 -178
  79. data/lib/plutonium/interaction/nested_attributes.rb +0 -93
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65714d92f3f0d6759f5b0fbc0818be12387892214b09959de80d589f859a6a56
4
- data.tar.gz: 2190fd85c3ac535df868581baace2be6565c709263c1059eeccc37bb9ba6c396
3
+ metadata.gz: fdb3a70cfc50828cc986040543d72e8e576497deffb3304aac601b113c507370
4
+ data.tar.gz: 7901dd81e5a9f11519224695071e1fe76c3121e8d311d1b1d6d7445fb2179667
5
5
  SHA512:
6
- metadata.gz: 5ec925506a147113242a8888e96797d56cfb76972e771792752a296b6036ca5adffc8698ad9b967056b043f3c9df86a422d014cea6ec699cac73c692408ee962
7
- data.tar.gz: e8c29ac3f6d9e83b00dba5817e209c1e7d276730b599660dbd4fe5fe4a14684695fb069b0ff5b226b9f0b7a3cc96a7d74bb02edb1cbcf0c23fe345c7d56ace13
6
+ metadata.gz: 3d89dc0f91a27bb0e0e1ac5503cb11059d35309767fa006c200216819e0c123d6d2a558a03ac162964527b992fabc974184d5f357f3b0389405f2c63a13bdfed
7
+ data.tar.gz: 584146030273aeac56b898049f4f9e6a05f364c1b334bdca4ef2e044288548f1277c4d419cf7051504d36e0fd32ffbe48a4540c03f7b80f08405f43b4a0313ab
@@ -656,6 +656,28 @@ attribute :date, :datetime
656
656
 
657
657
  The presence of `:resource` / `:resources` / neither determines the action type — see [[plutonium-resource]] › Action Types.
658
658
 
659
+ ### Structured / repeating input
660
+
661
+ To collect a structured object or a repeating list of field-groups, use
662
+ `structured_input` (it declares the backing attribute for you):
663
+
664
+ ```ruby
665
+ structured_input :address do |f| # single → execute sees { street:, city: }
666
+ f.input :street
667
+ f.input :city
668
+ end
669
+
670
+ structured_input :contacts, repeat: 3 do |f| # repeater → [ { label:, phone: }, ... ]
671
+ f.input :label
672
+ f.input :phone
673
+ end
674
+ ```
675
+
676
+ ⚠️ **`nested_input` and `accepts_nested_attributes_for` are NOT available on
677
+ interactions** (they were model-backed). Use `structured_input` instead — it's
678
+ classless and collects plain hashes/arrays. See [[plutonium-resource]] ›
679
+ Structured Inputs for options (`repeat:`, `using:`, `fields:`).
680
+
659
681
  ## Inputs
660
682
 
661
683
  Same DSL as definition `input` (load [[plutonium-resource]] for the full list of `as:` types, options, dynamic blocks, etc.):
@@ -484,7 +484,7 @@ end
484
484
  | Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
485
485
  | Rich Text | `:markdown` (EasyMDE) |
486
486
  | Numeric | `:number`, `:integer`, `:decimal`, `:range` |
487
- | Boolean | `:boolean` |
487
+ | Boolean | `:toggle` / `:switch` (switch — **default** for boolean columns), `:boolean` (plain checkbox) |
488
488
  | Date/Time | `:date`, `:time`, `:datetime` |
489
489
  | Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
490
490
  | Files | `:file`, `:uppy`, `:attachment` |
@@ -493,7 +493,26 @@ end
493
493
 
494
494
  ### Display Types (show / index)
495
495
 
496
- `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
496
+ `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:color`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
497
+
498
+ #### Auto-inferred display formatting
499
+
500
+ These render automatically — declare an `as:` only to override or pass options:
501
+
502
+ | Column | Renders as | Notes |
503
+ |--------|-----------|-------|
504
+ | `boolean` | Yes/No pill (`:boolean`) | green "Yes" / neutral "No". Override labels: `true_label:`, `false_label:` |
505
+ | `enum` | colored status badge (`:badge`) | known statuses (active, pending, failed…) auto-colored; unknown values get a stable decorative color |
506
+ | `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals, **no symbol** unless you add `unit:` |
507
+
508
+ ```ruby
509
+ display :status, as: :badge, colors: {archived: :neutral, vip: :accent} # override per-value color
510
+ display :price, as: :currency, unit: "£" # literal symbol
511
+ display :price, as: :currency, unit: :currency_symbol # Symbol → read off the record (per-row)
512
+ display :active, as: :boolean, true_label: "Live", false_label: "Off"
513
+ ```
514
+
515
+ Badge color keys: `:neutral`, `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:accent`.
497
516
 
498
517
  ## Field Options
499
518
 
@@ -684,6 +703,61 @@ end
684
703
  - For custom class names, use `class_name:` in the model and `using:` in the definition.
685
704
  - `update_only: true` hides the Add button.
686
705
 
706
+ ## Structured Inputs
707
+
708
+ `structured_input` collects a **classless** group of fields — a single hash, or
709
+ (with `repeat:`) an array of hashes. No association or model class is involved.
710
+ On resources the value is stored in a **JSON/jsonb column**; use it when you
711
+ want structured data in a JSON column rather than a real association (which is
712
+ `nested_input`'s job).
713
+
714
+ ```ruby
715
+ class Spec < ResourceRecord
716
+ # t.json :payload / t.json :rows (jsonb in production)
717
+ end
718
+
719
+ class SpecDefinition < ResourceDefinition
720
+ structured_input :payload do |f| # single → { title:, notes: }
721
+ f.input :title
722
+ f.input :notes
723
+ end
724
+
725
+ structured_input :rows, repeat: 5 do |f| # repeater → [ { key:, value: }, ... ] (max 5)
726
+ f.input :key
727
+ f.input :value
728
+ end
729
+ end
730
+
731
+ class SpecPolicy < ResourcePolicy
732
+ # NOTE: unlike nested_input, you DO permit the column name here.
733
+ # (update inherits permitted_attributes_for_create automatically.)
734
+ def permitted_attributes_for_create = [:payload, :rows]
735
+ end
736
+ ```
737
+
738
+ `execute`/the record sees `payload => { "title" => …, "notes" => … }` and
739
+ `rows => [ { "key" => …, "value" => … }, … ]` (string keys from the JSON column;
740
+ blank rows are dropped, `_destroy` stripped).
741
+
742
+ ### Options
743
+
744
+ | Option | Description |
745
+ |--------|-------------|
746
+ | `repeat` | Presence ⇒ array (repeater). `Integer` = max rows; `true` = default cap (10); absent = single hash |
747
+ | `using` | A fields definition class instead of a block |
748
+ | `fields` | Subset of fields from the referenced definition |
749
+
750
+ ### Gotchas
751
+
752
+ - The column must be `json`/`jsonb` (or otherwise hold a hash/array). No model macro is needed — the value assigns directly.
753
+ - **Unlike `nested_input`, you DO permit the column name** in `permitted_attributes_for_*` (it's a regular attribute on a JSON column).
754
+ - `repeat: 1` is "array, max one row" — **not** the single form. Presence of `repeat:` always means an array.
755
+ - Rows are positional plain hashes — **no ids, no per-row class, no type coercion**.
756
+ - **No automatic validation.** Classless ⇒ nothing to attach `validates` to. `required:` and a select's `choices:` are **client-side only**, not enforced on the server. To enforce, add a model `validate` (resource) or a `validate` on the interaction (ActiveModel, checked before `execute`).
757
+ - **`as: :select` drops unknown values.** If a stored value isn't in `choices:`, the `<select>` renders blank and **saving overwrites it with `nil`** (standard `<select>` behaviour). Keep `choices:` a stable superset or use free text when values can drift.
758
+ - Inside repeater rows, prefer **native** field types (string, number, text, native `select`, checkbox). JS-enhanced inputs (slim-select, flatpickr, easymde, uppy, intl-tel) transform the DOM and may not survive the repeater's clone-by-innerHTML — verify before relying on them.
759
+ - Same DSL works on **interactions** (see [[plutonium-behavior]] › Interactions) — there it backs an ActiveModel attribute reaching `execute`.
760
+
687
761
  ## File Uploads
688
762
 
689
763
  ```ruby
@@ -252,6 +252,7 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
252
252
  | `input_tag` | text (auto-detected type) |
253
253
  | `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `tel_tag`, `hidden_tag` | standard HTML inputs |
254
254
  | `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
255
+ | `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`) — default for boolean columns; `as: :boolean` for a plain checkbox |
255
256
 
256
257
  ### Plutonium-enhanced tags
257
258
 
@@ -302,7 +303,8 @@ end
302
303
  These all live in the definition layer:
303
304
 
304
305
  - **Pre-submit / dynamic forms** — see [[plutonium-resource]] › Dynamic Forms.
305
- - **Nested inputs** (`nested_input :variants`) — see [[plutonium-resource]] › Nested Inputs.
306
+ - **Nested inputs** (`nested_input :variants`) — association-backed inline forms; see [[plutonium-resource]] › Nested Inputs.
307
+ - **Structured inputs** (`structured_input :payload`, `structured_input :rows, repeat: 5`) — classless hash / array-of-hashes into a JSON column (resources) or an attribute (interactions); reuses the repeater chrome. See [[plutonium-resource]] › Structured Inputs.
306
308
  - **Interaction forms** — interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [[plutonium-behavior]] › Interactions.
307
309
 
308
310
  ---
@@ -512,6 +514,16 @@ rails generate pu:eject:layout
512
514
 
513
515
  These copy `_resource_header.html.erb`, `_resource_sidebar.html.erb`, and `layouts/resource.html.erb` into the portal so you can edit them directly.
514
516
 
517
+ ## Navigation menu items
518
+
519
+ The sidebar/icon-rail menu is built with `Phlexi::Menu::Builder` in `_resource_sidebar.html.erb`. Extra options on `item` are spread onto the rendered `<a>`, so an item can opt into `target` / `rel` / `data:` / `aria:`:
520
+
521
+ ```ruby
522
+ m.item "Inbox", url: inbox_path, icon: Icon, target: "_blank", rel: "noopener", data: {turbo_frame: "_top"}
523
+ ```
524
+
525
+ Applies to both shells (icon-rail leaf, parent flyout trigger, and flyout children; classic sidebar). Framework `class`/`data`/`aria` win on conflict — `class:` merges with the base classes, and on a parent trigger `data:`/`aria:` merge with the flyout wiring so options can't break the toggle. Phlexi's reserved `:active` key is never emitted as an attribute.
526
+
515
527
  ## Custom layout class (Phlex)
516
528
 
517
529
  ```ruby
@@ -732,7 +744,8 @@ Ready-to-use styled components in `src/css/components.css`. **Prefer these over
732
744
  ### Inputs, cards, panels, tables, toolbars, empty states
733
745
 
734
746
  ```
735
- .pu-input / -invalid / -valid .pu-label / -required .pu-hint / .pu-error .pu-checkbox
747
+ .pu-input / -invalid / -valid .pu-label / -required .pu-hint / .pu-error .pu-checkbox / .pu-toggle
748
+ .pu-badge / -neutral / -primary / -secondary / -success / -danger / -warning / -info / -accent
736
749
  .pu-card / .pu-card-body
737
750
  .pu-panel-header / -title / -description
738
751
  .pu-table-wrapper / .pu-table / -header / -header-cell / -body-row / -body-row-selected / -body-cell / .pu-selection-cell
@@ -844,7 +857,7 @@ end
844
857
 
845
858
  ### Display theme keys
846
859
 
847
- `fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`.
860
+ `fields_wrapper`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`, `boolean`, `badge`, `currency`, `color`.
848
861
 
849
862
  ## Table theme
850
863
 
@@ -913,6 +926,7 @@ end
913
926
  - **Dark mode is `selector`, not `class`.** Toggle via `document.documentElement.classList.toggle('dark')`.
914
927
  - **Tokens are CSS variables, not Tailwind keys** — `bg-[var(--pu-surface)]`, not `bg-pu-surface`.
915
928
  - **`render_actions` is mandatory in custom `form_template`** — otherwise no submit button.
929
+ - **Dropdowns (`resource-drop-down`) teleport their menu to `<body>` while open.** popper's `fixed` strategy alone is still clipped by a transformed + `overflow:hidden` ancestor (e.g. grid cards, app shells), so the controller reparents the open menu to `<body>` and restores it on close. Don't rely on the menu being a DOM child of its trigger while open.
916
930
 
917
931
  ---
918
932
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## [0.56.0] - 2026-06-05
2
+
3
+ ### 🚀 Features
4
+
5
+ - *(ui)* Auto-rendered components for boolean, enum & money fields
6
+ - *(generators/lite)* Add pu:lite:tune and pu:lite:maintenance for SQLite tuning + maintenance
7
+ - *(ui)* Sidebar menu items accept arbitrary link attributes
8
+ - *(ui)* Restore deleted nested rows + shared, polished removed bar
9
+ - *(ui)* Type-aware grid cards + overhaul KitchenSink dummy resource
10
+
11
+ ### 🐛 Bug Fixes
12
+
13
+ - *(generators/update)* Sync skills in a fresh process; pin post-install notice to 0.49.0
14
+ - *(ui)* Native multi-selects render at a usable height
15
+ - *(ui)* Dropdown menu teleports to <body> to escape overflow clipping
16
+ - *(routing)* Force :resources route_type for has_many nested routes
17
+ - *(ui)* Record-scoped commit URL for actions with record_action: false
18
+ - *(api)* Serialize JSON values via as_json (ISO 8601 datetimes)
19
+ - *(generators)* Add reading role to Rails Pulse connects_to config
20
+ - *(actions)* Bind subject during interaction param extraction
21
+ - *(ui)* Dirty-form-guard tracks edits via first-interaction baseline
22
+ - *(ui)* Give the JSON form input dark-mode styling
23
+
24
+ ### 🧪 Testing
25
+
26
+ - Fix stale generator assertions and drop committed dummy schema
27
+ - *(dummy)* Add KitchenSink resource exercising every input/display type
28
+
29
+ ### ⚙️ Miscellaneous Tasks
30
+
31
+ - Sync appraisal gemfile.lock files to v0.55.0
32
+ ## [0.55.0] - 2026-06-03
33
+
34
+ ### 🚀 Features
35
+
36
+ - Structured_input — classless structured & repeater inputs (resources + interactions) (#60)
37
+
38
+ ### 🐛 Bug Fixes
39
+
40
+ - *(ui)* Keep modal backdrop static to smooth dialog dismiss
41
+
42
+ ### 🧪 Testing
43
+
44
+ - *(dummy)* Land authenticated users on the entity-scoped org portal
45
+ - *(dummy)* Serve the Organization resource in the org portal
1
46
  ## [0.54.0] - 2026-06-01
2
47
 
3
48
  ### 🚀 Features