plutonium 0.51.0 → 0.52.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-app/SKILL.md +2 -0
  3. data/.claude/skills/plutonium-auth/SKILL.md +6 -4
  4. data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
  5. data/.claude/skills/plutonium-tenancy/SKILL.md +25 -6
  6. data/.claude/skills/plutonium-testing/SKILL.md +3 -1
  7. data/.claude/skills/plutonium-ui/SKILL.md +3 -3
  8. data/CHANGELOG.md +17 -0
  9. data/app/assets/plutonium.css +1 -1
  10. data/app/assets/plutonium.js +1 -0
  11. data/app/assets/plutonium.js.map +3 -3
  12. data/app/assets/plutonium.min.js +1 -1
  13. data/app/assets/plutonium.min.js.map +3 -3
  14. data/docs/.vitepress/config.ts +1 -2
  15. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  16. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  17. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  18. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  19. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  20. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  21. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  22. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  23. data/docs/.vitepress/theme/custom.css +144 -0
  24. data/docs/.vitepress/theme/index.ts +58 -1
  25. data/docs/getting-started/index.md +33 -50
  26. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  27. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  28. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  29. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  30. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  31. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  32. data/docs/guides/authentication.md +10 -5
  33. data/docs/guides/authorization.md +3 -3
  34. data/docs/guides/creating-packages.md +8 -11
  35. data/docs/guides/custom-actions.md +6 -1
  36. data/docs/guides/customizing-ui.md +258 -0
  37. data/docs/guides/index.md +49 -32
  38. data/docs/guides/multi-tenancy.md +10 -2
  39. data/docs/guides/nested-resources.md +69 -0
  40. data/docs/guides/search-filtering.md +6 -0
  41. data/docs/guides/testing.md +5 -1
  42. data/docs/guides/theming.md +13 -0
  43. data/docs/guides/user-invites.md +10 -4
  44. data/docs/guides/user-profile.md +8 -0
  45. data/docs/index.md +10 -219
  46. data/docs/public/asciinema/home-scaffold.cast +305 -0
  47. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  48. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  49. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  50. data/docs/public/images/guides/nested-inputs.png +0 -0
  51. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  52. data/docs/public/images/guides/search-filtering-index.png +0 -0
  53. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  54. data/docs/public/images/guides/theming-after.png +0 -0
  55. data/docs/public/images/guides/theming-before.png +0 -0
  56. data/docs/public/images/guides/user-invites-landing.png +0 -0
  57. data/docs/public/images/guides/user-profile-edit.png +0 -0
  58. data/docs/public/images/guides/user-profile-show.png +0 -0
  59. data/docs/public/images/home-index.png +0 -0
  60. data/docs/public/images/home-new.png +0 -0
  61. data/docs/public/images/home-show.png +0 -0
  62. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  63. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  64. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  65. data/docs/public/images/tutorial/02-new-form.png +0 -0
  66. data/docs/public/images/tutorial/03-create-account.png +0 -0
  67. data/docs/public/images/tutorial/03-login.png +0 -0
  68. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  69. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  70. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  71. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  72. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  73. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  74. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  75. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  76. data/docs/reference/app/generators.md +4 -4
  77. data/docs/reference/auth/accounts.md +6 -7
  78. data/docs/reference/auth/index.md +1 -1
  79. data/docs/reference/behavior/policies.md +1 -1
  80. data/docs/reference/index.md +67 -55
  81. data/docs/reference/resource/definition.md +1 -1
  82. data/docs/reference/tenancy/entity-scoping.md +8 -1
  83. data/docs/reference/tenancy/index.md +1 -1
  84. data/docs/reference/tenancy/invites.md +12 -5
  85. data/docs/reference/ui/tables.md +8 -4
  86. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  87. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  88. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  89. data/gemfiles/rails_7.gemfile.lock +1 -1
  90. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  91. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  92. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  93. data/lib/generators/pu/invites/install_generator.rb +44 -0
  94. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  95. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  96. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  97. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  98. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  99. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  100. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  101. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  102. data/lib/generators/pu/saas/membership/USAGE +4 -1
  103. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  104. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  105. data/lib/plutonium/helpers/turbo_helper.rb +19 -0
  106. data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
  107. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  108. data/lib/plutonium/ui/component/methods.rb +1 -0
  109. data/lib/plutonium/ui/form/base.rb +17 -1
  110. data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
  111. data/lib/plutonium/ui/form/interaction.rb +1 -1
  112. data/lib/plutonium/ui/form/theme.rb +1 -1
  113. data/lib/plutonium/ui/page/edit.rb +1 -1
  114. data/lib/plutonium/ui/page/new.rb +1 -1
  115. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  116. data/lib/plutonium/version.rb +1 -1
  117. data/package.json +4 -1
  118. data/src/js/controllers/form_controller.js +5 -4
  119. data/yarn.lock +108 -1
  120. metadata +45 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fd905a5d5a930d6805e0df02632a59a0c5ecd9052c190d341f22264640c3425
