plutonium 0.42.0 → 0.43.1

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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-controller/SKILL.md +38 -1
  3. data/.claude/skills/plutonium-definition/SKILL.md +14 -0
  4. data/.claude/skills/plutonium-forms/SKILL.md +16 -1
  5. data/.claude/skills/plutonium-profile/SKILL.md +276 -0
  6. data/.claude/skills/plutonium-views/SKILL.md +23 -1
  7. data/CHANGELOG.md +42 -0
  8. data/app/assets/plutonium.css +2 -2
  9. data/app/views/plutonium/_resource_header.html.erb +6 -27
  10. data/app/views/plutonium/_resource_sidebar.html.erb +1 -2
  11. data/app/views/resource/_resource_details.rabl +3 -2
  12. data/app/views/resource/index.rabl +3 -2
  13. data/app/views/resource/show.rabl +3 -2
  14. data/docs/guides/user-profile.md +322 -0
  15. data/docs/reference/controller/index.md +38 -1
  16. data/docs/reference/definition/index.md +16 -0
  17. data/docs/reference/views/forms.md +15 -0
  18. data/docs/reference/views/index.md +23 -1
  19. data/gemfiles/rails_7.gemfile.lock +1 -1
  20. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  21. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  22. data/lib/generators/pu/core/assets/assets_generator.rb +12 -0
  23. data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +11 -0
  24. data/lib/generators/pu/core/typespec/templates/common.tsp.tt +95 -0
  25. data/lib/generators/pu/core/typespec/templates/main.tsp.tt +27 -0
  26. data/lib/generators/pu/core/typespec/templates/main_multi.tsp.tt +25 -0
  27. data/lib/generators/pu/core/typespec/templates/model.tsp.tt +226 -0
  28. data/lib/generators/pu/core/typespec/typespec_generator.rb +342 -0
  29. data/lib/generators/pu/invites/USAGE +0 -1
  30. data/lib/generators/pu/invites/install_generator.rb +62 -15
  31. data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +2 -2
  32. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +2 -0
  33. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +1 -0
  34. data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +5 -5
  35. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +4 -4
  36. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +1 -1
  37. data/lib/generators/pu/lib/plutonium_generators/generator.rb +29 -0
  38. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +6 -23
  39. data/lib/generators/pu/pkg/portal/portal_generator.rb +5 -1
  40. data/lib/generators/pu/profile/USAGE +59 -0
  41. data/lib/generators/pu/profile/concerns/profile_arguments.rb +27 -0
  42. data/lib/generators/pu/profile/conn/USAGE +33 -0
  43. data/lib/generators/pu/profile/conn_generator.rb +167 -0
  44. data/lib/generators/pu/profile/install_generator.rb +119 -0
  45. data/lib/generators/pu/profile/setup/USAGE +42 -0
  46. data/lib/generators/pu/profile/setup_generator.rb +73 -0
  47. data/lib/generators/pu/rodauth/account_generator.rb +2 -4
  48. data/lib/generators/pu/rodauth/install_generator.rb +2 -2
  49. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
  50. data/lib/generators/pu/saas/api_client_generator.rb +0 -2
  51. data/lib/generators/pu/saas/membership_generator.rb +68 -19
  52. data/lib/generators/pu/saas/setup_generator.rb +7 -2
  53. data/lib/generators/pu/saas/user_generator.rb +0 -2
  54. data/lib/plutonium/auth/rodauth.rb +8 -0
  55. data/lib/plutonium/core/controller.rb +7 -4
  56. data/lib/plutonium/core/controllers/authorizable.rb +5 -1
  57. data/lib/plutonium/definition/base.rb +7 -0
  58. data/lib/plutonium/helpers/display_helper.rb +6 -0
  59. data/lib/plutonium/profile/security_section.rb +118 -0
  60. data/lib/plutonium/resource/controller.rb +17 -7
  61. data/lib/plutonium/resource/controllers/interactive_actions.rb +11 -25
  62. data/lib/plutonium/resource/controllers/presentable.rb +46 -3
  63. data/lib/plutonium/resource/record/associated_with.rb +7 -1
  64. data/lib/plutonium/routing/mapper_extensions.rb +18 -18
  65. data/lib/plutonium/routing/route_set_extensions.rb +23 -2
  66. data/lib/plutonium/ui/breadcrumbs.rb +111 -131
  67. data/lib/plutonium/ui/dyna_frame/content.rb +12 -2
  68. data/lib/plutonium/ui/form/resource.rb +26 -19
  69. data/lib/plutonium/ui/page/base.rb +14 -14
  70. data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -74
  71. data/lib/plutonium/ui/table/components/selection_column.rb +6 -2
  72. data/lib/plutonium/ui/table/resource.rb +3 -2
  73. data/lib/plutonium/version.rb +1 -1
  74. data/lib/tasks/release.rake +6 -6
  75. data/package.json +1 -1
  76. metadata +17 -3
  77. data/lib/generators/pu/rodauth/concerns/gem_helpers.rb +0 -19
