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.
- checksums.yaml +4 -4
- data/# Plutonium: The pre-alpha demo.md +4 -2
- data/.claude/skills/assets/SKILL.md +416 -0
- data/.claude/skills/connect-resource/SKILL.md +112 -0
- data/.claude/skills/controller/SKILL.md +302 -0
- data/.claude/skills/create-resource/SKILL.md +240 -0
- data/.claude/skills/definition/SKILL.md +218 -0
- data/.claude/skills/definition-actions/SKILL.md +386 -0
- data/.claude/skills/definition-fields/SKILL.md +474 -0
- data/.claude/skills/definition-query/SKILL.md +334 -0
- data/.claude/skills/forms/SKILL.md +439 -0
- data/.claude/skills/installation/SKILL.md +300 -0
- data/.claude/skills/interaction/SKILL.md +382 -0
- data/.claude/skills/model/SKILL.md +267 -0
- data/.claude/skills/model-features/SKILL.md +286 -0
- data/.claude/skills/nested-resources/SKILL.md +274 -0
- data/.claude/skills/package/SKILL.md +191 -0
- data/.claude/skills/policy/SKILL.md +352 -0
- data/.claude/skills/portal/SKILL.md +400 -0
- data/.claude/skills/resource/SKILL.md +281 -0
- data/.claude/skills/rodauth/SKILL.md +452 -0
- data/.claude/skills/views/SKILL.md +563 -0
- data/Appraisals +46 -4
- data/CHANGELOG.md +32 -1
- data/app/assets/plutonium.css +2 -2
- data/config/brakeman.ignore +239 -0
- data/config/initializers/action_policy.rb +1 -1
- data/docs/.vitepress/config.ts +132 -47
- data/docs/concepts/architecture.md +226 -0
- data/docs/concepts/auto-detection.md +254 -0
- data/docs/concepts/index.md +61 -0
- data/docs/concepts/packages-portals.md +304 -0
- data/docs/concepts/resources.md +224 -0
- data/docs/cookbook/blog.md +412 -0
- data/docs/cookbook/index.md +289 -0
- data/docs/cookbook/saas.md +481 -0
- data/docs/getting-started/index.md +56 -0
- data/docs/getting-started/installation.md +146 -0
- data/docs/getting-started/tutorial/01-setup.md +118 -0
- data/docs/getting-started/tutorial/02-first-resource.md +180 -0
- data/docs/getting-started/tutorial/03-authentication.md +246 -0
- data/docs/getting-started/tutorial/04-authorization.md +170 -0
- data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
- data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
- data/docs/getting-started/tutorial/index.md +64 -0
- data/docs/guides/adding-resources.md +420 -0
- data/docs/guides/authentication.md +551 -0
- data/docs/guides/authorization.md +468 -0
- data/docs/guides/creating-packages.md +380 -0
- data/docs/guides/custom-actions.md +523 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/multi-tenancy.md +302 -0
- data/docs/guides/nested-resources.md +411 -0
- data/docs/guides/search-filtering.md +266 -0
- data/docs/guides/theming.md +321 -0
- data/docs/index.md +67 -26
- data/docs/public/CLAUDE.md +64 -21
- data/docs/reference/assets/index.md +496 -0
- data/docs/reference/controller/index.md +363 -0
- data/docs/reference/definition/actions.md +400 -0
- data/docs/reference/definition/fields.md +350 -0
- data/docs/reference/definition/index.md +252 -0
- data/docs/reference/definition/query.md +342 -0
- data/docs/reference/generators/index.md +469 -0
- data/docs/reference/index.md +49 -0
- data/docs/reference/interaction/index.md +445 -0
- data/docs/reference/model/features.md +248 -0
- data/docs/reference/model/index.md +219 -0
- data/docs/reference/policy/index.md +385 -0
- data/docs/reference/portal/index.md +382 -0
- data/docs/reference/views/forms.md +396 -0
- data/docs/reference/views/index.md +479 -0
- data/gemfiles/rails_7.gemfile +9 -2
- data/gemfiles/rails_7.gemfile.lock +146 -111
- data/gemfiles/rails_8.0.gemfile +20 -0
- data/gemfiles/rails_8.0.gemfile.lock +417 -0
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/gemfiles/rails_8.1.gemfile.lock +419 -0
- data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
- data/lib/generators/pu/pkg/portal/USAGE +65 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
- data/lib/generators/pu/res/conn/USAGE +71 -0
- data/lib/generators/pu/res/model/USAGE +106 -110
- data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
- data/lib/generators/pu/res/scaffold/USAGE +85 -0
- data/lib/generators/pu/rodauth/install_generator.rb +2 -6
- data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
- data/lib/generators/pu/skills/sync/USAGE +14 -0
- data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/interaction/base.rb +1 -0
- data/lib/plutonium/package/engine.rb +2 -2
- data/lib/plutonium/query/adhoc_block.rb +6 -2
- data/lib/plutonium/query/model_scope.rb +1 -1
- data/lib/plutonium/railtie.rb +4 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
- data/lib/plutonium/resource/query_object.rb +38 -8
- data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +19 -4
- data/package.json +1 -1
- metadata +76 -39
- data/brakeman.ignore +0 -28
- data/docs/api-examples.md +0 -49
- data/docs/guide/claude-code-guide.md +0 -74
- data/docs/guide/deep-dive/authorization.md +0 -189
- data/docs/guide/deep-dive/multitenancy.md +0 -256
- data/docs/guide/deep-dive/resources.md +0 -390
- data/docs/guide/getting-started/01-installation.md +0 -165
- data/docs/guide/index.md +0 -28
- data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
- data/docs/guide/introduction/02-core-concepts.md +0 -440
- data/docs/guide/tutorial/01-project-setup.md +0 -75
- data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
- data/docs/guide/tutorial/03-defining-resources.md +0 -90
- data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
- data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
- data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
- data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
- data/docs/markdown-examples.md +0 -85
- data/docs/modules/action.md +0 -244
- data/docs/modules/authentication.md +0 -236
- data/docs/modules/configuration.md +0 -599
- data/docs/modules/controller.md +0 -443
- data/docs/modules/core.md +0 -316
- data/docs/modules/definition.md +0 -1308
- data/docs/modules/display.md +0 -759
- data/docs/modules/form.md +0 -495
- data/docs/modules/generator.md +0 -400
- data/docs/modules/index.md +0 -167
- data/docs/modules/interaction.md +0 -642
- data/docs/modules/package.md +0 -151
- data/docs/modules/policy.md +0 -176
- data/docs/modules/portal.md +0 -710
- data/docs/modules/query.md +0 -297
- data/docs/modules/resource_record.md +0 -618
- data/docs/modules/routing.md +0 -690
- data/docs/modules/table.md +0 -301
- data/docs/modules/ui.md +0 -631
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: views
|
|
3
|
+
description: Customizing Plutonium views - pages, forms, displays, tables, and layouts using Phlex
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plutonium Views
|
|
7
|
+
|
|
8
|
+
Plutonium uses [Phlex](https://www.phlex.fun/) for all view components. This provides a Ruby-first approach to building HTML with full IDE support and type safety.
|
|
9
|
+
|
|
10
|
+
## Architecture Overview
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Definition
|
|
14
|
+
├── IndexPage → renders Table
|
|
15
|
+
├── ShowPage → renders Display
|
|
16
|
+
├── NewPage → renders Form
|
|
17
|
+
├── EditPage → renders Form
|
|
18
|
+
└── InteractiveActionPage → renders Form
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Each definition has nested classes you can override:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class PostDefinition < ResourceDefinition
|
|
25
|
+
class IndexPage < IndexPage; end
|
|
26
|
+
class ShowPage < ShowPage; end
|
|
27
|
+
class NewPage < NewPage; end
|
|
28
|
+
class EditPage < EditPage; end
|
|
29
|
+
class Form < Form; end
|
|
30
|
+
class Table < Table; end
|
|
31
|
+
class Display < Display; end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Page Customization
|
|
36
|
+
|
|
37
|
+
### Page Configuration
|
|
38
|
+
|
|
39
|
+
Set page titles and descriptions in definitions:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class PostDefinition < ResourceDefinition
|
|
43
|
+
# Static titles
|
|
44
|
+
index_page_title "Blog Posts"
|
|
45
|
+
index_page_description "Manage all published articles"
|
|
46
|
+
|
|
47
|
+
show_page_title "Article Details"
|
|
48
|
+
new_page_title "Write New Article"
|
|
49
|
+
edit_page_title "Edit Article"
|
|
50
|
+
|
|
51
|
+
# Control breadcrumbs
|
|
52
|
+
breadcrumbs true # Global default
|
|
53
|
+
index_page_breadcrumbs false # Per-page override
|
|
54
|
+
show_page_breadcrumbs true
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Custom Page Class
|
|
59
|
+
|
|
60
|
+
Override page rendering by subclassing:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class PostDefinition < ResourceDefinition
|
|
64
|
+
class ShowPage < ShowPage
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Custom title logic
|
|
68
|
+
def page_title
|
|
69
|
+
"#{object.title} - #{object.author.name}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Add content before the main area
|
|
73
|
+
def render_before_content
|
|
74
|
+
div(class: "alert alert-info") {
|
|
75
|
+
"This post has #{object.comments.count} comments"
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Add content after
|
|
80
|
+
def render_after_content
|
|
81
|
+
render RelatedPostsComponent.new(post: object)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Override the toolbar
|
|
85
|
+
def render_toolbar
|
|
86
|
+
div(class: "flex gap-2") {
|
|
87
|
+
button(class: "btn") { "Preview" }
|
|
88
|
+
button(class: "btn btn-primary") { "Publish" }
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Page Hooks
|
|
96
|
+
|
|
97
|
+
All pages inherit these customization hooks:
|
|
98
|
+
|
|
99
|
+
| Hook | Purpose |
|
|
100
|
+
|------|---------|
|
|
101
|
+
| `render_before_header` | Before entire header section |
|
|
102
|
+
| `render_after_header` | After entire header section |
|
|
103
|
+
| `render_before_breadcrumbs` | Before breadcrumbs |
|
|
104
|
+
| `render_after_breadcrumbs` | After breadcrumbs |
|
|
105
|
+
| `render_before_page_header` | Before title/actions |
|
|
106
|
+
| `render_after_page_header` | After title/actions |
|
|
107
|
+
| `render_before_toolbar` | Before toolbar |
|
|
108
|
+
| `render_after_toolbar` | After toolbar |
|
|
109
|
+
| `render_before_content` | Before main content |
|
|
110
|
+
| `render_after_content` | After main content |
|
|
111
|
+
| `render_before_footer` | Before footer |
|
|
112
|
+
| `render_after_footer` | After footer |
|
|
113
|
+
|
|
114
|
+
### Custom View Files
|
|
115
|
+
|
|
116
|
+
For complete control, create custom ERB view files that replace the default entirely.
|
|
117
|
+
|
|
118
|
+
**File locations:**
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
# Main app (for a PostsController)
|
|
122
|
+
app/views/posts/index.html.erb
|
|
123
|
+
app/views/posts/show.html.erb
|
|
124
|
+
app/views/posts/new.html.erb
|
|
125
|
+
app/views/posts/edit.html.erb
|
|
126
|
+
|
|
127
|
+
# Portal-specific
|
|
128
|
+
packages/admin_portal/app/views/admin_portal/posts/show.html.erb
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Default view structure:**
|
|
132
|
+
|
|
133
|
+
The default views simply render the page class:
|
|
134
|
+
|
|
135
|
+
```erb
|
|
136
|
+
<%# app/views/resource/show.html.erb %>
|
|
137
|
+
<%= render current_definition.show_page_class.new %>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Custom view example:**
|
|
141
|
+
|
|
142
|
+
```erb
|
|
143
|
+
<%# app/views/posts/show.html.erb %>
|
|
144
|
+
<div class="max-w-4xl mx-auto">
|
|
145
|
+
<article class="prose lg:prose-xl">
|
|
146
|
+
<h1><%= resource_record!.title %></h1>
|
|
147
|
+
<div class="meta text-gray-500">
|
|
148
|
+
By <%= resource_record!.author.name %> on <%= resource_record!.created_at.strftime("%B %d, %Y") %>
|
|
149
|
+
</div>
|
|
150
|
+
<%= raw resource_record!.content %>
|
|
151
|
+
</article>
|
|
152
|
+
|
|
153
|
+
<div class="mt-8">
|
|
154
|
+
<%= link_to "Edit", resource_url_for(resource_record!, action: :edit), class: "btn" %>
|
|
155
|
+
<%= link_to "Back", resource_url_for(Post), class: "btn" %>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Mixing approaches:**
|
|
161
|
+
|
|
162
|
+
Render the default page with additions:
|
|
163
|
+
|
|
164
|
+
```erb
|
|
165
|
+
<%# app/views/posts/show.html.erb %>
|
|
166
|
+
<div class="announcement-banner">
|
|
167
|
+
Special announcement here
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<%= render current_definition.show_page_class.new %>
|
|
171
|
+
|
|
172
|
+
<div class="related-posts">
|
|
173
|
+
<%= render partial: "related_posts", locals: { post: resource_record! } %>
|
|
174
|
+
</div>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Form Customization
|
|
178
|
+
|
|
179
|
+
### Custom Form Template
|
|
180
|
+
|
|
181
|
+
Override how fields are rendered:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class PostDefinition < ResourceDefinition
|
|
185
|
+
class Form < Form
|
|
186
|
+
def form_template
|
|
187
|
+
# Custom layout with sections
|
|
188
|
+
div(class: "grid grid-cols-2 gap-6") {
|
|
189
|
+
div {
|
|
190
|
+
h3(class: "text-lg font-medium") { "Basic Info" }
|
|
191
|
+
render_resource_field :title
|
|
192
|
+
render_resource_field :slug
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
div {
|
|
196
|
+
h3(class: "text-lg font-medium") { "Content" }
|
|
197
|
+
render_resource_field :content
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
div(class: "mt-6") {
|
|
202
|
+
h3(class: "text-lg font-medium") { "Publishing" }
|
|
203
|
+
render_resource_field :published_at
|
|
204
|
+
render_resource_field :category
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
render_actions
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Form Methods
|
|
214
|
+
|
|
215
|
+
| Method | Purpose |
|
|
216
|
+
|--------|---------|
|
|
217
|
+
| `render_fields` | Render all permitted fields |
|
|
218
|
+
| `render_resource_field(name)` | Render a single field |
|
|
219
|
+
| `render_actions` | Render submit buttons |
|
|
220
|
+
| `record` | The form object (alias: `object`) |
|
|
221
|
+
| `resource_fields` | List of permitted field names |
|
|
222
|
+
| `resource_definition` | The definition instance |
|
|
223
|
+
|
|
224
|
+
## Display Customization
|
|
225
|
+
|
|
226
|
+
### Custom Display Template
|
|
227
|
+
|
|
228
|
+
Override the show page detail rendering:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
class PostDefinition < ResourceDefinition
|
|
232
|
+
class Display < Display
|
|
233
|
+
def display_template
|
|
234
|
+
# Hero section
|
|
235
|
+
div(class: "bg-gradient-to-r from-blue-500 to-purple-600 p-8 rounded-lg text-white mb-6") {
|
|
236
|
+
h1(class: "text-3xl font-bold") { object.title }
|
|
237
|
+
p(class: "mt-2 opacity-90") { object.excerpt }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Main content
|
|
241
|
+
Block do
|
|
242
|
+
fields_wrapper do
|
|
243
|
+
render_resource_field :author
|
|
244
|
+
render_resource_field :published_at
|
|
245
|
+
render_resource_field :category
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Full-width content
|
|
250
|
+
Block do
|
|
251
|
+
div(class: "prose max-w-none") {
|
|
252
|
+
raw object.content
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Associations (tabs)
|
|
257
|
+
render_associations if present_associations?
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Display Methods
|
|
264
|
+
|
|
265
|
+
| Method | Purpose |
|
|
266
|
+
|--------|---------|
|
|
267
|
+
| `render_fields` | Render all permitted fields in a block |
|
|
268
|
+
| `render_resource_field(name)` | Render single field |
|
|
269
|
+
| `render_associations` | Render association tabs |
|
|
270
|
+
| `object` | The record being displayed |
|
|
271
|
+
| `resource_fields` | List of permitted field names |
|
|
272
|
+
| `resource_associations` | List of permitted associations |
|
|
273
|
+
|
|
274
|
+
## Table Customization
|
|
275
|
+
|
|
276
|
+
### Custom Table Rendering
|
|
277
|
+
|
|
278
|
+
Override list page table:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class PostDefinition < ResourceDefinition
|
|
282
|
+
class Table < Table
|
|
283
|
+
def view_template
|
|
284
|
+
render_search_bar
|
|
285
|
+
render_scopes_bar
|
|
286
|
+
|
|
287
|
+
if collection.empty?
|
|
288
|
+
render_empty_card
|
|
289
|
+
else
|
|
290
|
+
# Custom card grid instead of table
|
|
291
|
+
div(class: "grid grid-cols-3 gap-4") {
|
|
292
|
+
collection.each do |post|
|
|
293
|
+
render PostCardComponent.new(post:)
|
|
294
|
+
end
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
render_footer
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Table Methods
|
|
305
|
+
|
|
306
|
+
| Method | Purpose |
|
|
307
|
+
|--------|---------|
|
|
308
|
+
| `render_search_bar` | Search input |
|
|
309
|
+
| `render_scopes_bar` | Scope tabs |
|
|
310
|
+
| `render_table` | Default table |
|
|
311
|
+
| `render_empty_card` | Empty state |
|
|
312
|
+
| `render_footer` | Pagination |
|
|
313
|
+
| `collection` | The paginated records |
|
|
314
|
+
| `resource_fields` | Column field names |
|
|
315
|
+
|
|
316
|
+
## Component Kit
|
|
317
|
+
|
|
318
|
+
Plutonium provides shorthand methods for common components:
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
class MyPage < Plutonium::UI::Page::Base
|
|
322
|
+
def render_content
|
|
323
|
+
# These are automatically rendered
|
|
324
|
+
PageHeader(title: "Dashboard")
|
|
325
|
+
|
|
326
|
+
Panel(class: "mt-4") {
|
|
327
|
+
p { "Content here" }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Block {
|
|
331
|
+
TabList(items: tabs)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
EmptyCard("No items found")
|
|
335
|
+
|
|
336
|
+
ActionButton(action, url: "/posts/new")
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Available kit methods:
|
|
342
|
+
- `Breadcrumbs()`
|
|
343
|
+
- `PageHeader(title:, description:, actions:)`
|
|
344
|
+
- `Panel(**attrs)`
|
|
345
|
+
- `Block(**attrs)`
|
|
346
|
+
- `TabList(items:)`
|
|
347
|
+
- `EmptyCard(message)`
|
|
348
|
+
- `ActionButton(action, url:)`
|
|
349
|
+
- `DynaFrameHost()` / `DynaFrameContent()`
|
|
350
|
+
- `TableSearchBar()`
|
|
351
|
+
- `TableScopesBar()`
|
|
352
|
+
- `TableInfo(pagy)`
|
|
353
|
+
- `TablePagination(pagy)`
|
|
354
|
+
|
|
355
|
+
## Custom Components
|
|
356
|
+
|
|
357
|
+
### Creating a Phlex Component
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# app/components/post_card_component.rb
|
|
361
|
+
class PostCardComponent < Plutonium::UI::Component::Base
|
|
362
|
+
def initialize(post:)
|
|
363
|
+
@post = post
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def view_template
|
|
367
|
+
div(class: "bg-white rounded-lg shadow p-4") {
|
|
368
|
+
h3(class: "font-bold") { @post.title }
|
|
369
|
+
p(class: "text-gray-600 mt-2") { @post.excerpt }
|
|
370
|
+
|
|
371
|
+
div(class: "mt-4 flex justify-between items-center") {
|
|
372
|
+
span(class: "text-sm text-gray-500") { @post.published_at&.strftime("%B %d, %Y") }
|
|
373
|
+
a(href: resource_url_for(@post), class: "text-blue-600") { "Read more" }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Using in Definitions
|
|
381
|
+
|
|
382
|
+
Reference components in field definitions:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
class PostDefinition < ResourceDefinition
|
|
386
|
+
# Custom display component
|
|
387
|
+
display :status, as: StatusBadgeComponent
|
|
388
|
+
|
|
389
|
+
# Custom input component
|
|
390
|
+
input :color, as: ColorPickerComponent
|
|
391
|
+
|
|
392
|
+
# Block with component
|
|
393
|
+
display :metrics do |field|
|
|
394
|
+
MetricsChartComponent.new(data: field.value)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Layout Customization
|
|
400
|
+
|
|
401
|
+
### Eject Layout
|
|
402
|
+
|
|
403
|
+
Copy the layout template to your project:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
rails generate pu:eject:layout
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
This copies `layouts/resource.html.erb` to your portal.
|
|
410
|
+
|
|
411
|
+
### Custom Layout Class
|
|
412
|
+
|
|
413
|
+
Override the Phlex layout:
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
# packages/admin_portal/app/views/layouts/admin_portal/resource_layout.rb
|
|
417
|
+
module AdminPortal
|
|
418
|
+
class ResourceLayout < Plutonium::UI::Layout::ResourceLayout
|
|
419
|
+
private
|
|
420
|
+
|
|
421
|
+
# Custom body classes
|
|
422
|
+
def body_attributes
|
|
423
|
+
{class: "antialiased bg-slate-100 dark:bg-slate-900"}
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Add custom header content
|
|
427
|
+
def render_before_main
|
|
428
|
+
super
|
|
429
|
+
render AnnouncementBanner.new if Announcement.active.any?
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Custom scripts
|
|
433
|
+
def render_body_scripts
|
|
434
|
+
super
|
|
435
|
+
script(src: "/custom-analytics.js")
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Layout Hooks
|
|
442
|
+
|
|
443
|
+
| Hook | Purpose |
|
|
444
|
+
|------|---------|
|
|
445
|
+
| `render_before_main` | Before main content area |
|
|
446
|
+
| `render_after_main` | After main (modals, etc.) |
|
|
447
|
+
| `render_before_content` | Inside main, before content |
|
|
448
|
+
| `render_after_content` | Inside main, after content |
|
|
449
|
+
| `render_flash` | Flash messages |
|
|
450
|
+
| `render_head` | HTML head section |
|
|
451
|
+
| `render_title` | Page title tag |
|
|
452
|
+
| `render_metatags` | Meta tags |
|
|
453
|
+
| `render_assets` | CSS/JS assets |
|
|
454
|
+
| `render_body_scripts` | Scripts at end of body |
|
|
455
|
+
|
|
456
|
+
## Available Context
|
|
457
|
+
|
|
458
|
+
Both ERB views and Phlex components have access to the same context.
|
|
459
|
+
|
|
460
|
+
### Resource Methods
|
|
461
|
+
|
|
462
|
+
| Method | Description |
|
|
463
|
+
|--------|-------------|
|
|
464
|
+
| `resource_class` | The model class (e.g., `Post`) |
|
|
465
|
+
| `resource_record!` | Current record (raises if not found) |
|
|
466
|
+
| `resource_record?` | Current record (nil if not found) |
|
|
467
|
+
| `current_parent` | Parent record for nested routes |
|
|
468
|
+
| `current_scoped_entity` | Entity for multi-tenant portals |
|
|
469
|
+
|
|
470
|
+
### Definition & Policy
|
|
471
|
+
|
|
472
|
+
| Method | Description |
|
|
473
|
+
|--------|-------------|
|
|
474
|
+
| `current_definition` | Definition instance for current resource |
|
|
475
|
+
| `current_policy` | Policy instance for current record |
|
|
476
|
+
| `current_authorized_scope` | Scoped collection user can access |
|
|
477
|
+
|
|
478
|
+
### Authentication
|
|
479
|
+
|
|
480
|
+
| Method | Description |
|
|
481
|
+
|--------|-------------|
|
|
482
|
+
| `current_user` | Authenticated user (if using Rodauth) |
|
|
483
|
+
|
|
484
|
+
### URL Helpers
|
|
485
|
+
|
|
486
|
+
| Method | Description |
|
|
487
|
+
|--------|-------------|
|
|
488
|
+
| `resource_url_for(record)` | URL for a record |
|
|
489
|
+
| `resource_url_for(record, action: :edit)` | Action URL for record |
|
|
490
|
+
| `resource_url_for(Model)` | Index URL for model |
|
|
491
|
+
| `resource_url_for(Model, action: :new)` | New URL for model |
|
|
492
|
+
| `resource_url_for(record, parent: parent)` | Nested resource URL |
|
|
493
|
+
|
|
494
|
+
### Display Helpers
|
|
495
|
+
|
|
496
|
+
| Method | Description |
|
|
497
|
+
|--------|-------------|
|
|
498
|
+
| `display_name_of(record)` | Human-readable name for record |
|
|
499
|
+
| `resource_name(klass)` | Singular model name |
|
|
500
|
+
| `resource_name_plural(klass)` | Plural model name |
|
|
501
|
+
|
|
502
|
+
### In Phlex Components
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
class MyComponent < Plutonium::UI::Component::Base
|
|
506
|
+
def view_template
|
|
507
|
+
# All the above methods work directly
|
|
508
|
+
current_user
|
|
509
|
+
resource_record!
|
|
510
|
+
resource_url_for(@post)
|
|
511
|
+
|
|
512
|
+
# Rails helpers via helpers proxy
|
|
513
|
+
helpers.link_to(...)
|
|
514
|
+
helpers.image_tag(...)
|
|
515
|
+
helpers.number_to_currency(...)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### In ERB Views
|
|
521
|
+
|
|
522
|
+
```erb
|
|
523
|
+
<%# All methods available directly %>
|
|
524
|
+
<%= resource_record!.title %>
|
|
525
|
+
<%= current_user.name %>
|
|
526
|
+
<%= link_to "Edit", resource_url_for(resource_record!, action: :edit) %>
|
|
527
|
+
|
|
528
|
+
<%# Render Phlex components %>
|
|
529
|
+
<%= render current_definition.show_page_class.new %>
|
|
530
|
+
<%= render MyCustomComponent.new(post: resource_record!) %>
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Portal-Specific Views
|
|
534
|
+
|
|
535
|
+
Each portal can have its own view overrides:
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
# Base definition
|
|
539
|
+
class PostDefinition < ResourceDefinition
|
|
540
|
+
class ShowPage < ShowPage
|
|
541
|
+
# Default behavior
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Admin portal override
|
|
546
|
+
class AdminPortal::PostDefinition < ::PostDefinition
|
|
547
|
+
class ShowPage < ShowPage # Inherits from ::PostDefinition::ShowPage
|
|
548
|
+
def render_after_content
|
|
549
|
+
super
|
|
550
|
+
render AdminOnlySection.new(post: object)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Related Skills
|
|
557
|
+
|
|
558
|
+
- `forms` - Custom form templates and field builders
|
|
559
|
+
- `assets` - TailwindCSS and component theming
|
|
560
|
+
- `definition-fields` - Field/input/display configuration
|
|
561
|
+
- `definition-actions` - Action buttons and interactions
|
|
562
|
+
- `controller` - Presentation hooks (`present_parent?`, etc.)
|
|
563
|
+
- `portal` - Portal-specific customization
|
data/Appraisals
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
appraise "rails-7" do
|
|
2
|
-
gem "rails", "~> 7.
|
|
3
|
-
|
|
4
|
-
gem "sqlite3", "~> 1.4"
|
|
2
|
+
gem "rails", "~> 7.2"
|
|
3
|
+
gem "sqlite3"
|
|
5
4
|
gem "puma", ">= 5.0"
|
|
6
5
|
gem "importmap-rails"
|
|
7
6
|
gem "turbo-rails"
|
|
8
7
|
gem "stimulus-rails"
|
|
8
|
+
gem "propshaft"
|
|
9
|
+
# Rodauth dependencies
|
|
10
|
+
gem "rodauth-rails"
|
|
11
|
+
gem "sequel-activerecord_connection"
|
|
12
|
+
gem "tilt"
|
|
13
|
+
gem "bcrypt"
|
|
14
|
+
gem "rotp"
|
|
15
|
+
gem "rqrcode"
|
|
16
|
+
gem "tzinfo-data", platforms: %i[windows jruby]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
appraise "rails-8.0" do
|
|
20
|
+
gem "rails", "~> 8.0.0"
|
|
21
|
+
gem "sqlite3"
|
|
22
|
+
gem "puma", ">= 5.0"
|
|
23
|
+
gem "importmap-rails"
|
|
24
|
+
gem "turbo-rails"
|
|
25
|
+
gem "stimulus-rails"
|
|
26
|
+
gem "propshaft"
|
|
27
|
+
# Rodauth dependencies
|
|
28
|
+
gem "rodauth-rails"
|
|
29
|
+
gem "sequel-activerecord_connection"
|
|
30
|
+
gem "tilt"
|
|
31
|
+
gem "bcrypt"
|
|
32
|
+
gem "rotp"
|
|
33
|
+
gem "rqrcode"
|
|
34
|
+
gem "tzinfo-data", platforms: %i[windows jruby]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
appraise "rails-8.1" do
|
|
38
|
+
gem "rails", "~> 8.1.0"
|
|
39
|
+
gem "sqlite3"
|
|
40
|
+
gem "puma", ">= 5.0"
|
|
41
|
+
gem "importmap-rails"
|
|
42
|
+
gem "turbo-rails"
|
|
43
|
+
gem "stimulus-rails"
|
|
44
|
+
gem "propshaft"
|
|
45
|
+
# Rodauth dependencies
|
|
46
|
+
gem "rodauth-rails"
|
|
47
|
+
gem "sequel-activerecord_connection"
|
|
48
|
+
gem "tilt"
|
|
49
|
+
gem "bcrypt"
|
|
50
|
+
gem "rotp"
|
|
51
|
+
gem "rqrcode"
|
|
9
52
|
gem "tzinfo-data", platforms: %i[windows jruby]
|
|
10
|
-
# gem "bootsnap", require: false
|
|
11
53
|
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
## [0.
|
|
1
|
+
## [0.34.0] - 2026-01-18
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- *(generators)* Configure default_url_options via RAILS_DEFAULT_URL env var
|
|
6
|
+
- *(generators)* Add pu:skills:sync to install Claude Code skills
|
|
7
|
+
- *(query)* Execute adhoc blocks in controller context
|
|
8
|
+
- *(railtie)* Map ActionPolicy::Unauthorized to 403 Forbidden
|
|
9
|
+
- *(query)* Add default scope support for resource queries
|
|
2
10
|
|
|
3
11
|
### 🐛 Bug Fixes
|
|
4
12
|
|
|
5
13
|
- *(controllers)* Handle turbo_stream format in CRUD and interactive actions
|
|
14
|
+
- *(generators)* Support nullable syntax with type options
|
|
15
|
+
- *(generators)* Prevent overwriting existing url_options configuration
|
|
16
|
+
- *(generators)* Normalize reference names before comparing namespaces
|
|
17
|
+
- *(package)* Include engine migrations in programmatic migration paths
|
|
18
|
+
- *(release)* Handle missing version tag in next_version task
|
|
19
|
+
|
|
20
|
+
### 📚 Documentation
|
|
21
|
+
|
|
22
|
+
- Add Claude Code skills and improve generator documentation
|
|
23
|
+
- Add definition skills and update module documentation
|
|
24
|
+
- Add comprehensive Claude Code skills for resources
|
|
25
|
+
- Add package and portal skills
|
|
26
|
+
- Add interaction skill for business logic actions
|
|
27
|
+
- *(skills)* Add new Claude Code skills and enhance existing ones
|
|
28
|
+
|
|
29
|
+
### 🎨 Styling
|
|
30
|
+
|
|
31
|
+
- Standardrb linting
|
|
32
|
+
|
|
33
|
+
### ⚙️ Miscellaneous Tasks
|
|
34
|
+
|
|
35
|
+
- Overhaul documentation structure and test infrastructure
|
|
36
|
+
- Move brakeman config to config/ and update ignore list
|
|
6
37
|
## [0.33.0] - 2026-01-12
|
|
7
38
|
|
|
8
39
|
### ◀️ Revert
|