4
- data.tar.gz: db6fbe37f645953c545c1f1fcd10ef82e0dc58cc996b47c2ef574812b0d6394b
3
+ metadata.gz: b3015426fb7d1a742cd436928a9a8e2c24597f391d68657b8f00ed31058cf59c
4
+ data.tar.gz: 20bf4101ff956923bf01b28ad890bc5000760a5181ab65ac07f83f2bb739b0d7
5
5
  SHA512:
6
- metadata.gz: d30de7e455c8798a60a92777141197507595f8a5d37b7e9d355e0530d4590369470f24a36f62554f6c2dcede93e9de8ac0dc6e732fa8f984b64b41d75525a3a2
7
- data.tar.gz: e95d855a9d9aee83d2edc4dac9d44f001389d4ef0f10cc9338f13c371e6798db4f73f67197b2d03b8699111ab8cb56ec8ed7335b34ec622c08dff9d8af336b1d
6
+ metadata.gz: 14b0e28c435b199564a46eae98b84c4273c2783344a3257dcb9adc872002c8e761e058f7f76fbe909b3419facd0e90e1d9ee1a3c8fa5f128189eb1df6e365761
7
+ data.tar.gz: 19e6dfaf2c2526fcb4cfadba2eb2e09fa294141abb2175f52348f45d050d975aa92b59541d8aa5eb4c14b2bd973f60b45abea9498ea20bc7340529d8e2f96370
@@ -505,6 +505,8 @@ register_resource ::Post
505
505
  register_resource ::Profile, singular: true # if --singular
506
506
  ```
507
507
 
508
+ Re-running `pu:res:conn` for the same resource is **idempotent** — already-registered entries report `identical` and are not duplicated. Insertion falls back gracefully when the conventional `# register resources above` marker is missing (uses the `routes.draw do` opening), and warns clearly if it can't find any anchor.
509
+
508
510
  ### Generated controller
509
511
 
510
512
  ```ruby
@@ -45,7 +45,6 @@ rails generate pu:rodauth:account user [options]
45
45
  |---|---|
46
46
  | `--defaults` | Enables login, logout, remember, password reset |
47
47
  | `--kitchen_sink` | Enables ALL features |
48
- | `--primary` | Mark as primary account (no URL prefix) |
49
48
  | `--no-mails` | Skip mailer setup |
50
49
  | `--argon2` | Use Argon2 instead of bcrypt |
51
50
  | `--api_only` | JSON API only (no sessions) |
@@ -90,12 +89,15 @@ rails generate pu:rodauth:admin admin --extra-attributes=name:string,department:
90
89
  enum :role, super_admin: 0, admin: 1
91
90
  ```
92
91
 
93
- Rake task for direct admin creation:
92
+ Rake task for direct admin creation (namespace is `rodauth`, task name is the account name):
94
93
 
95
94
  ```bash
96
- rails rodauth_admin:create[admin@example.com,password123]
95
+ EMAIL=admin@example.com rails rodauth:admin
96
+ # (run without EMAIL to be prompted)
97
97
  ```
98
98
 
99
+ Creates the account and sends a verification email; the admin sets their own password through the flow. No password is passed on the command line.
100
+
99
101
  ### SaaS setup — `pu:saas:setup` (meta-generator)
100
102
 
101
103
  Creates the User + Entity + Membership trio AND runs:
