plutonium 0.44.0 → 0.45.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/skill.md +88 -11
  3. data/.claude/skills/plutonium-assets/SKILL.md +1 -1
  4. data/.claude/skills/plutonium-controller/SKILL.md +6 -2
  5. data/.claude/skills/plutonium-create-resource/SKILL.md +1 -1
  6. data/.claude/skills/plutonium-definition/SKILL.md +445 -53
  7. data/.claude/skills/plutonium-definition-actions/SKILL.md +2 -2
  8. data/.claude/skills/plutonium-definition-query/SKILL.md +2 -2
  9. data/.claude/skills/plutonium-forms/SKILL.md +6 -2
  10. data/.claude/skills/plutonium-installation/SKILL.md +3 -3
  11. data/.claude/skills/plutonium-interaction/SKILL.md +3 -3
  12. data/.claude/skills/plutonium-invites/SKILL.md +1 -1
  13. data/.claude/skills/plutonium-model/SKILL.md +228 -55
  14. data/.claude/skills/plutonium-nested-resources/SKILL.md +9 -2
  15. data/.claude/skills/plutonium-package/SKILL.md +3 -3
  16. data/.claude/skills/plutonium-policy/SKILL.md +6 -2
  17. data/.claude/skills/plutonium-portal/SKILL.md +97 -59
  18. data/.claude/skills/plutonium-profile/SKILL.md +2 -2
  19. data/.claude/skills/plutonium-rodauth/SKILL.md +1 -1
  20. data/.claude/skills/plutonium-theming/SKILL.md +1 -1
  21. data/.claude/skills/plutonium-views/SKILL.md +2 -2
  22. data/CHANGELOG.md +25 -0
  23. data/app/assets/plutonium.css +1 -1
  24. data/gemfiles/rails_7.gemfile.lock +3 -3
  25. data/gemfiles/rails_8.0.gemfile.lock +3 -3
  26. data/gemfiles/rails_8.1.gemfile.lock +3 -3
  27. data/lib/generators/pu/invites/install_generator.rb +2 -2
  28. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +7 -7
  29. data/lib/generators/pu/saas/portal_generator.rb +17 -0
  30. data/lib/generators/pu/skills/sync/sync_generator.rb +21 -0
  31. data/lib/plutonium/engine.rb +1 -1
  32. data/lib/plutonium/railtie.rb +1 -1
  33. data/lib/plutonium/ui/form/components/resource_select.rb +1 -1
  34. data/lib/plutonium/ui/form/components/secure_association.rb +2 -2
  35. data/lib/plutonium/ui/form/components/secure_polymorphic_association.rb +6 -11
  36. data/lib/plutonium/version.rb +1 -1
  37. data/package.json +1 -1
  38. data/plutonium.gemspec +1 -1
  39. data/src/css/tokens.css +2 -0
  40. metadata +4 -8
  41. data/.claude/skills/plutonium-connect-resource/SKILL.md +0 -130
  42. data/.claude/skills/plutonium-definition-fields/SKILL.md +0 -535
  43. data/.claude/skills/plutonium-model-features/SKILL.md +0 -286
  44. data/.claude/skills/plutonium-resource/SKILL.md +0 -281