@@ -3,39 +3,18 @@
3
3
  <%= resource_logo_tag(classname: "h-10 rounded-md") %>
4
4
  <% end %>
5
5
 
6
- <% header.with_action do %>
7
- <%=
8
- render Plutonium::UI::NavGridMenu.new(label: "Apps", icon: Phlex::TablerIcons::Apps) do |menu|
9
- menu.with_item(name: "Dashboard", icon: Phlex::TablerIcons::Dashboard, href: "#")
10
- menu.with_item(name: "Settings", icon: Phlex::TablerIcons::Settings, href: "#")
11
- menu.with_item(name: "Profile", icon: Phlex::TablerIcons::User, href: "#")
12
- end
13
- %>
14
- <% end %>
15
-
16
6
  <% header.with_action do %>
17
7
  <%=
18
8
  render Plutonium::UI::NavUser.new(
19
9
  name: nil,
20
10
  email: current_user.try(:email) || current_user
21
11
  ) do |nav|
22
- nav.with_section do |section|
23
- section.with_link(label: "My profile", href: "#")
24
- section.with_link(label: "Account settings", href: "#")
25
- end
26
-
27
- nav.with_section do |section|
28
- section.with_link(label: "My likes", href: "#") do |link|
29
- link.with_leading do
30
- render Phlex::TablerIcons::Heart.new(class: "mr-2 text-gray-400 w-4 h-4")
31
- end
32
- end
33
- section.with_link(label: "Pro version", href: "#") do |link|
34
- link.with_leading do
35
- render Phlex::TablerIcons::Flame.new(class: "mr-2 text-primary-600 dark:text-primary-500 w-4 h-4")
36
- end
37
- link.with_trailing do
38
- render Phlex::TablerIcons::CaretRight.new(class: "w-3 h-3")
12
+ if try(:profile_url)
13
+ nav.with_section do |section|
14
+ section.with_link(label: "Profile", href: profile_url) do |link|
15
+ link.with_leading do
16
+ render Phlex::TablerIcons::User.new(class: "mr-2 text-gray-400 w-4 h-4")
17
+ end
39
18
  end
40
19
  end
41
20
  end
@@ -8,8 +8,7 @@
8
8
 
9
9
  m.item "Resources", icon: Phlex::TablerIcons::GridDots do |n|
10
10
  registered_resources.each do |resource|
11
- n.item resource.model_name.human.pluralize,
12
- url: resource_url_for(resource, parent: nil)
11
+ n.item resource_label(resource), url: resource_url_for(resource, parent: nil)
13
12
  end
14
13
  end
15
14
  end
@@ -1,5 +1,6 @@
1
- attributes :id
2
- attributes :created_at, :updated_at
1
+ attributes resource_class.primary_key.to_sym
2
+ attributes :created_at if resource_class.column_names.include?("created_at")
3
+ attributes :updated_at if resource_class.column_names.include?("updated_at")
3
4
 
4
5
  node(:sgid) { |resource| resource.to_signed_global_id.to_s }
5
6
 
@@ -1,7 +1,8 @@
1
1
  collection @resource_records, root: resource_class.to_s.demodulize.underscore.pluralize.to_sym, object_root: false
2
2
 
3
- attributes :id
4
- attributes :created_at, :updated_at
3
+ attributes resource_class.primary_key.to_sym
4
+ attributes :created_at if resource_class.column_names.include?("created_at")
5
+ attributes :updated_at if resource_class.column_names.include?("updated_at")
5
6
 
6
7
  node(:sgid) { |resource| resource.to_signed_global_id.to_s }
7
8
 
@@ -1,7 +1,8 @@
1
1
  object @resource_record
2
2
 
3
- attributes :id
4
- attributes :created_at, :updated_at
3
+ attributes resource_class.primary_key.to_sym
4
+ attributes :created_at if resource_class.column_names.include?("created_at")
5
+ attributes :updated_at if resource_class.column_names.include?("updated_at")
5
6
 
