plutonium 0.50.0 → 0.51.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -1,413 +0,0 @@
1
- ---
2
- name: plutonium-interaction
3
- description: Use BEFORE writing an interaction class, encapsulating business logic, or building multi-step operations beyond basic CRUD. Covers Plutonium::Resource::Interaction.
4
- ---
5
-
6
- # Plutonium Interactions
7
-
8
- ## 🚨 Critical (read first)
9
- - **`ActiveRecord::RecordInvalid` is NOT rescued automatically.** Always rescue it when using `create!`/`update!`/`save!` and return `failed(e.record.errors)`.
10
- - **Return `succeed(...)` or `failed(...)`** from `execute` — the controller won't know what happened otherwise.
11
- - **Redirect is automatic on success.** Only call `with_redirect_response` for a *different* destination.
12
- - **Bulk actions use `resources` (plural).** Policy methods are checked per record; if any fails, the whole request fails.
13
- - **Related skills:** `plutonium-definition` (registering actions), `plutonium-policy` (authorizing actions), `plutonium-forms` (interaction form templates).
14
-
15
- Interactions encapsulate business logic into reusable, testable units. They handle input validation, execution, and outcomes.
16
-
17
- ## Basic Structure
18
-
19
- ```ruby
20
- # app/interactions/resource_interaction.rb (generated during install)
21
- class ResourceInteraction < Plutonium::Resource::Interaction
22
- end
23
-
24
- # app/interactions/publish_post_interaction.rb
25
- class PublishPostInteraction < ResourceInteraction
26
- # Presentation
27
- presents label: "Publish",
28
- icon: Phlex::TablerIcons::Send,
29
- description: "Make this post public"
30
-
31
- # Attributes (inputs)
32
- attribute :resource # The record being acted upon
33
- attribute :publish_date, :datetime, default: -> { Time.current }
34
-
35
- # Form inputs (what user sees)
36
- input :publish_date, as: :datetime
37
-
38
- # Validations
39
- validates :publish_date, presence: true
40
-
41
- private
42
-
43
- def execute
44
- resource.update!(published_at: publish_date)
45
- succeed(resource).with_message("Post published!")
46
- rescue ActiveRecord::RecordInvalid => e
47
- failed(e.record.errors)
48
- end
49
- end
50
- ```
51
-
52
- ## Attributes
53
-
54
- Define inputs using ActiveModel attributes:
55
-
56
- ```ruby
57
- attribute :resource # Record (for record actions)
58
- attribute :resources # Collection (for bulk actions)
59
- attribute :email, :string # String input
60
- attribute :count, :integer, default: 1 # With default
61
- attribute :active, :boolean, default: -> { true } # Callable default
62
- attribute :tags, :array # Array
63
- attribute :metadata, :hash # Hash
64
- attribute :date, :datetime # DateTime
65
- ```
66
-
67
- ## Form Inputs
68
-
69
- Define form fields with the `input` method (same as definitions):
70
-
71
- ```ruby
72
- input :email
73
- input :role, as: :select, choices: %w[admin user]
74
- input :content, as: :text
75
- input :date, as: :date
76
- ```
77
-
78
- See `plutonium-definition` skill for all input types and options.
79
-
80
- ## Presentation
81
-
82
- Configure how the action appears in the UI:
83
-
84
- ```ruby
85
- presents label: "Archive Record",
86
- icon: Phlex::TablerIcons::Archive,
87
- description: "Move to archive for later reference"
88
- ```
89
-
90
- Access presentation:
91
- ```ruby
92
- MyInteraction.label # => "Archive Record"
93
- MyInteraction.icon # => Phlex::TablerIcons::Archive
94
- MyInteraction.description # => "Move to archive..."
95
- ```
96
-
97
- ## Execution and Outcomes
98
-
99
- ### The execute Method
100
-
101
- ```ruby
102
- private
103
-
104
- def execute
105
- # Your business logic here
106
- # Must return succeed() or failed()
107
- end
108
- ```
109
-
110
- ### Success Outcomes
111
-
112
- ```ruby
113
- # Basic success (redirects automatically to resource)
114
- succeed(resource)
115
-
116
- # With message
117
- succeed(resource).with_message("Done!")
118
- succeed(resource).with_message("Warning!", :alert)
119
-
120
- # With custom redirect (only if different from default)
121
- succeed(resource).with_redirect_response(custom_path)
122
-
123
- # With file download
124
- succeed(resource).with_file_response(file_path, filename: "report.pdf")
125
- ```
126
-
127
- **Note:** Redirect is automatic on success - the controller redirects to the resource by default. Only use `with_redirect_response` if you need a different destination.
128
-
129
- ### Failure Outcomes
130
-
131
- ```ruby
132
- # Basic failure
133
- failed("Something went wrong")
134
-
135
- # With ActiveModel errors
136
- failed(resource.errors)
137
-
138
- # With hash of errors
139
- failed(email: "is invalid", name: "is required")
140
- ```
141
-
142
- ### Chaining Interactions
143
-
144
- ```ruby
145
- def execute
146
- CreateUserInteraction.call(view_context:, **user_params)
147
- .and_then { |result| SendWelcomeEmail.call(view_context:, user: result.value) }
148
- .and_then { |result| LogActivity.call(view_context:, user: result.value) }
149
- .with_message("User created and welcomed!")
150
- end
151
- ```
152
-
153
- On failure, the chain short-circuits and returns the failure immediately.
154
-
155
- ## Validations
156
-
157
- Use standard ActiveModel validations:
158
-
159
- ```ruby
160
- validates :email, presence: true
161
- validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
162
- validates :role, inclusion: { in: %w[admin user guest] }
163
-
164
- validate :custom_validation
165
-
166
- private
167
-
168
- def custom_validation
169
- if resource.archived?
170
- errors.add(:resource, "cannot be modified when archived")
171
- end
172
- end
173
- ```
174
-
175
- Validations run automatically before `execute`. If invalid, returns `failed()` with errors.
176
-
177
- ## Interaction Types
178
-
179
- ### Record Actions
180
-
181
- Act on a single record:
182
-
183
- ```ruby
184
- class ArchiveInteraction < Plutonium::Resource::Interaction
185
- attribute :resource # Single record
186
-
187
- def execute
188
- resource.update!(archived: true)
189
- succeed(resource)
190
- rescue ActiveRecord::RecordInvalid => e
191
- failed(e.record.errors)
192
- end
193
- end
194
- ```
195
-
196
- **Note:** `ActiveRecord::RecordInvalid` is NOT rescued automatically. Always rescue it when using bang methods (`create!`, `update!`, `save!`).
197
-
198
- ### Resource Actions
199
-
200
- Act at the collection/class level (no specific record):
201
-
202
- ```ruby
203
- class ImportInteraction < Plutonium::Resource::Interaction
204
- # No :resource attribute
205
- attribute :file
206
-
207
- input :file, as: :file
208
-
209
- def execute
210
- records = CSV.parse(file)
211
- Post.import(records)
212
- succeed(records)
213
- end
214
- end
215
- ```
216
-
217
- ### Bulk Actions (Multiple Records)
218
-
219
- Act on multiple selected records. When registered, the table shows checkboxes and a toolbar appears when records are selected.
220
-
221
- ```ruby
222
- class BulkArchiveInteraction < Plutonium::Resource::Interaction
223
- attribute :resources # Collection of records (note: plural)
224
-
225
- def execute
226
- resources.update_all(archived: true)
227
- succeed(resources).with_message("Archived #{resources.count} records")
228
- end
229
- end
230
- ```
231
-
232
- **Authorization:** Bulk actions use per-record authorization. The policy method is checked for each selected record - if any fails, the entire request is rejected. The UI only shows actions that all selected records support.
233
-
234
- ## Connecting to Definitions
235
-
236
- Register interactions as actions:
237
-
238
- ```ruby
239
- class PostDefinition < ResourceDefinition
240
- # Record action (shows on individual records)
241
- action :publish, interaction: PublishPostInteraction
242
-
243
- # Resource action (shows at collection level)
244
- action :import, interaction: ImportInteraction
245
-
246
- # With options
247
- action :archive,
248
- interaction: ArchiveInteraction,
249
- confirmation: "Are you sure?",
250
- category: :danger,
251
- position: 100
252
- end
253
- ```
254
-
255
- ### Action Options
256
-
257
- | Option | Description |
258
- |--------|-------------|
259
- | `interaction:` | The interaction class |
260
- | `confirmation:` | Confirmation message before execution |
261
- | `category:` | `:primary`, `:secondary`, `:danger` |
262
- | `position:` | Display order (lower = first) |
263
- | `turbo_frame:` | Turbo frame target (default: `remote_modal`) |
264
- | `icon:` | Override interaction icon |
265
- | `label:` | Override interaction label |
266
-
267
- ## Policy Integration
268
-
269
- Control access with policy methods:
270
-
271
- ```ruby
272
- class PostPolicy < ResourcePolicy
273
- def publish?
274
- update? && record.draft?
275
- end
276
-
277
- def archive?
278
- destroy? && !record.archived?
279
- end
280
-
281
- def import?
282
- create? # Resource-level action
283
- end
284
- end
285
- ```
286
-
287
- The policy method name matches the action name with `?`.
288
-
289
- ## Accessing Context
290
-
291
- Inside interactions:
292
-
293
- ```ruby
294
- def execute
295
- # Access current user via view_context
296
- current_user = view_context.controller.helpers.current_user
297
-
298
- # Access the resource
299
- resource.update!(updated_by: current_user)
300
-
301
- succeed(resource)
302
- end
303
- ```
304
-
305
- ## Immediate vs Form Actions
306
-
307
- Plutonium automatically determines if an action needs a form:
308
-
309
- - **Has inputs defined** → Shows form first (GET), then executes (POST)
310
- - **No inputs** → Executes immediately (POST with confirmation)
311
-
312
- ```ruby
313
- # Shows form (has inputs)
314
- class InviteUserInteraction < Plutonium::Resource::Interaction
315
- attribute :resource
316
- attribute :email
317
- input :email # This triggers form display
318
- end
319
-
320
- # Immediate execution (no inputs)
321
- class ArchiveInteraction < Plutonium::Resource::Interaction
322
- attribute :resource
323
- # No inputs = immediate with confirmation
324
- end
325
- ```
326
-
327
- ## Complete Example
328
-
329
- ```ruby
330
- class Company::InviteUserInteraction < Plutonium::Resource::Interaction
331
- presents label: "Invite User",
332
- icon: Phlex::TablerIcons::UserPlus,
333
- description: "Send an invitation email"
334
-
335
- attribute :resource # The company
336
- attribute :email, :string
337
- attribute :role, :string
338
-
339
- input :email
340
- input :role, as: :select, choices: -> { UserInvite.roles.keys }
341
-
342
- validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
343
- validates :role, presence: true, inclusion: { in: UserInvite.roles.keys }
344
- validate :not_already_invited
345
-
346
- private
347
-
348
- def execute
349
- invite = UserInvite.create!(
350
- company: resource,
351
- email: email,
352
- role: role,
353
- invited_by: current_user
354
- )
355
- UserInviteMailer.invitation(invite).deliver_later
356
-
357
- succeed(resource).with_message("Invitation sent to #{email}")
358
- rescue ActiveRecord::RecordInvalid => e
359
- failed(e.record.errors)
360
- end
361
-
362
- def not_already_invited
363
- return unless email.present?
364
-
365
- if UserInvite.exists?(company: resource, email: email, state: :pending)
366
- errors.add(:email, "already has a pending invitation")
367
- end
368
- end
369
-
370
- def current_user
371
- view_context.controller.helpers.current_user
372
- end
373
- end
374
- ```
375
-
376
- ## Generating Interaction URLs
377
-
378
- Use the `interaction:` kwarg on `resource_url_for`. The action type (record/bulk/resource) is inferred from the element and presence of `ids:`:
379
-
380
- ```ruby
381
- # Record action — instance argument
382
- resource_url_for(@post, interaction: :publish)
383
- # => /posts/:id/record_actions/publish
384
-
385
- # Resource (class-level) action — class with no ids
386
- resource_url_for(Post, interaction: :import)
387
- # => /posts/resource_actions/import
388
-
389
- # Bulk action — class + ids
390
- resource_url_for(Post, interaction: :archive, ids: [1, 2, 3])
391
- # => /posts/bulk_actions/archive?ids[]=1&ids[]=2&ids[]=3
392
-
393
- # Composes with parent / entity scoping
394
- resource_url_for(@post, parent: @user, interaction: :publish)
395
- ```
396
-
397
- The same URL serves both GET (form/confirmation) and POST (commit) — the verb determines which controller action runs. Passing both `interaction:` and `action:` raises `ArgumentError`.
398
-
399
- ## Best Practices
400
-
401
- 1. **Keep interactions focused** - One action per interaction
402
- 2. **Use validations** - Validate all inputs before execution
403
- 3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
404
- 4. **Return meaningful messages** - Help users understand what happened
405
- 5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
406
- 6. **Test independently** - Interactions are easy to unit test
407
-
408
- ## Related Skills
409
-
410
- - `plutonium-definition` - Declaring actions in definitions
411
- - `plutonium-forms` - Custom interaction form templates
412
- - `plutonium-policy` - Controlling access to actions
413
- - `plutonium` - How interactions fit in the architecture