@@ -109,7 +111,7 @@ Don't generate another entity portal after this. Pass `--force` to re-run.
109
111
 
110
112
  ```bash
111
113
  rails g pu:saas:setup --user Customer --entity Organization
112
- rails g pu:saas:setup --user Customer --entity Organization --roles=member,admin
114
+ rails g pu:saas:setup --user Customer --entity Organization --roles=admin,member
113
115
  rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
114
116
  rails g pu:saas:setup --user Customer --entity Organization \
115
117
  --user-attributes=name:string --entity-attributes=slug:string
@@ -18,7 +18,7 @@ For tenant-scoped `relation_scope` and entity scoping, load [[plutonium-tenancy]
18
18
  - **`ActiveRecord::RecordInvalid` is NOT rescued automatically in interactions.** Always rescue when using `create!` / `update!` / `save!`, return `failed(e.record.errors)`.
19
19
  - **Return `succeed(...)` or `failed(...)`** from `execute` — the controller can't tell what happened otherwise.
20
20
  - **Redirect is automatic on success** — only use `with_redirect_response` for a *different* destination.
21
- - **`relation_scope` must compose with `default_relation_scope(relation)` explicitly** not `super`. See [[plutonium-tenancy]].
21
+ - **`relation_scope` must end up calling `default_relation_scope(relation)` somewhere in the chain.** Prefer calling it explicitly. `super` works when extending a parent policy (e.g., a package base) that itself calls it. See [[plutonium-tenancy]].
22
22
  - **For `has_cents` fields, use the virtual name (`:price`), not `:price_cents`** in `permitted_attributes_for_*`.
23
23
  - **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (undefined methods return `false`).
24
24
  - **Named custom routes.** When adding custom routes, always pass `as:` so `resource_url_for` can build URLs.
@@ -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!`. Always call `default_relation_scope(relation)` explicitlynot `super`.
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.
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,7 @@ 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
- **Do not use `super`** from inside `relation_scope`. Call `default_relation_scope(relation)` explicitly — `super` semantics depend on how ActionPolicy's DSL registered the scope.
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.
174
174
 
175
175
  ### Intentionally skipping
176
176
 
@@ -235,6 +235,7 @@ entity_scope
235
235
 
236
236
  - **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
237
  - **`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.
238
239
  - **Forgetting compound uniqueness.** `validates :code, uniqueness: true` leaks across tenants. Use `uniqueness: {scope: :organization_id}`.
239
240
  - **"Temporary" `where` bypass for debugging.** Use `skip_default_relation_scope!` explicitly. Never leave a `where` bypass in code.
240
241
 
@@ -421,10 +422,18 @@ rails generate pu:invites:install
421
422
  | `--entity-model=NAME` | `Entity` | Entity model name |
422
423
  | `--user-model=NAME` | `User` | User model name |
423
424
  | `--invite-model=NAME` | `<EntityModel><UserModel>Invite` | Invite class name (omit for single-flow apps) |
424
- | `--membership-model=NAME` | `EntityUser` | Membership join model |
425
- | `--roles=ROLES` | `member,admin` | Comma-separated |
425
+ | `--membership-model=NAME` | `EntityUser` | Membership join model (must already exist; roles are read from its `enum :role`) |
426
426
  | `--rodauth=NAME` | `user` | Rodauth configuration for signup |
427
427
  | `--enforce-domain` | `false` | Require invited email domain to match entity |
428
+ | `--dest=PACKAGE` | `main_app` | Package where the entity model lives (controls where `invite_user_interaction.rb` is generated) |
429
+
430
+ ::: 🚨 No `--roles` flag here
431
+ Role list is derived from the membership model's `enum :role`. Set roles via `pu:saas:membership --roles=...` (or edit the enum directly). **Index 0 is the most privileged** — typically `owner`, which the invite UI excludes from selectable choices; new invitees default to the second role (`roles[1]`).
432
+ :::
433
+
434
+ ::: 🚨 ActiveRecord encryption keys required
435
+ The invite model uses `encrypts :token, deterministic: true`. Without configured AR encryption keys, creating or accepting an invite raises `ActiveRecord::Encryption::Errors::Configuration`. The generator detects this and warns at install time — generate keys with `bin/rails db:encryption:init`, then paste the printed `active_record_encryption:` block into `config/credentials.yml.enc` (or set the equivalent `ACTIVE_RECORD_ENCRYPTION_*` ENV vars in production).
436
+ :::
428
437
 
429
438
  ### What gets created
430
439
 
@@ -619,13 +628,23 @@ class Invites::UserInvite < Invites::ResourceRecord
619
628
  end
620
629
  ```