6
7
  node(:sgid) { |resource| resource.to_signed_global_id.to_s }
7
8
 
@@ -0,0 +1,322 @@
1
+ # User Profile
2
+
3
+ Plutonium provides a Profile resource generator for user account settings. The Profile page allows users to manage their personal information and access Rodauth security features like password changes, two-factor authentication, and session management.
4
+
5
+ ## Overview
6
+
7
+ The profile system provides:
8
+ - **Profile Resource**: A standard Plutonium resource linked to the User model
9
+ - **Security Section**: Automatic display of enabled Rodauth security features
10
+ - **Customizable Fields**: Add any fields you need (bio, avatar, preferences, etc.)
11
+
12
+ ## Prerequisites
13
+
14
+ Before installing the profile, ensure you have:
15
+
16
+ 1. **User Authentication**: A Rodauth user account set up
17
+ 2. **Model Markers**: The User model with marker comments
18
+
19
+ The easiest way to set this up is with the SaaS generator:
20
+
21
+ ```bash
22
+ rails g pu:saas:user User
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ ### Step 1: Install the Profile Resource
28
+
29
+ ```bash
30
+ rails generate pu:profile:install --dest=main_app
31
+ ```
32
+
33
+ With custom fields:
34
+
35
+ ```bash
36
+ rails g pu:profile:install \
37
+ bio:text \
38
+ avatar:attachment \
39
+ 'timezone:string?' \
40
+ notifications_enabled:boolean \
41
+ --dest=main_app
42
+ ```
43
+
44
+ ### Options
45
+
46
+ | Option | Default | Description |
47
+ |--------|---------|-------------|
48
+ | `--dest` | (prompts) | Target destination package or main_app |
49
+ | `--user-model` | User | Rodauth user model name |
50
+
51
+ ### Step 2: Run Migrations
52
+
53
+ ```bash
54
+ rails db:migrate
55
+ ```
56
+
57
+ ### Step 3: Connect to Portal
58
+
59
+ Connect the Profile to your portal using the profile connect generator:
60
+
61
+ ```bash
62
+ rails g pu:profile:conn --dest=customer_portal
63
+ ```
64
+
65
+ This:
66
+ - Registers the Profile as a singular resource (`/profile` instead of `/profiles/:id`)
67
+ - Configures the `profile_url` helper to enable the "Profile" link in the user menu
68
+
69
+ ## Generated Files
70
+
71
+ The generator creates a standard Plutonium resource:
72
+
73
+ ```
74
+ app/models/profile.rb
75
+ db/migrate/xxx_create_profiles.rb
76
+ app/controllers/profiles_controller.rb
77
+ app/policies/profile_policy.rb
78
+ app/definitions/profile_definition.rb
79
+ ```
80
+
81
+ And modifies:
82
+ - **User model**: Adds `has_one :profile, dependent: :destroy`
83
+ - **Definition**: Injects custom ShowPage with security links
84
+
85
+ ## Security Section
86
+
87
+ The ShowPage automatically displays links to enabled Rodauth security features.
88
+
89
+ Available features (only shown if enabled in Rodauth):
90
+
91
+ | Feature | Link Label | Description |
92
+ |---------|------------|-------------|
93
+ | `change_password` | Change Password | Update account password |
94
+ | `change_login` | Change Email | Update email address |
95
+ | `otp` | Two-Factor Authentication | Set up TOTP authenticator |
96
+ | `recovery_codes` | Recovery Codes | View or regenerate backup codes |
97
+ | `webauthn` | Security Keys | Manage passkeys and hardware keys |
98
+ | `active_sessions` | Active Sessions | View and revoke sessions |
99
+ | `close_account` | Close Account | Permanently delete account |
100
+
101
+ ## Creating Profiles for Users
102
+
103
+ Users need a profile created before they can access it. Choose one approach:
104
+
105
+ ### Option A: Automatic Creation on User Signup
106
+
107
+ ```ruby
108
+ # app/models/user.rb
109
+ class User < ApplicationRecord
110
+ has_one :profile, dependent: :destroy
111
+
112
+ after_create :create_profile!
113
+
114
+ private
115
+
116
+ def create_profile!
117
+ create_profile
118
+ end
119
+
120
+ # add has_one associations above.
121
+ end
122
+ ```
123
+
124
+ ### Option B: Create on First Access
125
+
126
+ In your portal's application controller:
127
+
128
+ ```ruby
129
+ class ApplicationController < PlutoniumController
130
+ before_action :ensure_profile
131
+
132
+ private
133
+
134
+ def ensure_profile
135
+ return unless current_user
136
+ current_user.profile || current_user.create_profile
137
+ end
138
+ end
139
+ ```
140
+
141
+ ### Option C: Find or Create in Profile Controller
142
+
143
+ ```ruby
144
+ # app/controllers/profiles_controller.rb
145
+ class ProfilesController < ResourceController
146
+ private
147
+
148
+ def current_resource
149
+ @current_resource ||= current_user.profile || current_user.create_profile
150
+ end
151
+ end
152
+ ```
153
+
154
+ ## Customization
155
+
156
+ ### Adding Custom Fields
157
+
158
+ Edit the generated definition to configure how fields appear:
159
+
160
+ ```ruby
161
+ # app/definitions/profile_definition.rb
162
+ class ProfileDefinition < Plutonium::Resource::Definition
163
+ form do |f|
164
+ f.field :bio, as: :text
165
+ f.field :avatar, as: :attachment
166
+ f.field :timezone, collection: ActiveSupport::TimeZone.all.map(&:name)
167
+ f.field :notifications_enabled
168
+ end
169
+
170
+ display do |d|
171
+ d.field :bio
172
+ d.field :avatar
173
+ d.field :timezone
174
+ d.field :notifications_enabled
175
+ end
176
+
177
+ class ShowPage < ShowPage
178
+ private
179
+
180
+ def render_after_content
181
+ render Plutonium::Profile::SecuritySection.new
182
+ end
183
+ end
184
+ end
185
+ ```
186
+
187
+ ### Custom Security Section
188
+
189
+ Create your own security section component:
190
+
191
+ ```ruby
192
+ # app/components/custom_security_section.rb
193
+ class CustomSecuritySection < Plutonium::UI::Component::Base
194
+ def view_template
195
+ div(class: "mt-8") do
196
+ h2(class: "text-lg font-semibold") { "Account Security" }
197
+
198
+ if rodauth.features.include?(:change_password)
199
+ a(href: rodauth.change_password_path) { "Change Password" }
200
+ end
201
+
202
+ # Add your custom links
203
+ a(href: "/settings/notifications") { "Notification Preferences" }
204
+ end
205
+ end
206
+ end
207
+ ```
208
+
209
+ Use it in your definition:
210
+
211
+ ```ruby
212
+ class ProfileDefinition < Plutonium::Resource::Definition
213
+ class ShowPage < ShowPage
214
+ private
215
+
216
+ def render_after_content
217
+ render CustomSecuritySection.new
218
+ end
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Adding Profile Actions
224
+
225
+ Add custom actions to the profile:
226
+
227
+ ```ruby
228
+ class ProfileDefinition < Plutonium::Resource::Definition
229
+ action :export_data,
230
+ interaction: Profile::ExportDataInteraction,
231
+ icon: Phlex::TablerIcons::Download
232
+
233
+ action :verify_email,
234
+ interaction: Profile::VerifyEmailInteraction,
235
+ category: :secondary
236
+ end
237
+ ```
238
+
239
+ ### Profile Link in Navigation
240
+
241
+ Add a profile link to your application layout or header:
242
+
243
+ ```ruby
244
+ # In your header component
245
+ if current_user && respond_to?(:profile_path)
246
+ a(href: profile_path) { "My Profile" }
247
+ end
248
+ ```
249
+
250
+ ## Policy Configuration
251
+
252
+ The generated policy controls access:
253
+
254
+ ```ruby
255
+ # app/policies/profile_policy.rb
256
+ class ProfilePolicy < Plutonium::Resource::Policy
257
+ # Users can only access their own profile
258
+ def read?
259
+ resource.user == user
260
+ end
261
+
262
+ def update?
263
+ resource.user == user
264
+ end
265
+
266
+ def destroy?
267
+ false # Disable deletion through the UI
268
+ end
269
+
270
+ # Only allow editing these attributes
271
+ def permitted_attributes_for_update
272
+ [:bio, :avatar, :timezone, :notifications_enabled]
273
+ end
274
+ end
275
+ ```
276
+
277
+ ## Troubleshooting
278
+
279
+ ### "Profile not found" Error
280
+
281
+ Ensure the user has a profile created. Use one of the creation strategies above.
282
+
283
+ ### Security Links Not Showing
284
+
285
+ Security links only appear for features enabled in your Rodauth configuration:
286
+
287
+ ```ruby
288
+ # app/rodauth/user_rodauth_plugin.rb
289
+ class UserRodauthPlugin < Plutonium::Auth::RodauthPlugin
290
+ configure do
291
+ enable :change_password, :change_login, :otp, :active_sessions
292
+ # Only these features will show in the security section
293
+ end
294
+ end
295
+ ```
296
+
297
+ ### Profile Routes Conflicting
298
+
299
+ Ensure you use `--singular` when connecting:
300
+
301
+ ```bash
302
+ rails g pu:res:conn Profile --dest=my_portal --singular
303
+ ```
304
+
305
+ This creates `/profile` (singular) instead of `/profiles/:id`.
306
+
307
+ ### Changes Not Saving
308
+
309
+ Check your policy's `permitted_attributes_for_update`:
310
+
311
+ ```ruby
312
+ def permitted_attributes_for_update
313
+ [:bio, :avatar, :timezone, :notifications_enabled]
314
+ end
315
+ ```
316
+
317
+ ## Next Steps
318
+
319
+ - [Authentication](/guides/authentication) - Set up Rodauth features
320
+ - [Custom Actions](/guides/custom-actions) - Add profile actions
321
+ - [Views](/guides/views) - Customize the profile page
322
+ - [Authorization](/guides/authorization) - Configure profile policies
@@ -303,9 +303,46 @@ end
303
303
 
