plutonium 0.54.0 → 0.55.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 (38) 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 +55 -0
  4. data/.claude/skills/plutonium-ui/SKILL.md +2 -1
  5. data/CHANGELOG.md +14 -0
  6. data/app/assets/plutonium.css +1 -1
  7. data/app/assets/plutonium.js +18 -0
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +30 -30
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/docs/public/images/reference/structured-inputs-removed.png +0 -0
  12. data/docs/public/images/reference/structured-inputs.png +0 -0
  13. data/docs/reference/resource/definition.md +110 -0
  14. data/docs/superpowers/plans/2026-06-02-structured-inputs.md +1061 -0
  15. data/docs/superpowers/plans/2026-06-02-structured-inputs.md.tasks.json +60 -0
  16. data/docs/superpowers/specs/2026-06-01-structured-inputs-design.md +191 -0
  17. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  18. data/lib/plutonium/definition/base.rb +1 -0
  19. data/lib/plutonium/definition/structured_inputs.rb +67 -0
  20. data/lib/plutonium/interaction/README.md +24 -78
  21. data/lib/plutonium/interaction/base.rb +10 -2
  22. data/lib/plutonium/resource/controller.rb +6 -1
  23. data/lib/plutonium/resource/controllers/interactive_actions.rb +10 -6
  24. data/lib/plutonium/structured_inputs/param_cleaner.rb +36 -0
  25. data/lib/plutonium/structured_inputs/params_concern.rb +36 -0
  26. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +3 -3
  27. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +178 -0
  28. data/lib/plutonium/ui/form/concerns/repeater_field_styles.rb +24 -0
  29. data/lib/plutonium/ui/form/resource.rb +4 -1
  30. data/lib/plutonium/ui/modal/slideover.rb +9 -3
  31. data/lib/plutonium/version.rb +1 -1
  32. data/package.json +1 -1
  33. data/src/css/components.css +10 -5
  34. data/src/js/controllers/register_controllers.js +2 -0
  35. data/src/js/controllers/structured_input_row_controller.js +26 -0
  36. metadata +14 -5
  37. data/docs/superpowers/specs/2026-06-01-interaction-repeater-inputs-design.md +0 -178
  38. data/lib/plutonium/interaction/nested_attributes.rb +0 -93
@@ -333,6 +333,116 @@ end
333
333
  - **`update_only: true` hides the Add button** — for `has_one` and "settings"-style associations.
334
334
  - **Custom class names** — use `class_name:` in the model AND `using:` in the definition.
335
335
 
336
+ ## Structured inputs
337
+
338
+ Classless inline fieldsets backed by a JSON/jsonb column. No model associations
339
+ required — the whole sub-form is serialised into a single column as a hash
340
+ (single form) or an array of hashes (repeater).
341
+
342
+ ![A single structured input (Payload) and a repeater (Rows)](/images/reference/structured-inputs.png)
343
+
344
+ ```ruby
345
+ # model
346
+ class Listing < ApplicationRecord
347
+ include Plutonium::Resource::Record
348
+ # columns: address (json), contacts (json)
349
+ end
350
+
351
+ # definition
352
+ class ListingDefinition < ResourceDefinition
353
+ # single → stored as a hash
354
+ structured_input :address do |f|
355
+ f.input :street
356
+ f.input :city
357
+ end
358
+
359
+ # repeater → stored as an array of hashes (max 5 rows)
360
+ structured_input :contacts, repeat: 5 do |f|
361
+ f.input :label
362
+ f.input :phone_number
363
+ end
364
+ end
365
+ ```
366
+
367
+ ### Options
368
+
369
+ | Option | Description |
370
+ |---|---|
371
+ | `repeat:` | `true` (default cap of 10) or an integer max-rows cap. Omit for a single-hash form. |
372
+ | `using:` | Another Definition class whose `input` declarations are used as the fieldset. |
373
+ | `fields:` | Subset of fields to take from the `using:` definition. |
374
+
375
+ ### Removing rows
376
+
377
+ Each repeater row has a **Remove** button. Removing a row collapses it to a
378
+ compact _Removed — Restore_ bar and disables its inputs, so the browser omits
379
+ them from the submission. The server simply rebuilds the JSON column from the
380
+ rows it receives — there is no `_destroy` marker. **Restore** brings the row
381
+ back before saving.
382
+
383
+ ![A removed row collapsed to a Restore bar](/images/reference/structured-inputs-removed.png)
384
+
385
+ ### Policy
386
+
387
+ Permit the column name as a plain symbol — Plutonium handles the nested hash
388
+ params automatically:
389
+
390
+ ```ruby
391
+ def permitted_attributes_for_create
392
+ super + %i[address contacts]
393
+ end
394
+ ```
395
+
396
+ ### On interactions
397
+
398
+ `structured_input` is also available on `Plutonium::Interaction::Base`. The
399
+ attribute is declared automatically; `execute` receives the value as a `Hash`
400
+ (single) or `Array<Hash>` (repeater). `nested_input` and
401
+ `accepts_nested_attributes_for` are **not** available on interactions.
402
+
403
+ ### Validation
404
+
405
+ ::: warning Structured inputs are not validated for you
406
+ The fields are classless render declarations, so there is nothing for Plutonium
407
+ to attach validations to (unlike [`nested_input`](#nested-inputs), whose nested
408
+ records run their own model validations). Whatever the form submits is stored
409
+ as-is, after blank rows are dropped — **no per-field server-side validation**.
410
+ :::
411
+
412
+ Specifically:
413
+
414
+ - **HTML constraints are client-side only.** A field's `required:` and a
415
+ select's `choices:` guide the browser but are **not** enforced on the server —
416
+ an API call or a crafted request can submit anything.
417
+ - **Selects silently drop unknown values.** If a stored value is not among a
418
+ `as: :select` field's `choices:`, the `<select>` renders **blank**, and saving
419
+ the form **overwrites the stored value with `nil`** (the option list is the
420
+ only thing constraining it). This is standard `<select>` behaviour, but it
421
+ bites harder here because JSON values aren't constrained by a DB enum and your
422
+ `choices:` can drift. Keep `choices:` a stable superset, or use a free-text
423
+ input, when values can change over time.
424
+
425
+ To enforce anything, add the validation yourself — it runs server-side:
426
+
427
+ ```ruby
428
+ # resource: validate the JSON column on the model
429
+ class Listing < ApplicationRecord
430
+ include Plutonium::Resource::Record
431
+
432
+ validate :contacts_have_labels
433
+ def contacts_have_labels
434
+ Array(contacts).each_with_index do |row, i|
435
+ errors.add(:contacts, "row #{i + 1} needs a label") if row["label"].blank?
436
+ end
437
+ end
438
+ end
439
+
440
+ # interaction: it's an ActiveModel, validated before `execute`
441
+ validate do
442
+ contacts.each { |c| errors.add(:contacts, "label required") if c[:label].blank? }
443
+ end
444
+ ```
445
+
336
446
  ## File uploads
337
447
 
338
448
  ```ruby