plutonium 0.52.0 → 0.53.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-resource/SKILL.md +6 -4
  3. data/.claude/skills/plutonium-tenancy/SKILL.md +9 -4
  4. data/.claude/skills/plutonium-ui/SKILL.md +29 -5
  5. data/CHANGELOG.md +16 -0
  6. data/app/assets/plutonium.css +1 -1
  7. data/app/assets/plutonium.js +257 -11
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +39 -39
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/app/views/plutonium/_resource_header.html.erb +2 -1
  12. data/docs/.vitepress/config.ts +1 -0
  13. data/docs/guides/authentication.md +1 -1
  14. data/docs/guides/custom-actions.md +2 -1
  15. data/docs/guides/customizing-ui.md +6 -5
  16. data/docs/guides/multi-tenancy.md +6 -6
  17. data/docs/guides/theming.md +1 -1
  18. data/docs/public/images/components/avatar.png +0 -0
  19. data/docs/reference/auth/accounts.md +1 -1
  20. data/docs/reference/behavior/policies.md +1 -1
  21. data/docs/reference/configuration.md +61 -0
  22. data/docs/reference/resource/actions.md +2 -1
  23. data/docs/reference/resource/definition.md +4 -3
  24. data/docs/reference/tenancy/entity-scoping.md +12 -13
  25. data/docs/reference/ui/components.md +53 -0
  26. data/docs/reference/ui/forms.md +1 -1
  27. data/docs/reference/ui/pages.md +6 -5
  28. data/docs/superpowers/specs/2026-05-29-avatar-component-design.md +153 -0
  29. data/gemfiles/rails_7.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  31. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  32. data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +7 -3
  33. data/lib/plutonium/action/base.rb +43 -63
  34. data/lib/plutonium/configuration.rb +7 -0
  35. data/lib/plutonium/definition/actions.rb +10 -11
  36. data/lib/plutonium/definition/base.rb +29 -0
  37. data/lib/plutonium/helpers/assets_helper.rb +0 -30
  38. data/lib/plutonium/helpers/content_helper.rb +0 -44
  39. data/lib/plutonium/helpers/display_helper.rb +0 -62
  40. data/lib/plutonium/helpers/turbo_helper.rb +0 -4
  41. data/lib/plutonium/helpers.rb +0 -2
  42. data/lib/plutonium/resource/definition.rb +0 -42
  43. data/lib/plutonium/ui/action_button.rb +4 -3
  44. data/lib/plutonium/ui/avatar.rb +182 -0
  45. data/lib/plutonium/ui/component/kit.rb +2 -0
  46. data/lib/plutonium/ui/form/base.rb +16 -2
  47. data/lib/plutonium/ui/form/components/secure_association.rb +3 -2
  48. data/lib/plutonium/ui/form/resource.rb +58 -0
  49. data/lib/plutonium/ui/form/theme.rb +7 -3
  50. data/lib/plutonium/ui/grid/card.rb +10 -26
  51. data/lib/plutonium/ui/modal/base.rb +36 -1
  52. data/lib/plutonium/ui/modal/centered.rb +24 -6
  53. data/lib/plutonium/ui/modal/slideover.rb +26 -11
  54. data/lib/plutonium/ui/nav_user.rb +3 -23
  55. data/lib/plutonium/ui/page/edit.rb +6 -3
  56. data/lib/plutonium/ui/page/interactive_action.rb +5 -3
  57. data/lib/plutonium/ui/page/new.rb +6 -3
  58. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +1 -1
  59. data/lib/plutonium/version.rb +1 -1
  60. data/package.json +1 -1
  61. data/src/css/components.css +38 -1
  62. data/src/css/slim_select.css +3 -2
  63. data/src/js/controllers/dirty_form_guard_controller.js +165 -0
  64. data/src/js/controllers/register_controllers.js +2 -0
  65. data/src/js/controllers/remote_modal_controller.js +53 -19
  66. data/src/js/turbo/index.js +1 -0
  67. data/src/js/turbo/turbo_confirm.js +128 -0
  68. metadata +10 -6
  69. data/lib/plutonium/helpers/attachment_helper.rb +0 -73
  70. data/lib/plutonium/helpers/table_helper.rb +0 -35
  71. /data/lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/{password_changed.text.erb → change_password_notify.text.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3015426fb7d1a742cd436928a9a8e2c24597f391d68657b8f00ed31058cf59c
4
- data.tar.gz: 20bf4101ff956923bf01b28ad890bc5000760a5181ab65ac07f83f2bb739b0d7
3
+ metadata.gz: 1575a79af57aa89f49041cbdb1e31364cea08fb3d541398e095772470a3da6b8
4
+ data.tar.gz: a3d4dfebbec5836c9557c903393f7deebcfb17b94ca8d61ccb5a1dd1ffbd7aee
5
5
  SHA512:
6
- metadata.gz: 14b0e28c435b199564a46eae98b84c4273c2783344a3257dcb9adc872002c8e761e058f7f76fbe909b3419facd0e90e1d9ee1a3c8fa5f128189eb1df6e365761
7
- data.tar.gz: 19e6dfaf2c2526fcb4cfadba2eb2e09fa294141abb2175f52348f45d050d975aa92b59541d8aa5eb4c14b2bd973f60b45abea9498ea20bc7340529d8e2f96370
6
+ metadata.gz: 8c6954af1d7e64be809c28e06140b5660f58e94ec254fe2960c4d0b28b615f515df84d107528899d791ab3cc5bf38ac18cf623aa4c00d3a683bf7b963b96bc47
7
+ data.tar.gz: a5f6a8ea0ad4e9fafc6559aac4b488c131a6c796c58506a49a49e8088dd3408cd04641ab4ffef357b66a373f88bd6c99476eb2d1cd7cb79eaaf2c993c6322cf5
@@ -725,9 +725,10 @@ class PostDefinition < ResourceDefinition
725
725
  # nil (default) = auto (hidden for singular, shown for plural)
726
726
  submit_and_continue false
727
727
 
728
- # How :new / :edit render
728
+ # How :new / :edit + interactive actions render
729
729
  # :slideover (default), :centered, or false (full pages)
730
- modal :centered
730
+ # size: :sm / :md (default) / :lg / :xl / :auto / :full
731
+ modal :centered, size: :lg
731
732
 
732
733
  # Titles
733
734
  index_page_title "All Posts"
@@ -757,7 +758,7 @@ class PostDefinition < ResourceDefinition
757
758
  end
758
759
  ```
759
760
 
760
- `modal:` only affects the framework `:new` / `:edit` actions. Custom actions have their own per-action `modal:` option (default `:centered`).
761
+ `modal:` is the default for framework `:new` / `:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win.
761
762
 
762
763
  ## Metadata Panel (show page)
763
764
 
@@ -983,7 +984,8 @@ action :name,
983
984
  confirmation: "Are you sure?",
984
985
  turbo_frame: "_top",
985
986
  route_options: {action: :foo},
986
- modal: :slideover # :centered (default) or :slideover
987
+ modal: :slideover, # :slideover / :centered overrides definition's modal mode
988
+ size: :lg # :sm / :md / :lg / :xl / :auto / :full — overrides definition's modal size
987
989
  ```
988
990
 
989
991
  `Action#with(...)` — actions are frozen value objects; clone with overrides:
@@ -15,7 +15,7 @@ Cross-references back to [[plutonium-resource]] (models, definitions) and [[plut
15
15
 
16
16
  ## 🚨 Critical (read first)
17
17
 
18
- - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins to the entity triggers `verify_default_relation_scope_applied!`. Make sure `default_relation_scope(relation)` is called somewhere in the chain — explicitly here, or via `super` to a parent policy (e.g., a package base) that calls it.
18
+ - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins to the entity triggers `verify_default_relation_scope_applied!`. Make sure the chain ends up calling `default_relation_scope(relation)` — explicitly, or via `super(relation)` (the framework base calls it).
19
19
  - **Always declare an association path from model to entity.** Direct `belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope. If `associated_with` can't resolve, Plutonium raises. Fix the **model**, not the policy.
20
20
  - **Parent scoping beats entity scoping.** When a parent is present (nested resource), `default_relation_scope` scopes via the parent, NOT via `entity_scope`. Don't double-scope.
21
21
  - **One level of nesting only.** Grandparent → parent → child nested routes are NOT supported. Use top-level routes for deeper relationships.
@@ -170,7 +170,12 @@ relation_scope { |r| r.joins(:project).where(projects: {organization_id: current
170
170
  relation_scope { |r| r.where(published: true) }
171
171
  ```
172
172
 
173
- **Prefer calling `default_relation_scope(relation)` explicitly** instead of relying on `super`. `super` works when you're extending a parent policy (e.g., a package-level base) that itself calls `default_relation_scope` but it's brittle against `Plutonium::Resource::Policy` directly because `super`'s semantics depend on how ActionPolicy's DSL registered the scope. The runtime verification checks `default_relation_scope` was hit somewhere — not that you wrote it in this class.
173
+ **`default_relation_scope(relation)` must end up being called somewhere in the chain** — runtime verification just checks it was hit, not that you wrote it in this class. Both work:
174
+
175
+ - `default_relation_scope(relation).where(...)` — explicit, always safe
176
+ - `super(relation).where(...)` — `Plutonium::Resource::Policy`'s `relation_scope` block calls `default_relation_scope`, so chaining through `super` picks it up
177
+
178
+ Pick the one that reads better for the situation.
174
179
 
175
180
  ### Intentionally skipping
176
181
 
@@ -199,7 +204,7 @@ module AdminPortal
199
204
  end
200
205
  ```
201
206
 
202
- Routes become `/organizations/:organization_id/posts`. Portal extracts `params[:organization_id]` and loads the entity automatically.
207
+ Routes become `/<mount>/:organization_scoped/posts` (resolving to `/<mount>/42/posts` at request time — the entity id is the first path segment after the mount). Portal extracts `params[:organization_scoped]` and loads the entity automatically. The `_scoped` suffix on the param name avoids colliding with `params[:organization]` from a `belongs_to :organization` on child models.
203
208
 
204
209
  ### Custom strategy (subdomain, session, etc.)
205
210
 
@@ -235,7 +240,7 @@ entity_scope
235
240
 
236
241
  - **Multiple associations to the same entity class.** E.g. `Match belongs_to :home_team, :away_team` both pointing at `Team`. Plutonium raises — override `scoped_entity_association` on the controller to pick one (`def scoped_entity_association = :home_team`).
237
242
  - **`param_key` differs from association name.** Fine — Plutonium matches by **class**, not param key. `scope_to_entity Competition::Team, param_key: :team` works with `belongs_to :competition_team`.
238
- - **Default `param_key` includes `_scoped` suffix.** `scope_to_entity Organization` produces routes like `/organization_scoped/:organization_scoped_id/posts` to avoid colliding with a `belongs_to :organization` on child models. Pass `param_key:` (and optionally `route_key:`) to override for cleaner URLs.
243
+ - **Default `param_key` includes `_scoped` suffix.** `scope_to_entity Organization` reads `params[:organization_scoped]` (not `params[:organization]`) so it doesn't collide with `params[:organization]` from a `belongs_to :organization` on child models. The URL itself is unchanged — the entity id is just the first path segment after the mount (`/<mount>/42/posts`). Pass `param_key:` only if you want a different param name in your controllers.
239
244
  - **Forgetting compound uniqueness.** `validates :code, uniqueness: true` leaks across tenants. Use `uniqueness: {scope: :organization_id}`.
240
245
  - **"Temporary" `where` bypass for debugging.** Use `skip_default_relation_scope!` explicitly. Never leave a `where` bypass in code.
241
246
 
@@ -390,6 +390,7 @@ Inside any `Plutonium::UI::Component::Base` (or any page/form/display):
390
390
  PageHeader(title: "Dashboard", description: "...", actions: [...])
391
391
  Panel(class: "mt-4") { p { "Content" } }
392
392
  Block { TabList(items: tabs) }
393
+ Avatar(user) # profile image: src → Navii fallback → icon
393
394
  EmptyCard("No items found")
394
395
  ActionButton(action, url: "/posts/new")
395
396
  DynaFrameHost(src: "/some/path", loading: :lazy)
@@ -401,6 +402,28 @@ TablePagination(pagy)
401
402
  Breadcrumbs()
402
403
  ```
403
404
 
405
+ ## Avatar
406
+
407
+ `Avatar(subject = nil, src: nil, size: :md, alt: nil, **attrs)` — profile image with a deterministic [Navii](https://navii.dev) fallback. Registered in the kit.
408
+
409
+ ```ruby
410
+ Avatar(user) # Navii fallback seeded from the record
411
+ Avatar(user, src: :avatar) # user.avatar if present, else Navii fallback
412
+ Avatar(user, src: user.avatar) # pass the attachment/uploader/URL directly
413
+ Avatar("acme-team") # String subject = deterministic seed
414
+ Avatar("https://.../p.png") # URL-shaped subject is shown as the image
415
+ Avatar(src: avatar_url) # bare image, no subject/fallback
416
+ ```
417
+
418
+ - **subject** (positional): record → PII-free hashed seed + default `alt` (display name); String → seed. A URL-shaped String (`http(s)://…` or `/…`) is routed to `src` (shown as the image), not used as a seed.
419
+ - **src**: a Symbol is sent to the subject (`:avatar` → `subject.avatar`, a **contract** — raises if absent); otherwise an ActiveStorage attachment, active_shrine/Shrine uploader, or URL string. ActiveStorage resolves via `helpers.url_for`; everything else via its own `#url`.
420
+ - **size**: `:xs 24 / :sm 32 / :md 40 / :lg 48 / :xl 64`, or a raw Integer.
421
+ - **Privacy**: the value sent to Navii is **always** a SHA256 hash — no ids, emails, or seed strings leave the app. Deterministic per subject.
422
+ - **Resolution order**: resolved `src` → Navii (from subject) → generic user icon.
423
+ - **Config**: `config.navii_host_url` (default `https://api.navii.dev`); the component appends `/avatar/:seed`.
424
+
425
+ 🚨 Ejected shells: `Avatar` only shows a Navii avatar when `NavUser` is passed `record:`. The gem's `_resource_header.html.erb` passes `record: (current_user if current_user.respond_to?(:id))`; portals that **ejected** the header before this must re-eject (`rails g pu:eject:shell --dest=<portal>`) or add the `record:` line, otherwise they keep the icon fallback. Pass a record only — a String `current_user` (e.g. a guest) would otherwise be seeded as a literal identity.
426
+
404
427
  ## Custom Phlex components
405
428
 
406
429
  ```ruby
@@ -450,17 +473,18 @@ All pages inherit this. Modals and frame navigation work without special handlin
450
473
 
451
474
  # Part 5 — Modals, Slideovers, Tabs
452
475
 
453
- ## Modal/slideover for `:new` / `:edit`
476
+ ## Modal/slideover for `:new` / `:edit` + interactive actions
454
477
 
455
478
  ```ruby
456
479
  class PostDefinition < ResourceDefinition
457
- modal :slideover # default — slide-in panel from the right
458
- # modal :centered # centered dialog
459
- # modal false # full standalone page
480
+ modal :slideover # default — slide-in panel from the right
481
+ # modal :centered # centered dialog
482
+ # modal :centered, size: :lg # centered, wider container
483
+ # modal false # full standalone page
460
484
  end
461
485
  ```
462
486
 
463
- Custom interactive actions render in their own dialog with their own per-action `modal:` option (`:centered` default, or `:slideover`). See [[plutonium-resource]] › Action Options.
487
+ Drives both framework `:new` / `:edit` and every interactive action on the definition. `size:` accepts `:sm`, `:md` (default), `:lg`, `:xl`, `:auto` (hugs content), or `:full`. Per-action `modal:` / `size:` overrides win. See [[plutonium-resource]] › Action Options.
464
488
 
465
489
  ## Tabs on the show page
466
490
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.53.0] - 2026-05-31
2
+
3
+ ### 🚀 Features
4
+
5
+ - *(ui)* Add Avatar component with Navii fallback (#59)
6
+
7
+ ### 🐛 Bug Fixes
8
+
9
+ - *(docs)* Correct broken cross-page anchors in guides
10
+ - *(docs)* Retract incorrect "never super" guidance in relation_scope
11
+ - *(docs)* Correct scoped-URL shape in multi-tenancy docs
12
+ - *(rodauth)* Match change_password_notify mailer template name
13
+
14
+ ### 🚜 Refactor
15
+
16
+ - *(helpers)* Remove dead view helpers superseded by Phlex components
1
17
  ## [0.52.0] - 2026-05-21
2
18
 
3
19
  ### 🐛 Bug Fixes