304
304
  Controllers automatically:
305
305
  - Scope all queries to the entity
306
- - Exclude entity field from forms
306
+ - Exclude entity field from forms (detected by association class)
307
+ - Inject entity value on create/update
307
308
  - Provide `current_scoped_entity` method
308
309
 
310
+ ### Association Detection
311
+
312
+ Plutonium auto-detects which `belongs_to` association points to the scoped entity class. This works even when `param_key` differs from the association name:
313
+
314
+ ```ruby
315
+ # Portal config
316
+ scope_to_entity Competition::Team, param_key: :team
317
+
318
+ # Model (association name differs from param_key)
319
+ class Match < ApplicationRecord
320
+ belongs_to :competition_team # Plutonium finds this by class
321
+ end
322
+ ```
323
+
324
+ ### Multiple Associations to Same Class
325
+
326
+ If a model has multiple associations to the scoped entity class, Plutonium raises an error:
327
+
328
+ ```
329
+ Match has multiple associations to Competition::Team: home_team, away_team.
330
+ Plutonium cannot auto-detect which one to use for entity scoping.
331
+ Override `scoped_entity_association` in your controller to specify the association.
332
+ ```
333
+
334
+ Resolve by overriding `scoped_entity_association`:
335
+
336
+ ```ruby
337
+ class MatchesController < ::ResourceController
338
+ private
339
+
340
+ def scoped_entity_association
341
+ :home_team # Return the association name as a symbol
342
+ end
343
+ end
344
+ ```
345
+
309
346
  ## Specifying Resource Class
