plutonium 0.33.1 → 0.34.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -0,0 +1,202 @@
1
+ # Chapter 5: Adding Custom Actions
2
+
3
+ In this chapter, you'll add a "Publish" action to posts using Interactions.
4
+
5
+ ## What are Interactions?
6
+
7
+ Interactions are classes that encapsulate business logic. They're used for:
8
+ - Operations more complex than simple CRUD
9
+ - Actions that need validation beyond the model
10
+ - Operations involving multiple models
11
+ - Business logic you want to reuse
12
+
13
+ ## Creating the Publish Interaction
14
+
15
+ Create an interaction to publish posts:
16
+
17
+ ```ruby
18
+ # packages/blogging/app/interactions/blogging/publish_post.rb
19
+ class Blogging::PublishPost < Blogging::ResourceInteraction
20
+ # Presentation
21
+ presents label: "Publish Post",
22
+ icon: Phlex::TablerIcons::Send
23
+
24
+ # Having `attribute :resource` makes this a record action
25
+ # (shows on individual records and table rows)
26
+ attribute :resource
27
+
28
+ # Validation
29
+ validate :post_not_already_published
30
+
31
+ private
32
+
33
+ def execute
34
+ resource.update!(published: true, published_at: Time.current)
35
+
36
+ succeed(resource)
37
+ .with_message("Post published successfully!")
38
+ end
39
+
40
+ def post_not_already_published
41
+ if resource.published?
42
+ errors.add(:base, "Post is already published")
43
+ end
44
+ end
45
+ end
46
+ ```
47
+
48
+ ## Registering the Action
49
+
50
+ Add the action to the Post definition:
51
+
52
+ ```ruby
53
+ # packages/blogging/app/definitions/blogging/post_definition.rb
54
+ class Blogging::PostDefinition < Blogging::ResourceDefinition
55
+ # Register the publish action
56
+ action :publish, interaction: Blogging::PublishPost
57
+ end
58
+ ```
59
+
60
+ Action placement is automatically determined:
61
+ - `attribute :resource` → shows on records and table rows
62
+ - `attribute :resources` → bulk action for selected records
63
+ - Neither → resource-level action (like "New" button)
64
+
65
+ ## Authorizing the Action
66
+
67
+ Add permission for the action in the policy:
68
+
69
+ ```ruby
70
+ # packages/blogging/app/policies/blogging/post_policy.rb
71
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
72
+ # ... existing permissions ...
73
+
74
+ def publish?
75
+ owner? && !record.published?
76
+ end
77
+ end
78
+ ```
79
+
80
+ ## Testing the Action
81
+
82
+ 1. Create an unpublished post
83
+ 2. View the post details
84
+ 3. Click the "Publish" action button
85
+ 4. The post is now published
86
+
87
+ ## Actions with User Input
88
+
89
+ Let's create a more complex action - scheduling publication:
90
+
91
+ ```ruby
92
+ # packages/blogging/app/interactions/blogging/schedule_post.rb
93
+ class Blogging::SchedulePost < Blogging::ResourceInteraction
94
+ presents label: "Schedule Publication",
95
+ icon: Phlex::TablerIcons::Calendar
96
+
97
+ attribute :resource
98
+ attribute :publish_at, :datetime
99
+
100
+ # Define form input
101
+ input :publish_at, as: :datetime
102
+
103
+ # Validations
104
+ validates :publish_at, presence: true
105
+ validate :publish_at_in_future
106
+
107
+ private
108
+
109
+ def execute
110
+ resource.update!(
111
+ scheduled_at: publish_at,
112
+ published: false
113
+ )
114
+
115
+ succeed(resource)
116
+ .with_message("Post scheduled for #{publish_at.strftime('%B %d, %Y at %I:%M %p')}")
117
+ end
118
+
119
+ def publish_at_in_future
120
+ if publish_at.present? && publish_at <= Time.current
121
+ errors.add(:publish_at, "must be in the future")
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ Register it:
128
+
129
+ ```ruby
130
+ # In PostDefinition
131
+ action :schedule, interaction: Blogging::SchedulePost
132
+ ```
133
+
134
+ Because the interaction defines an `input`, users see a form to select the publication date.
135
+
136
+ ## Resource-Level Actions
137
+
138
+ Actions can operate at the resource level (not on a specific record):
139
+
140
+ ```ruby
141
+ # packages/blogging/app/interactions/blogging/import_posts.rb
142
+ class Blogging::ImportPosts < Blogging::ResourceInteraction
143
+ presents label: "Import Posts",
144
+ icon: Phlex::TablerIcons::Upload
145
+
146
+ attribute :file
147
+
148
+ input :file, as: :file
149
+
150
+ validates :file, presence: true
151
+
152
+ private
153
+
154
+ def execute
155
+ # Process CSV file...
156
+ succeed(nil).with_message("Posts imported successfully")
157
+ end
158
+ end
159
+ ```
160
+
161
+ Register it:
162
+
163
+ ```ruby
164
+ action :import, interaction: Blogging::ImportPosts
165
+ ```
166
+
167
+ Since `ImportPosts` has no `attribute :resource` or `attribute :resources`, it automatically becomes a resource-level action.
168
+
169
+ ## Action Placement
170
+
171
+ For **interactive actions**, placement is auto-determined from attributes:
172
+
173
+ | Attribute | Placement |
174
+ |-----------|-----------|
175
+ | `attribute :resource` | Record show page + table rows |
176
+ | `attribute :resources` | Bulk action (selected records) |
177
+ | Neither | Resource-level (like "New" button) |
178
+
179
+ You can override with explicit options if needed:
180
+
181
+ | Option | Location |
182
+ |--------|----------|
183
+ | `record_action:` | Record show page |
184
+ | `collection_record_action:` | Table row actions |
185
+ | `resource_action:` | Above the table |
186
+
187
+ ## Action Styling
188
+
189
+ Customize action appearance:
190
+
191
+ ```ruby
192
+ action :archive,
193
+ interaction: ArchivePost,
194
+ category: :danger, # red styling
195
+ confirmation: "Are you sure?" # confirmation dialog
196
+ ```
197
+
198
+ ## What's Next
199
+
200
+ We have posts with custom actions. In the next chapter, we'll add Comments as a nested resource.
201
+
202
+ [Continue to Chapter 6: Nested Resources →](./06-nested-resources)
@@ -0,0 +1,147 @@
1
+ # Chapter 6: Nested Resources
2
+
3
+ In this chapter, you'll add Comments as a nested resource under Posts.
4
+
5
+ ## What are Nested Resources?
6
+
7
+ Nested resources are resources that belong to a parent resource. In our blog:
8
+ - Comments belong to Posts
9
+ - The URL reflects this: `/admin/blogging/posts/1/blogging/comments`
10
+ - Comments are automatically scoped to their parent post
11
+
12
+ ## Generating the Comment Resource
13
+
14
+ ```bash
15
+ rails generate pu:res:scaffold Comment body:text user:belongs_to Blogging/Post:belongs_to --dest=blogging
16
+ ```
17
+
18
+ ## Setting Up the Association
19
+
20
+ Update the Post model to add the `has_many` association:
21
+
22
+ ```ruby
23
+ # packages/blogging/app/models/blogging/post.rb
24
+ class Blogging::Post < Blogging::ResourceRecord
25
+ belongs_to :user
26
+ has_many :comments, foreign_key: :post_id, dependent: :destroy
27
+ # ... existing code
28
+ end
29
+ ```
30
+
31
+ ## The Comment Model
32
+
33
+ The generator creates:
34
+
35
+ ```ruby
36
+ # packages/blogging/app/models/blogging/comment.rb
37
+ class Blogging::Comment < Blogging::ResourceRecord
38
+ belongs_to :user
39
+ belongs_to :post, class_name: "Blogging::Post"
40
+ # ... generated code
41
+ end
42
+ ```
43
+
44
+ Note: When referencing resources within the same package, the generator uses the short name (`:post`) while setting the appropriate `class_name` and foreign key to the correct table.
45
+
46
+ ## Connecting to the Portal
47
+
48
+ Connect comments to the admin portal:
49
+
50
+ ```bash
51
+ rails generate pu:res:conn Blogging::Comment --dest=admin_portal
52
+ ```
53
+
54
+ Because `Comment` has `belongs_to :post`, Plutonium automatically creates nested routes:
55
+ - `GET /admin/blogging/posts/:blogging_post_id/blogging/comments`
56
+ - `POST /admin/blogging/posts/:blogging_post_id/blogging/comments`
57
+ - `GET /admin/blogging/posts/:blogging_post_id/blogging/comments/:id`
58
+
59
+ ## Showing Comments on Post Detail
60
+
61
+ To show a comments panel on the post detail page, add `comments` to `permitted_associations` in the policy:
62
+
63
+ ```ruby
64
+ # packages/blogging/app/policies/blogging/post_policy.rb
65
+ class Blogging::PostPolicy < Blogging::ResourcePolicy
66
+ def permitted_associations
67
+ %i[user comments]
68
+ end
69
+ end
70
+ ```
71
+
72
+ The panel links to the nested comments route and shows "Add Comment" if the user has permission.
73
+
74
+ ## Comment Policy
75
+
76
+ Add authorization for comments:
77
+
78
+ ```ruby
79
+ # packages/blogging/app/policies/blogging/comment_policy.rb
80
+ class Blogging::CommentPolicy < Blogging::ResourcePolicy
81
+ def read?
82
+ true # Anyone can read comments
83
+ end
84
+
85
+ def create?
86
+ true # Anyone authenticated can comment
87
+ end
88
+
89
+ def update?
90
+ owner?
91
+ end
92
+
93
+ def destroy?
94
+ owner? || post_owner?
95
+ end
96
+
97
+ # Scope to comments on published posts (or user's own posts)
98
+ def relation_scope(relation)
99
+ relation.joins(:post).where(
100
+ blogging_posts: {published: true}
101
+ ).or(
102
+ relation.where(user_id: user.id)
103
+ )
104
+ end
105
+
106
+ private
107
+
108
+ def owner?
109
+ record.user_id == user.id
110
+ end
111
+
112
+ def post_owner?
113
+ record.post.user_id == user.id
114
+ end
115
+ end
116
+ ```
117
+
118
+ ## Nested Forms
119
+
120
+ You can edit comments directly on the post form using nested attributes:
121
+
122
+ ```ruby
123
+ # In Post model
124
+ accepts_nested_attributes_for :comments, allow_destroy: true
125
+ ```
126
+
127
+ ```ruby
128
+ # In PostDefinition - option 1: inline block
129
+ nested_input :comments do |definition|
130
+ definition.input :body
131
+ end
132
+
133
+ # In PostDefinition - option 2: use existing definition
134
+ nested_input :comments, using: Blogging::CommentDefinition, fields: %i[body]
135
+ ```
136
+
137
+ This adds an inline comments editor to the post form.
138
+
139
+ ## Nesting Limitations
140
+
141
+ Plutonium supports one level of nesting. For deeper hierarchies (e.g., Replies to Comments), keep URLs flat: `/blogging/comments/:blogging_comment_id/blogging/replies`
142
+
143
+ ## What's Next
144
+
145
+ Our blog has posts and comments. In the final chapter, we'll customize the UI to make it look polished.
146
+
147
+ [Continue to Chapter 7: Customizing the UI →](./07-customizing-ui)
@@ -0,0 +1,254 @@
1
+ # Chapter 7: Customizing the UI
2
+
3
+ In this chapter, you'll customize forms, tables, and pages to create a polished interface.
4
+
5
+ ## Customizing Fields
6
+
7
+ Fields control how attributes appear in forms and displays. Plutonium auto-infers fields from your model, so you only need to declare fields when customizing their behavior.
8
+
9
+ ### Field Types
10
+
11
+ ```ruby
12
+ # packages/blogging/app/definitions/blogging/post_definition.rb
13
+ class Blogging::PostDefinition < Blogging::ResourceDefinition
14
+ # Rich text editor instead of plain textarea
15
+ field :body, as: :rich_text
16
+
17
+ # Select with predefined options
18
+ input :status, as: :select, choices: %w[draft review published]
19
+ end
20
+ ```
21
+
22
+ ### Conditional Fields
23
+
24
+ Show fields based on conditions:
25
+
26
+ ```ruby
27
+ # Only show published_at when published is true
28
+ field :published_at, condition: -> { object.published? }
29
+
30
+ # Show different fields for new vs existing records
31
+ field :author, condition: :new_record?
32
+ ```
33
+
34
+ ## Customizing Tables
35
+
36
+ Columns are auto-inferred from your model. Only declare columns when customizing their behavior.
37
+
38
+ ### Column Configuration
39
+
40
+ ```ruby
41
+ # Custom label and sortable
42
+ column :user, label: "Author", sortable: true
43
+
44
+ # Computed column with block
45
+ column :comment_count do |post|
46
+ post.comments.count
47
+ end
48
+ ```
49
+
50
+ ### Table Actions
51
+
52
+ ```ruby
53
+ # Show page actions
54
+ action :publish, interaction: Blogging::PublishPost, record_action: true
55
+
56
+ # Table row actions
57
+ action :archive, interaction: Blogging::ArchivePost, collection_record_action: true
58
+
59
+ # Index page actions
60
+ action :import, interaction: Blogging::ImportPosts, resource_action: true
61
+
62
+ # Bulk actions (selected records)
63
+ action :bulk_publish, interaction: Blogging::BulkPublish, bulk_action: true
64
+ ```
65
+
66
+ ## Customizing Search and Filters
67
+
68
+ ```ruby
69
+ # Search configuration
70
+ search do |scope, query|
71
+ scope.where("title ILIKE ? OR body ILIKE ?", "%#{query}%", "%#{query}%")
72
+ end
73
+
74
+ # Predefined scopes (reference model scopes)
75
+ scope :published, default: true # Applied by default, uses Post.published
76
+ scope :drafts # Uses Post.draft
77
+
78
+ # Inline scope with block
79
+ scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
80
+
81
+ # Inline scope with controller context
82
+ scope(:mine) { |scope| scope.where(user: current_user) }
83
+
84
+ # Filters
85
+ filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
86
+ filter :status, with: Plutonium::Query::Filters::Text, predicate: :eq
87
+
88
+ # Custom filter with lambda
89
+ filter :published, with: ->(scope, value) {
90
+ value == "true" ? scope.where.not(published_at: nil) : scope.where(published_at: nil)
91
+ }
92
+
93
+ # Sorting options
94
+ sort :title
95
+ sort :created_at
96
+ sort :published
97
+
98
+ # Default sort
99
+ default_sort :created_at, :desc
100
+ ```
101
+
102
+ ## Custom Page Classes
103
+
104
+ Override page title and description in definitions:
105
+
106
+ ```ruby
107
+ class Blogging::PostDefinition < Blogging::ResourceDefinition
108
+ # Custom page titles
109
+ index_page_title "Blog Posts"
110
+ index_page_description "Manage your blog content"
111
+
112
+ show_page_title { |record| record.title }
113
+ show_page_description "View post details"
114
+ end
115
+ ```
116
+
117
+ For more advanced customization, you can create custom page classes that inherit from Plutonium's page components:
118
+
119
+ ```ruby
120
+ # packages/admin_portal/app/views/admin_portal/blogging/posts/index_page.rb
121
+ class AdminPortal::Blogging::Posts::IndexPage < Blogging::PostDefinition::IndexPage
122
+ private
123
+
124
+ def page_title
125
+ "Blog Posts"
126
+ end
127
+
128
+ def page_description
129
+ "Manage your blog content"
130
+ end
131
+
132
+ # Add content after the page header
133
+ def render_after_page_header
134
+ div(class: "mb-4 p-4 bg-blue-50 rounded") do
135
+ p { "Custom content here" }
136
+ end
137
+ end
138
+ end
139
+ ```
140
+
141
+ ## Custom Form Layout
142
+
143
+ Control form layout using wrapper options in definitions:
144
+
145
+ ```ruby
146
+ class Blogging::PostDefinition < Blogging::ResourceDefinition
147
+ # Full-width fields
148
+ input :title, wrapper: {class: "col-span-full"}
149
+ input :body, as: :rich_text, wrapper: {class: "col-span-full"}
150
+
151
+ # Side-by-side fields (default is col-span-full)
152
+ input :published_at, wrapper: {class: "col-span-1"}
153
+ input :category, wrapper: {class: "col-span-1"}
154
+ end
155
+ ```
156
+
157
+ For advanced form customization, use the block syntax:
158
+
159
+ ```ruby
160
+ input :birth_date do |f|
161
+ f.date_tag(min: 18.years.ago.to_date)
162
+ end
163
+ ```
164
+
165
+ ## Theming with TailwindCSS
166
+
167
+ Plutonium uses TailwindCSS 4. Customize the theme:
168
+
169
+ ```css
170
+ /* app/assets/stylesheets/application.css */
171
+ @import "tailwindcss";
172
+ @import "gem:plutonium/src/css/plutonium.css";
173
+
174
+ @theme {
175
+ --color-primary-500: #6366f1; /* Indigo */
176
+ --color-primary-600: #4f46e5;
177
+ --color-primary-700: #4338ca;
178
+
179
+ --radius-md: 0.5rem;
180
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
181
+ }
182
+ ```
183
+
184
+ ## Custom Components
185
+
186
+ Create reusable components with Phlex:
187
+
188
+ ```ruby
189
+ # app/components/status_badge.rb
190
+ class StatusBadge < Plutonium::UI::Component::Base
191
+ def initialize(published:)
192
+ @published = published
193
+ end
194
+
195
+ def view_template
196
+ if @published
197
+ span(class: "px-2 py-1 text-xs bg-green-100 text-green-800 rounded") { "Published" }
198
+ else
199
+ span(class: "px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded") { "Draft" }
200
+ end
201
+ end
202
+ end
203
+
204
+ # Use in definition
205
+ column :status do |post|
206
+ render StatusBadge.new(published: post.published?)
207
+ end
208
+ ```
209
+
210
+ ## Layout Customization
211
+
212
+ Layouts are Phlex components that wrap page content. The base layout provides hooks for customization:
213
+
214
+ ```ruby
215
+ class CustomLayout < Plutonium::UI::Layout::ResourceLayout
216
+ private
217
+
218
+ # Customize body classes
219
+ def body_attributes
220
+ {class: "antialiased min-h-screen bg-white dark:bg-gray-900"}
221
+ end
222
+
223
+ # Add content before the main section
224
+ def render_before_main
225
+ super
226
+ # Add custom header content
227
+ end
228
+
229
+ # Add content after the main section
230
+ def render_after_main
231
+ super
232
+ # Add custom footer content
233
+ end
234
+ end
235
+ ```
236
+
237
+ See the [Theming Guide](/guides/theming) for comprehensive customization options.
238
+
239
+ ## What's Next
240
+
241
+ Congratulations! You've built a complete blog application with:
242
+ - Resource CRUD operations
243
+ - Authentication with Rodauth
244
+ - Authorization with policies
245
+ - Custom actions with Interactions
246
+ - Nested resources
247
+ - Customized UI
248
+
249
+ Continue exploring:
250
+ - [Guides](/guides/) - Deep dives on specific topics
251
+ - [Reference](/reference/) - Complete API documentation
252
+ - [Cookbook](/cookbook/) - Real-world recipes
253
+
254
+ Happy building with Plutonium!
@@ -0,0 +1,64 @@
1
+ # Tutorial: Building a Blog
2
+
3
+ In this tutorial, you'll build a complete blog application with Plutonium. You'll learn:
4
+
5
+ - How to structure a Plutonium application
6
+ - Creating resources with models, definitions, and policies
7
+ - Setting up authentication with Rodauth
8
+ - Implementing authorization rules
9
+ - Adding custom actions with Interactions
10
+ - Customizing the UI
11
+
12
+ ## What We'll Build
13
+
14
+ A blog application with:
15
+ - **Posts** - Articles with title, body, and publication status
16
+ - **Comments** - Nested under posts
17
+ - **Users** - Authors who can manage their own posts
18
+ - **Admin Portal** - Full access for administrators
19
+ - **Author Portal** - Limited access for content authors
20
+
21
+ ## Prerequisites
22
+
23
+ - Ruby 3.2+
24
+ - Rails 8.0+ (or 7.1+)
25
+ - Node.js 18+
26
+ - PostgreSQL (or SQLite for development)
27
+
28
+ ## Time Required
29
+
30
+ This tutorial takes approximately 45-60 minutes to complete.
31
+
32
+ ## Chapters
33
+
34
+ ### [1. Project Setup](./01-setup)
35
+ Create a new Plutonium application and understand the project structure.
36
+
37
+ ### [2. Creating Your First Resource](./02-first-resource)
38
+ Generate the Post resource with model, definition, policy, and controller.
39
+
40
+ ### [3. Setting Up Authentication](./03-authentication)
41
+ Configure Rodauth for user authentication with multiple account types.
42
+
43
+ ### [4. Implementing Authorization](./04-authorization)
44
+ Add policies to control who can view, create, edit, and delete posts.
45
+
46
+ ### [5. Adding Custom Actions](./05-custom-actions)
47
+ Create a "Publish" action using Interactions for business logic.
48
+
49
+ ### [6. Nested Resources](./06-nested-resources)
50
+ Add Comments as a nested resource under Posts.
51
+
52
+ ### [7. Customizing the UI](./07-customizing-ui)
53
+ Customize forms, tables, and views to match your requirements.
54
+
55
+ ## Getting Help
56
+
57
+ If you get stuck:
58
+ - Check the [Guides](/guides/) for detailed explanations
59
+ - Browse the [Reference Documentation](/reference/) for API details
60
+ - Visit our [GitHub Issues](https://github.com/radioactive-labs/plutonium-core/issues)
61
+
62
+ Let's get started!
63
+
64
+ [Begin Chapter 1: Project Setup →](./01-setup)