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,440 +0,0 @@
1
- ---
2
- name: plutonium-model
3
- description: Use BEFORE editing a Plutonium resource model, adding associations, has_cents, SGID, or routing helpers. For tenancy / associated_with / relation_scope, also load plutonium-entity-scoping.
4
- ---
5
-
6
- # Plutonium Resource Models
7
-
8
- ## 🚨 Critical (read first)
9
- - **Use `pu:res:scaffold`.** Never hand-write resource model files — the scaffold sets up `Plutonium::Resource::Record`, associations, and the expected section layout.
10
- - **Declare associations for the entity.** For multi-tenant apps, add `belongs_to`, `has_one :through`, or an `associated_with_<entity>` scope so `associated_with` can resolve. Fix the model, not the policy.
11
- - **Compound uniqueness** — in multi-tenant models, scope unique constraints to the tenant FK (`uniqueness: {scope: :organization_id}`), or you leak across tenants.
12
- - **Keep business logic out of the model.** Use interactions for multi-step ops, policies for authorization.
13
- - **Related skills:** `plutonium-entity-scoping` (tenancy mechanics), `plutonium-create-resource` (scaffold), `plutonium-definition` (UI), `plutonium-policy` (authorization).
14
-
15
- ## Quick checklist
16
-
17
- Adding/editing a Plutonium model:
18
-
19
- 1. Use `pu:res:scaffold` for new models; include `Plutonium::Resource::Record` on existing ones.
20
- 2. Place associations/enums/validations in the right section (enums → belongs_to → has_one → has_many → scopes → validations → callbacks).
21
- 3. For monetary fields, use `has_cents :field_cents`.
22
- 4. For multi-tenancy, declare an association path to the entity (`belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope).
23
- 5. Add compound uniqueness scoped to the tenant FK.
24
- 6. For SEO URLs, override `path_parameter` or `dynamic_path_parameter`.
25
- 7. Override `to_label` if `:name`/`:title` isn't meaningful.
26
- 8. Verify with `rails runner "puts Model.first.associated_with(entity).count"`.
27
-
28
- **Always use generators to create models** - never create model files manually:
29
- ```bash
30
- rails g pu:res:scaffold Post title:string content:text --dest=main_app
31
- ```
32
- See `plutonium-create-resource` for full field type syntax and generator options.
33
-
34
- A model becomes a Plutonium resource by including `Plutonium::Resource::Record`. This provides enhanced ActiveRecord functionality for routing, labeling, field introspection, associations, and monetary handling.
35
-
36
- ## Setup
37
-
38
- ### Standard Setup
39
-
40
- ```ruby
41
- # app/models/application_record.rb
42
- class ApplicationRecord < ActiveRecord::Base
43
- include Plutonium::Resource::Record
44
- primary_abstract_class
45
- end
46
-
47
- # app/models/resource_record.rb (optional abstract class)
48
- class ResourceRecord < ApplicationRecord
49
- self.abstract_class = true
50
- end
51
-
52
- # app/models/property.rb
53
- class Property < ResourceRecord
54
- # Now has access to all Plutonium features
55
- end
56
- ```
57
-
58
- ### What's Included
59
-
60
- `Plutonium::Resource::Record` includes six modules:
61
-
62
- | Module | Purpose |
63
- |--------|---------|
64
- | `HasCents` | Monetary value handling (cents → decimal) |
65
- | `Routes` | URL parameters, path customization |
66
- | `Labeling` | Human-readable `to_label` method |
67
- | `FieldNames` | Field introspection and categorization |
68
- | `Associations` | SGID support for secure serialization |
69
- | `AssociatedWith` | Entity scoping for multi-tenant apps |
70
-
71
- ## Model Structure
72
-
73
- Follow the template structure (comment markers indicate where to add code):
74
-
75
- ```ruby
76
- class Property < ResourceRecord
77
- # add concerns above.
78
-
79
- TYPES = {apartment: "Apartment", house: "House"}.freeze
80
- # add constants above.
81
-
82
- enum :state, archived: 0, active: 1
83
- enum :property_class, residential: 0, commercial: 1
84
- # add enums above.
85
-
86
- has_cents :market_value_cents
87
- # add model configurations above.
88
-
89
- belongs_to :company
90
- # add belongs_to associations above.
91
-
92
- has_one :address
93
- # add has_one associations above.
94
-
95
- has_many :units
96
- has_many :amenities, class_name: "PropertyAmenity"
97
- # add has_many associations above.
98
-
99
- has_one_attached :photo
100
- has_many_attached :documents
101
- # add attachments above.
102
-
103
- scope :active, -> { where(state: :active) }
104
- scope :by_company, ->(company) { where(company: company) }
105
- # add scopes above.
106
-
107
- validates :name, presence: true
108
- validates :property_code, presence: true, uniqueness: {scope: :company_id}
109
- # add validations above.
110
-
111
- before_validation :generate_code, on: :create
112
- # add callbacks above.
113
-
114
- delegate :name, to: :company, prefix: true
115
- # add delegations above.
116
-
117
- has_rich_text :description
118
- # add misc attribute macros above.
119
-
120
- def full_address
121
- address&.to_s
122
- end
123
-
124
- # add methods above. add private methods below.
125
-
126
- private
127
-
128
- def generate_code
129
- self.property_code ||= SecureRandom.hex(4).upcase
130
- end
131
- end
132
- ```
133
-
134
- ### Section Order
135
-
136
- 1. **Concerns** - `include` statements
137
- 2. **Constants** - `TYPES = {...}.freeze`, etc.
138
- 3. **Enums** - `enum :state, ...`
139
- 4. **Model configurations** - `has_cents`
140
- 5. **belongs_to associations**
141
- 6. **has_one associations**
142
- 7. **has_many associations**
143
- 8. **Attachments** - `has_one_attached`, `has_many_attached`
144
- 9. **Scopes**
145
- 10. **Validations**
146
- 11. **Callbacks**
147
- 12. **Delegations**
148
- 13. **Misc attribute macros** - `has_rich_text`, `has_secure_token`, `has_secure_password`
149
- 14. **Methods** - Public methods above, private methods below
150
-
151
- ## Monetary Handling (has_cents)
152
-
153
- Store monetary values as integers (cents) while exposing decimal interfaces.
154
-
155
- ### Basic Usage
156
-
157
- ```ruby
158
- class Product < ResourceRecord
159
- has_cents :price_cents # Creates price getter/setter
160
- has_cents :cost_cents, name: :wholesale # Custom accessor name
161
- has_cents :tax_cents, rate: 1000 # 3 decimal places
162
- has_cents :quantity_cents, rate: 1 # Whole numbers only
163
- end
164
-
165
- product = Product.new
166
- product.price = 19.99
167
- product.price_cents # => 1999
168
- product.price # => 19.99
169
-
170
- # Truncates (doesn't round)
171
- product.price = 10.999
172
- product.price_cents # => 1099
173
- ```
174
-
175
- ### Options
176
-
177
- ```ruby
178
- has_cents :field_cents,
179
- name: :custom_name, # Accessor name (default: field without _cents)
180
- rate: 100, # Conversion rate (default: 100)
181
- suffix: "amount" # Suffix for generated name (default: "amount")
182
- ```
183
-
184
- ### Using `has_cents` fields in policies and definitions
185
-
186
- **Always reference the virtual accessor (`:price`), never the underlying column (`:price_cents`).**
187
-
188
- ```ruby
189
- # Model
190
- class Product < ResourceRecord
191
- has_cents :price_cents # exposes virtual :price
192
- end
193
-
194
- # ✅ Policy — use the virtual name
195
- class ProductPolicy < ResourcePolicy
196
- def permitted_attributes_for_create
197
- %i[name price] # NOT :price_cents
198
- end
199
- end
200
-
201
- # ✅ Definition — use the virtual name
202
- class ProductDefinition < ResourceDefinition
203
- field :price, as: :decimal
204
- end
205
- ```
206
-
207
- The virtual accessor handles form input, validation, and display as a decimal. Using `:price_cents` directly in a policy or definition forces users to enter integer cents and bypasses the conversion. Generators sometimes emit the `_cents` name in the policy — fix it by hand if you see it (and add `has_cents` if it's missing from the model).
208
-
209
- ### Validation
210
-
211
- ```ruby
212
- class Product < ResourceRecord
213
- has_cents :price_cents
214
-
215
- # Validate the cents field
216
- validates :price_cents, numericality: {greater_than: 0}
217
- end
218
-
219
- product = Product.new(price: -10)
220
- product.valid? # => false
221
- product.errors[:price_cents] # => ["must be greater than 0"]
222
- product.errors[:price] # => ["is invalid"] (propagated)
223
- ```
224
-
225
- ### Introspection
226
-
227
- ```ruby
228
- Product.has_cents_attributes
229
- # => {price_cents: {name: :price, rate: 100}, ...}
230
-
231
- Product.has_cents_attribute?(:price_cents) # => true
232
- ```
233
-
234
- ## Association SGID Support
235
-
236
- All associations get Signed Global ID (SGID) methods for secure serialization.
237
-
238
- ### Singular Associations (belongs_to, has_one)
239
-
240
- ```ruby
241
- class Post < ResourceRecord
242
- belongs_to :user
243
- has_one :featured_image
244
- end
245
-
246
- post = Post.first
247
-
248
- # Get SGID
249
- post.user_sgid # => "BAh7CEkiCG..."
250
- post.featured_image_sgid # => "BAh7CEkiCG..."
251
-
252
- # Set by SGID (finds and assigns)
253
- post.user_sgid = "BAh7CEkiCG..."
254
- post.featured_image_sgid = "BAh7CEkiCG..."
255
- ```
256
-
257
- ### Collection Associations (has_many, has_and_belongs_to_many)
258
-
259
- ```ruby
260
- class User < ResourceRecord
261
- has_many :posts
262
- has_and_belongs_to_many :roles
263
- end
264
-
265
- user = User.first
266
-
267
- # Get SGIDs
268
- user.post_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
269
- user.role_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
270
-
271
- # Bulk assignment
272
- user.post_sgids = ["BAh7CEkiCG...", ...]
273
-
274
- # Individual manipulation
275
- user.add_post_sgid("BAh7CEkiCG...") # Add to collection
276
- user.remove_post_sgid("BAh7CEkiCG...") # Remove from collection
277
- ```
278
-
279
- ## Entity Scoping (associated_with)
280
-
281
- `Plutonium::Resource::Record` provides `Model.associated_with(entity)` for multi-tenant queries. It resolves via a custom `associated_with_<entity>` scope, a direct `belongs_to`, or an auto-detected `has_one :through` chain.
282
-
283
- Quick example:
284
-
285
- ```ruby
286
- class Comment < ResourceRecord
287
- belongs_to :post
288
- end
289
-
290
- Comment.associated_with(post) # => Comment.where(post: post)
291
- ```
292
-
293
- > **For entity scoping details — the three model shapes (direct child, join table, grandchild), `has_one :through` patterns, custom scopes, `default_relation_scope`, and how it fits with policies and portals — see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth.**
294
-
295
- ## URL Routing
296
-
297
- ### Default Behavior
298
-
299
- ```ruby
300
- user = User.find(1)
301
- user.to_param # => "1"
302
- ```
303
-
304
- ### Custom Path Parameters
305
-
306
- Use a stable, unique field instead of ID:
307
-
308
- ```ruby
309
- class User < ResourceRecord
310
- private
311
-
312
- def path_parameter(param_name)
313
- :username # Must be unique
314
- end
315
- end
316
-
317
- user = User.create(username: "john_doe")
318
- user.to_param # => "john_doe"
319
- # URLs: /users/john_doe
320
- ```
321
-
322
- ### Dynamic Path Parameters (SEO-friendly)
323
-
324
- Include ID prefix for uniqueness with human-readable suffix:
325
-
326
- ```ruby
327
- class Article < ResourceRecord
328
- private
329
-
330
- def dynamic_path_parameter(param_name)
331
- :title
332
- end
333
- end
334
-
335
- article = Article.create(id: 1, title: "My Great Article")
336
- article.to_param # => "1-my-great-article"
337
- # URLs: /articles/1-my-great-article
338
- ```
339
-
340
- ### Path Parameter Lookup
341
-
342
- ```ruby
343
- User.from_path_param("john_doe")
344
- Article.from_path_param("1-my-great-article") # Extracts ID
345
- ```
346
-
347
- ## Labeling
348
-
349
- The `to_label` method provides human-readable record labels:
350
-
351
- ```ruby
352
- # Automatic - checks :name, then :title, then fallback
353
- user = User.new(name: "John Doe")
354
- user.to_label # => "John Doe"
355
-
356
- user = User.create(id: 1)
357
- user.to_label # => "User #1"
358
-
359
- # Custom override
360
- class Product < ResourceRecord
361
- def to_label
362
- "#{name} (#{sku})"
363
- end
364
- end
365
- ```
366
-
367
- ## Field Introspection
368
-
369
- Access field information programmatically:
370
-
371
- ```ruby
372
- # All resource fields
373
- User.resource_field_names
374
- # => [:id, :name, :email, :company, :avatar, ...]
375
-
376
- # By category
377
- User.content_column_field_names # Database columns
378
- User.belongs_to_association_field_names # belongs_to associations
379
- User.has_one_association_field_names # has_one associations
380
- User.has_many_association_field_names # has_many associations
381
- User.has_one_attached_field_names # Active Storage single
382
- User.has_many_attached_field_names # Active Storage multiple
383
- ```
384
-
385
- ## Common Patterns
386
-
387
- ### Archiving (State-Based)
388
-
389
- ```ruby
390
- class Property < ResourceRecord
391
- enum :state, archived: 0, active: 1
392
-
393
- scope :active, -> { where(state: :active) }
394
- scope :archived, -> { where(state: :archived) }
395
- end
396
- ```
397
-
398
- ### Multi-Tenant Scoping
399
-
400
- ```ruby
401
- class Property < ResourceRecord
402
- belongs_to :company
403
-
404
- # Compound uniqueness for multi-tenant
405
- validates :property_code, uniqueness: {scope: :company_id}
406
-
407
- # Custom scope for entity scoping
408
- scope :associated_with_company, ->(company) { where(company: company) }
409
- end
410
- ```
411
-
412
- ## Performance Tips
413
-
414
- ```ruby
415
- # Efficient: Direct belongs_to
416
- Comment.associated_with(post) # Simple WHERE
417
-
418
- # Less efficient: Reverse has_many (logs warning)
419
- Post.associated_with(comment) # JOIN required
420
-
421
- # Optimal: Custom scope when direct isn't possible
422
- scope :associated_with_user, ->(user) { where(user_id: user.id) }
423
-
424
- # SGID: Batch assignment over individual adds
425
- user.post_sgids = sgid_array # Single operation
426
- ```
427
-
428
- ## Best Practices
429
-
430
- 1. **Use enums for state** - `enum :state, archived: 0, active: 1` instead of soft-delete
431
- 2. **Compound uniqueness** - Always scope uniqueness to tenant/parent
432
- 3. **Organize with comments** - Use section headers for readability
433
- 4. **Keep models focused** - Business logic in interactions, not models
434
- 5. **Validate at boundaries** - Validate user input, trust internal code
435
- 6. **Use scopes** - Define commonly used queries as scopes
436
-
437
- ## Related Skills
438
-
439
- - `plutonium-create-resource` - Scaffold generator for new resources
440
- - `plutonium-definition` - Definition overview, fields, inputs, displays