310
347
 
311
348
  The resource class is inferred from the controller name. Override if needed:
@@ -153,6 +153,22 @@ class PostDefinition < Plutonium::Resource::Definition
153
153
  end
154
154
  ```
155
155
 
156
+ ## Form Configuration
157
+
158
+ Control form behavior:
159
+
160
+ ```ruby
161
+ class PostDefinition < Plutonium::Resource::Definition
162
+ # Controls "Save and add another" / "Update and continue editing" buttons
163
+ # nil (default) = auto-detect (hidden for singular resources, shown for plural)
164
+ # true = always show
165
+ # false = always hide
166
+ submit_and_continue false
167
+ end
168
+ ```
169
+
170
+ Singular resources (e.g., `resource :profile` routes or `has_one` nested) auto-hide the secondary submit button since creating "another" doesn't make sense.
171
+
156
172
  ## Custom Page Classes
157
173
 
158
174
  Override default page components:
@@ -301,6 +301,21 @@ When `post_type` changes, the form re-renders via Turbo and shows/hides conditio
301
301
 
302
302
  ## Form Actions
303
303
 
304
+ ### Submit and Continue Button
305
+
306
+ Forms include a secondary button ("Create and add another" for new records, "Update and continue editing" for existing). Control this in your definition:
307
+
308
+ ```ruby
309
+ class PostDefinition < ResourceDefinition
310
+ # nil (default) = auto-detect (hidden for singular resources, shown for plural)
311
+ # true = always show
312
+ # false = always hide
313
+ submit_and_continue false
314
+ end
315
+ ```
316
+
317
+ Singular resources (e.g., `resource :profile` routes or `has_one` nested) auto-hide this button.
318
+
304
319
  ### Default Actions
305
320
 
306
321
  ```ruby