621
630
 
622
- ### Domain enforcement / custom roles
631
+ ### Domain enforcement
623
632
 
624
633
  ```bash
625
634
  rails g pu:invites:install --enforce-domain
626
- rails g pu:invites:install --roles=viewer,editor,admin,owner
627
635
  ```
628
636
 
637
+ ### Custom roles
638
+
639
+ Set roles when generating the membership model (ordering: index 0 = most privileged):
640
+
641
+ ```bash
642
+ rails g pu:saas:membership --user Customer --entity Organization --roles=admin,editor,viewer
643
+ # → enum :role, { owner: 0, admin: 1, editor: 2, viewer: 3 } (owner auto-prepended)
644
+ ```
645
+
646
+ Or edit `enum :role` on the existing membership model directly. Then run `pu:invites:install`.
647
+
629
648
  ## Portal connection
630
649
 
631
650
  ```ruby
@@ -207,7 +207,9 @@ current_account(portal: :admin)
207
207
  with_portal(:org) { ... } # scoped portal switch
208
208
  ```
209
209
 
210
- **Override hook for non-Rodauth apps:** define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use). `AuthHelpers` will defer to it.
210
+ **Default Rodauth login expects `password: "password123"`** `login_as` POSTs to `/<account_table>/login` with that hardcoded password. Either seed test accounts with it (fixtures/factories) or override via `sign_in_for_tests` below.
211
+
212
+ **Override hook for non-Rodauth apps (or to bypass Rodauth in tests):** define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use). `AuthHelpers` will defer to it.
211
213
 
212
214
  ```ruby
213
215
  def sign_in_for_tests(account, portal:)
@@ -351,8 +351,8 @@ end
351
351
  class PostDefinition < ResourceDefinition
352
352
  class Table < Table
353
353
  def view_template
354
- render_search_bar
355
- render_scopes_bar
354
+ render_toolbar
355
+ render_scopes_pills
356
356
 
357
357
  if collection.empty?
358
358
  render_empty_card
@@ -371,7 +371,7 @@ end
371
371
 
372
372
  | Method | Purpose |
373
373
  |---|---|
374
- | `render_search_bar`, `render_scopes_bar` | Toolbar pieces |
374
+ | `render_toolbar`, `render_scopes_pills`, `render_filter_pills`, `render_bulk_actions_toolbar` | Toolbar pieces |
375
375
  | `render_table` | Default table |
376
376
  | `render_empty_card` | Empty state |
377
377
  | `render_footer` | Pagination |
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [0.52.0] - 2026-05-21
2
+
3
+ ### 🐛 Bug Fixes
4
+
5
+ - *(generators)* Use inclusion validation for required booleans
6
+ - *(generators/assets)* Warn on failed yarn add instead of silently continuing
7
+ - *(docs)* Let StopWriting terminals scroll horizontally instead of overflowing the column
8
+ - *(docs)* Drop misleading 15-min claim — hero CTA → 'Tutorial', getting-started title → 'Learn Plutonium by building'
9
+ - *(generators,docs)* Tutorial walkthrough + reference audit (#57)
10
+ - *(ui/form)* Scope form ids per turbo frame to prevent stream-replace collisions
11
+ - *(ui/form)* Hide secure_association "+" inside secondary modal
12
+ - *(js/form)* Dedupe pre_submit hidden field on repeat change events
13
+ - *(ui)* Form error alert margin + include model name in New/Edit page titles
14
+
15
+ ### ⚙️ Miscellaneous Tasks
16
+
17
+ - *(appraisal)* Refresh rails-8.1 gemfile.lock for v0.51.0
1
18
  ## [0.51.0] - 2026-05-14
2
19
 
3
20
  ### 🚀 Features