@@ -1,535 +0,0 @@
1
- ---
2
- name: plutonium-definition-fields
3
- description: Configure how resource fields are rendered in forms, show pages, and tables
4
- ---
5
-
6
- # Definition Fields
7
-
8
- Configure how fields are rendered using `field`, `input`, `display`, and `column` declarations.
9
-
10
- ## Core Methods
11
-
12
- | Method | Applies To | Use When |
13
- |--------|-----------|----------|
14
- | `field` | Forms + Show + Table | Universal type override |
15
- | `input` | Forms only | Form-specific options |
16
- | `display` | Show page only | Display-specific options |
17
- | `column` | Table only | Table-specific options |
18
-
19
- ## Basic Usage
20
-
21
- ```ruby
22
- class PostDefinition < ResourceDefinition
23
- # field - changes type everywhere
24
- field :content, as: :markdown
25
-
26
- # input - form-specific
27
- input :title,
28
- label: "Article Title",
29
- hint: "Enter a descriptive title",
30
- placeholder: "e.g. Getting Started"
31
-
32
- # display - show page specific
33
- display :content,
34
- as: :markdown,
35
- description: "Published content",
36
- wrapper: {class: "col-span-full"}
37
-
38
- # column - table specific
39
- column :title, label: "Article", align: :center
40
- column :view_count, align: :end
41
- end
42
- ```
43
-
44
- ## Available Field Types
45
-
46
- ### Input Types (Forms)
47
-
48
- | Category | Types |
49
- |----------|-------|
50
- | **Text** | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
51
- | **Rich Text** | `:markdown` (EasyMDE editor) |
52
- | **Numeric** | `:number`, `:integer`, `:decimal`, `:range` |
53
- | **Boolean** | `:boolean` |
54
- | **Date/Time** | `:date`, `:time`, `:datetime` |
55
- | **Selection** | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
56
- | **Files** | `:file`, `:uppy`, `:attachment` |
57
- | **Associations** | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
58
- | **Special** | `:hidden`, `:color`, `:phone` |
59
-
60
- ### Display Types (Show/Index)
61
-
62
- `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
63
-
64
- ## Field Options
65
-
66
- ### Field-Level Options (wrapper)
67
-
68
- ```ruby
69
- input :title,
70
- label: "Custom Label", # Custom label text
71
- hint: "Help text for forms", # Form help text
72
- placeholder: "Enter value", # Input placeholder
73
- description: "For displays" # Display description
74
- ```
75
-
76
- ### Tag-Level Options (HTML element)
77
-
78
- ```ruby
79
- input :title,
80
- class: "custom-class", # CSS class
81
- data: {controller: "custom"}, # Data attributes
82
- required: true, # HTML required
83
- readonly: true, # HTML readonly
84
- disabled: true # HTML disabled
85
- ```
86
-
87
- ### Wrapper Options
88
-
89
- ```ruby
90
- display :content, wrapper: {class: "col-span-full"}
91
- input :notes, wrapper: {class: "bg-gray-50"}
92
- ```
93
-
94
- ## Select/Choices
95
-
96
- ### Static Choices
97
-
98
- ```ruby
99
- input :category, as: :select, choices: %w[Tech Business Lifestyle]
100
- input :status, as: :select, choices: Post.statuses.keys
101
- ```
102
-
103
- ### Dynamic Choices (requires block)
104
-
105
- ```ruby
106
- # Basic dynamic
107
- input :author do |f|
108
- choices = User.active.pluck(:name, :id)
109
- f.select_tag choices: choices
110
- end
111
-
112
- # With context access
113
- input :team_members do |f|
114
- choices = current_user.organization.users.pluck(:name, :id)
115
- f.select_tag choices: choices
116
- end
117
-
118
- # Based on object state
119
- input :related_posts do |f|
120
- choices = if object.persisted?
121
- Post.where.not(id: object.id).published.pluck(:title, :id)
122
- else
123
- []
124
- end
125
- f.select_tag choices: choices
126
- end
127
- ```
128
-
129
- ## Conditional Rendering
130
-
131
- ```ruby
132
- class PostDefinition < ResourceDefinition
133
- # Show based on object state
134
- display :published_at, condition: -> { object.published? }
135
- display :rejection_reason, condition: -> { object.rejected? }
136
-
137
- # Show based on environment
138
- field :debug_info, condition: -> { Rails.env.development? }
139
- end
140
- ```
141
-
142
- **Note:** Use `condition` for UI state logic. Use **policies** for authorization.
143
-
144
- ## Dynamic Forms (pre_submit)
145
-
146
- Use `pre_submit: true` to create forms that dynamically show/hide fields based on other field values. When a `pre_submit` field changes, the form re-renders server-side and conditions are re-evaluated.
147
-
148
- ### Basic Pattern
149
-
150
- ```ruby
151
- class PostDefinition < ResourceDefinition
152
- # Trigger field - causes form to re-render on change
153
- input :send_notifications, pre_submit: true
154
-
155
- # Dependent field - only shown when condition is true
156
- input :notification_channel,
157
- as: :select,
158
- choices: %w[Email SMS],
159
- condition: -> { object.send_notifications? }
160
- end
161
- ```
162
-
163
- ### How It Works
164
-
165
- 1. User changes a `pre_submit: true` field
166
- 2. Form submits via Turbo (no page reload)
167
- 3. Server re-renders the form with updated `object` state
168
- 4. Fields with `condition` procs are re-evaluated
169
- 5. Newly visible fields appear, hidden fields disappear
170
-
171
- ### Multiple Dependent Fields
172
-
173
- ```ruby
174
- class QuestionDefinition < ResourceDefinition
175
- # Primary selector
176
- input :question_type, as: :select,
177
- choices: %w[text choice scale date boolean],
178
- pre_submit: true
179
-
180
- # Conditional fields based on question_type
181
- input :max_length,
182
- as: :integer,
183
- condition: -> { object.question_type == "text" }
184
-
185
- input :choices,
186
- as: :text,
187
- hint: "One choice per line",
188
- condition: -> { object.question_type == "choice" }
189
-
190
- input :min_value,
191
- as: :integer,
192
- condition: -> { object.question_type == "scale" }
193
-
194
- input :max_value,
195
- as: :integer,
196
- condition: -> { object.question_type == "scale" }
197
- end
198
- ```
199
-
200
- ### Cascading Dependencies
201
-
202
- ```ruby
203
- class PropertyDefinition < ResourceDefinition
204
- # First level
205
- input :property_type, as: :select,
206
- choices: %w[residential commercial],
207
- pre_submit: true
208
-
209
- # Second level - depends on property_type
210
- input :residential_type, as: :select,
211
- choices: %w[apartment house condo],
212
- condition: -> { object.property_type == "residential" },
213
- pre_submit: true
214
-
215
- input :commercial_type, as: :select,
216
- choices: %w[office retail warehouse],
217
- condition: -> { object.property_type == "commercial" },
218
- pre_submit: true
219
-
220
- # Third level - depends on residential_type
221
- input :apartment_floor,
222
- as: :integer,
223
- condition: -> { object.residential_type == "apartment" }
224
- end
225
- ```
226
-
227
- ### Dynamic Choices with pre_submit
228
-
229
- Combine `pre_submit` with block-based dynamic choices:
230
-
231
- ```ruby
232
- class SurveyResponseDefinition < ResourceDefinition
233
- input :category, as: :select,
234
- choices: Category.pluck(:name, :id),
235
- pre_submit: true
236
-
237
- # Choices change based on selected category
238
- input :subcategory do |f|
239
- choices = if object.category.present?
240
- Category.find(object.category).subcategories.pluck(:name, :id)
241
- else
242
- []
243
- end
244
- f.select_tag choices: choices
245
- end
246
- end
247
- ```
248
-
249
- ### Tips
250
-
251
- - Only add `pre_submit: true` to fields that control visibility of other fields
252
- - Keep dependencies simple - deeply nested conditions are hard to debug
253
- - The form submits on change, so avoid `pre_submit` on frequently-changed fields
254
-
255
- ## Custom Rendering
256
-
257
- ### Block Syntax
258
-
259
- **For Display (can return any component):**
260
- ```ruby
261
- display :status do |field|
262
- StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
263
- end
264
-
265
- display :metrics do |field|
266
- if field.value.present?
267
- MetricsChartComponent.new(data: field.value)
268
- else
269
- EmptyStateComponent.new(message: "No metrics")
270
- end
271
- end
272
- ```
273
-
274
- **For Input (must use form builder methods):**
275
- ```ruby
276
- input :birth_date do |f|
277
- case object.age_category
278
- when 'adult'
279
- f.date_tag(min: 18.years.ago.to_date)
280
- when 'minor'
281
- f.date_tag(max: 18.years.ago.to_date)
282
- else
283
- f.date_tag
284
- end
285
- end
286
- ```
287
-
288
- ### phlexi_tag (Advanced Display)
289
-
290
- ```ruby
291
- # With component class
292
- display :status, as: :phlexi_tag, with: StatusBadgeComponent
293
-
294
- # With inline proc
295
- display :priority, as: :phlexi_tag, with: ->(value, attrs) {
296
- case value
297
- when 'high'
298
- span(class: "badge badge-danger") { "High" }
299
- when 'medium'
300
- span(class: "badge badge-warning") { "Medium" }
301
- else
302
- span(class: "badge badge-info") { "Low" }
303
- end
304
- }
305
- ```
306
-
307
- ### Custom Component Class
308
-
309
- ```ruby
310
- input :color_picker, as: ColorPickerComponent
311
- display :chart, as: ChartComponent
312
- ```
313
-
314
- ## Column Options
315
-
316
- ### Alignment
317
-
318
- ```ruby
319
- column :title, align: :start # Left (default)
320
- column :status, align: :center # Center
321
- column :amount, align: :end # Right
322
- ```
323
-
324
- ### Value Formatting
325
-
326
- Use `formatter` for simple value transformations without a full block:
327
-
328
- ```ruby
329
- # Truncate long text
330
- column :description, formatter: ->(value) { value&.truncate(30) }
331
-
332
- # Format numbers
333
- column :price, formatter: ->(value) { "$%.2f" % value if value }
334
-
335
- # Transform values
336
- column :status, formatter: ->(value) { value&.humanize&.upcase }
337
- ```
338
-
339
- The `formatter` option:
340
- - Receives the field value as its argument
341
- - Returns the transformed value for display
342
- - Works with `column` and `display` declarations
343
- - Is simpler than block syntax when you only need to transform the value
344
-
345
- **formatter vs block:** Use `formatter` when you only need the value. Use a block when you need access to the full record:
346
-
347
- ```ruby
348
- # formatter - receives just the value
349
- column :name, formatter: ->(value) { value&.titleize }
350
-
351
- # block - receives the full record
352
- column :full_name do |record|
353
- "#{record.first_name} #{record.last_name}"
354
- end
355
- ```
356
-
357
- ### Custom Column Rendering
358
-
359
- Use a block to customize how a column value is displayed. The block receives the raw record:
360
-
361
- ```ruby
362
- column :price do |record|
363
- "$#{"%.2f" % record.price}" if record.price
364
- end
365
-
366
- column :status do |record|
367
- case record.status
368
- when 'active' then "✓ Active"
369
- when 'pending' then "⏳ Pending"
370
- else record.status.humanize
371
- end
372
- end
373
-
374
- column :description do |record|
375
- record.description&.truncate(50)
376
- end
377
-
378
- column :author do |record|
379
- record.author&.name || "Unknown"
380
- end
381
- ```
382
-
383
- ## Nested Inputs
384
-
385
- Render inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
386
-
387
- ### Model Setup
388
-
389
- ```ruby
390
- class Post < ResourceRecord
391
- has_many :comments
392
- has_one :metadata
393
-
394
- accepts_nested_attributes_for :comments, allow_destroy: true, limit: 10
395
- accepts_nested_attributes_for :metadata, update_only: true
396
- end
397
- ```
398
-
399
- ### Basic Declaration
400
-
401
- ```ruby
402
- class PostDefinition < ResourceDefinition
403
- # Block syntax
404
- nested_input :comments do |n|
405
- n.input :body, as: :text
406
- n.input :author_name
407
- end
408
-
409
- # Using another definition
410
- nested_input :metadata, using: PostMetadataDefinition, fields: %i[seo_title seo_description]
411
- end
412
- ```
413
-
414
- ### Options
415
-
416
- | Option | Description |
417
- |--------|-------------|
418
- | `limit` | Max records (auto-detected from model, default: 10) |
419
- | `allow_destroy` | Show delete checkbox (auto-detected from model) |
420
- | `update_only` | Hide "Add" button, only edit existing |
421
- | `description` | Help text above the section |
422
- | `condition` | Proc to show/hide section |
423
- | `using` | Reference another Definition class |
424
- | `fields` | Which fields to render from the definition |
425
-
426
- ```ruby
427
- nested_input :amenities,
428
- allow_destroy: true,
429
- limit: 20,
430
- description: "Add property amenities" do |n|
431
- n.input :name
432
- n.input :icon, as: :select, choices: ICONS
433
- end
434
- ```
435
-
436
- ### Singular Associations
437
-
438
- For `has_one` and `belongs_to`, limit is automatically 1:
439
-
440
- ```ruby
441
- nested_input :profile do |n| # has_one
442
- n.input :bio
443
- n.input :website
444
- end
445
- ```
446
-
447
- ### Conditional Nested Inputs
448
-
449
- ```ruby
450
- nested_input :shipping_address,
451
- condition: -> { object.requires_shipping? } do |n|
452
- n.input :street
453
- n.input :city
454
- end
455
- ```
456
-
457
- ### How It Works
458
-
459
- 1. Renders a template (hidden) for new records
460
- 2. Renders fieldsets for existing records
461
- 3. Stimulus controller handles Add/Remove
462
- 4. `_destroy` checkbox marks records for deletion
463
- 5. Parameters submitted as `model[association_attributes][id][field]`
464
-
465
- ### Gotchas
466
-
467
- - Model must have `accepts_nested_attributes_for`
468
- - For custom class names, use `class_name:` in both model and `using:` in definition
469
- - `update_only: true` hides the Add button
470
- - Limit is enforced in UI (Add button hidden when reached)
471
-
472
- ## File Uploads
473
-
474
- ```ruby
475
- input :avatar, as: :file
476
- input :avatar, as: :uppy
477
-
478
- input :documents, as: :file, multiple: true
479
- input :documents, as: :uppy,
480
- allowed_file_types: ['.pdf', '.doc'],
481
- max_file_size: 5.megabytes
482
- ```
483
-
484
- ## Common Patterns
485
-
486
- ### Rich Text Content
487
-
488
- ```ruby
489
- field :content, as: :markdown # Form: rich editor
490
- display :content, as: :markdown # Show: rendered markdown
491
- ```
492
-
493
- ### Money Fields
494
-
495
- ```ruby
496
- input :price, as: :decimal, class: "font-mono"
497
- display :price, class: "font-bold text-green-600"
498
- ```
499
-
500
- ### Status Badges
501
-
502
- ```ruby
503
- display :status do |field|
504
- color = case field.value
505
- when 'active' then 'green'
506
- when 'pending' then 'yellow'
507
- else 'gray'
508
- end
509
- span(class: "badge badge-#{color}") { field.value.humanize }
510
- end
511
- ```
512
-
513
- ### Hidden Fields
514
-
515
- ```ruby
516
- field :author_id, as: :hidden
517
- input :tenant_id, as: :hidden
518
- ```
519
-
520
- ## Context in Blocks
521
-
522
- Inside `condition` procs and `input` blocks:
523
- - `object` - The record being edited/displayed
524
- - `current_user` - The authenticated user
525
- - `current_parent` - Parent record for nested resources
526
- - `request`, `params` - Request information
527
- - All helper methods
528
-
529
- ## Related Skills
530
-
531
- - `plutonium-definition` - Overview and structure
532
- - `plutonium-definition-actions` - Actions and interactions
533
- - `plutonium-definition-query` - Search, filters, scopes
534
- - `plutonium-forms` - Custom form templates and field builders
535
- - `plutonium-views` - Custom page and display templates