@@ -267,13 +267,35 @@ Available kit methods:
267
267
  | `TabList(items:)` | Tab navigation |
268
268
  | `EmptyCard(message)` | Empty state card |
269
269
  | `ActionButton(action, url:)` | Action button |
270
- | `DynaFrameHost()` / `DynaFrameContent()` | Turbo frame helpers |
270
+ | `DynaFrameHost(src:, loading:)` | Lazy-loading turbo frame |
271
+ | `DynaFrameContent(content) { \|frame\| ... }` | Frame-aware content wrapper |
271
272
  | `TableSearchBar()` | Search bar for tables |
272
273
  | `TableScopesBar()` | Scope tabs for tables |
273
274
  | `TableInfo(pagy)` | Pagination info |
274
275
  | `TablePagination(pagy)` | Pagination links |
275
276
  | `FrameNavigatorPanel(title:, src:, panel_id:)` | Frame navigation panel |
276
277
 
278
+ ## DynaFrameContent Pattern
279
+
280
+ `DynaFrameContent` enables frame-aware rendering. For turbo-frame requests, only the content is rendered inside the frame. For regular requests, the full page renders with header/footer.
281
+
282
+ ```ruby
283
+ # How Page::Base uses DynaFrameContent
284
+ def view_template(&block)
285
+ DynaFrameContent(page_content(block)) do |frame|
286
+ render_header # Skipped for frame requests
287
+ frame.render_content # Always rendered (wrapped in turbo-frame for frame requests)
288
+ render_footer # Skipped for frame requests
289
+ end
290
+ end
291
+ ```
292
+
293
+ This pattern means:
294
+ - **Regular page load**: Full page with header, content, footer
295
+ - **Turbo-frame request**: Only `<turbo-frame id="...">` with content inside
296
+
297
+ All pages automatically inherit this behavior - modals, lazy-loaded frames, and navigation all work correctly without special handling.
298
+
277
299
  ## Custom Components
278
300
 
279
301
  ### Creating a Phlex Component
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.41.1)
4
+ plutonium (0.42.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.41.1)
4
+ plutonium (0.42.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.41.1)
4
+ plutonium (0.43.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -17,6 +17,7 @@ module Pu
17
17
  configure_application
18
18
  replace_build_script
19
19
  import_styles
20
+ fix_layout_stylesheet_tag
20
21
  rescue => e
21
22
  exception "#{self.class} failed:", e
22
23
  end
@@ -72,6 +73,17 @@ module Pu
72
73
  prepend_to_file "app/assets/stylesheets/application.tailwind.css",
73
74
  "@import \"gem:plutonium/src/css/plutonium.css\";\n\n"
74
75
  end
76
+
77
+ # Rails 8 generates layouts with `stylesheet_link_tag :app` which includes all
78
+ # stylesheets in the asset paths. With cssbundling-rails, this causes both the
79
+ # built CSS and the source CSS to be served, leading to errors when the browser
80
+ # tries to load the unprocessed source file containing PostCSS directives.
81
+ def fix_layout_stylesheet_tag
82
+ layout_file = "app/views/layouts/application.html.erb"
83
+ return unless File.exist?(layout_file)
84
+
85
+ gsub_file layout_file, /stylesheet_link_tag\s+:app\b/, 'stylesheet_link_tag "application"'
86
+ end
75
87
  end
76
88
  end
77
89
  end
@@ -20,4 +20,15 @@ class ResourceController < PlutoniumController
20
20
 
21
21
  helper_method :logout_url
22
22
  <%- end -%>
23
+ <%- if !ApplicationController.new.respond_to?(:profile_url, true) -%>
24
+
25
+ private def profile_url
26
+ # return a profile url to render a profile link in the user menu
27
+ # e.g. rodauth.change_password_path or your custom profile_path
28
+ end
29
+ helper_method :profile_url
30
+ <%- elsif !ApplicationController._helper_methods.include?(:profile_url) -%>
31
+
32
+ helper_method :profile_url
33
+ <%- end -%>
23
34
  end