plutonium 0.51.0 → 0.52.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-app/SKILL.md +2 -0
  3. data/.claude/skills/plutonium-auth/SKILL.md +6 -4
  4. data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
  5. data/.claude/skills/plutonium-tenancy/SKILL.md +25 -6
  6. data/.claude/skills/plutonium-testing/SKILL.md +3 -1
  7. data/.claude/skills/plutonium-ui/SKILL.md +3 -3
  8. data/CHANGELOG.md +17 -0
  9. data/app/assets/plutonium.css +1 -1
  10. data/app/assets/plutonium.js +1 -0
  11. data/app/assets/plutonium.js.map +3 -3
  12. data/app/assets/plutonium.min.js +1 -1
  13. data/app/assets/plutonium.min.js.map +3 -3
  14. data/docs/.vitepress/config.ts +1 -2
  15. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  16. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  17. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  18. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  19. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  20. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  21. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  22. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  23. data/docs/.vitepress/theme/custom.css +144 -0
  24. data/docs/.vitepress/theme/index.ts +58 -1
  25. data/docs/getting-started/index.md +33 -50
  26. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  27. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  28. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  29. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  30. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  31. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  32. data/docs/guides/authentication.md +10 -5
  33. data/docs/guides/authorization.md +3 -3
  34. data/docs/guides/creating-packages.md +8 -11
  35. data/docs/guides/custom-actions.md +6 -1
  36. data/docs/guides/customizing-ui.md +258 -0
  37. data/docs/guides/index.md +49 -32
  38. data/docs/guides/multi-tenancy.md +10 -2
  39. data/docs/guides/nested-resources.md +69 -0
  40. data/docs/guides/search-filtering.md +6 -0
  41. data/docs/guides/testing.md +5 -1
  42. data/docs/guides/theming.md +13 -0
  43. data/docs/guides/user-invites.md +10 -4
  44. data/docs/guides/user-profile.md +8 -0
  45. data/docs/index.md +10 -219
  46. data/docs/public/asciinema/home-scaffold.cast +305 -0
  47. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  48. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  49. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  50. data/docs/public/images/guides/nested-inputs.png +0 -0
  51. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  52. data/docs/public/images/guides/search-filtering-index.png +0 -0
  53. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  54. data/docs/public/images/guides/theming-after.png +0 -0
  55. data/docs/public/images/guides/theming-before.png +0 -0
  56. data/docs/public/images/guides/user-invites-landing.png +0 -0
  57. data/docs/public/images/guides/user-profile-edit.png +0 -0
  58. data/docs/public/images/guides/user-profile-show.png +0 -0
  59. data/docs/public/images/home-index.png +0 -0
  60. data/docs/public/images/home-new.png +0 -0
  61. data/docs/public/images/home-show.png +0 -0
  62. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  63. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  64. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  65. data/docs/public/images/tutorial/02-new-form.png +0 -0
  66. data/docs/public/images/tutorial/03-create-account.png +0 -0
  67. data/docs/public/images/tutorial/03-login.png +0 -0
  68. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  69. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  70. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  71. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  72. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  73. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  74. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  75. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  76. data/docs/reference/app/generators.md +4 -4
  77. data/docs/reference/auth/accounts.md +6 -7
  78. data/docs/reference/auth/index.md +1 -1
  79. data/docs/reference/behavior/policies.md +1 -1
  80. data/docs/reference/index.md +67 -55
  81. data/docs/reference/resource/definition.md +1 -1
  82. data/docs/reference/tenancy/entity-scoping.md +8 -1
  83. data/docs/reference/tenancy/index.md +1 -1
  84. data/docs/reference/tenancy/invites.md +12 -5
  85. data/docs/reference/ui/tables.md +8 -4
  86. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  87. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  88. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  89. data/gemfiles/rails_7.gemfile.lock +1 -1
  90. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  91. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  92. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  93. data/lib/generators/pu/invites/install_generator.rb +44 -0
  94. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  95. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  96. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  97. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  98. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  99. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  100. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  101. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  102. data/lib/generators/pu/saas/membership/USAGE +4 -1
  103. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  104. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  105. data/lib/plutonium/helpers/turbo_helper.rb +19 -0
  106. data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
  107. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  108. data/lib/plutonium/ui/component/methods.rb +1 -0
  109. data/lib/plutonium/ui/form/base.rb +17 -1
  110. data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
  111. data/lib/plutonium/ui/form/interaction.rb +1 -1
  112. data/lib/plutonium/ui/form/theme.rb +1 -1
  113. data/lib/plutonium/ui/page/edit.rb +1 -1
  114. data/lib/plutonium/ui/page/new.rb +1 -1
  115. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  116. data/lib/plutonium/version.rb +1 -1
  117. data/package.json +4 -1
  118. data/src/js/controllers/form_controller.js +5 -4
  119. data/yarn.lock +108 -1
  120. metadata +45 -3
@@ -16,6 +16,9 @@ module Pu
16
16
  class_option :entity, type: :string, required: true,
17
17
  desc: "The entity model name (e.g., Organization)"
18
18
 
19
+ class_option :dest, type: :string, default: "main_app",
20
+ desc: "Destination feature/package for entity, membership, and api_client (default: main_app)"
21
+
19
22
  class_option :allow_signup, type: :boolean, default: true,
20
23
  desc: "Whether to allow users to sign up to the platform"
21
24
 
@@ -52,6 +55,9 @@ module Pu
52
55
  class_option :profile, type: :boolean, default: true,
53
56
  desc: "Generate user profile resource"
54
57
 
58
+ class_option :profile_attributes, type: :array, default: %w[name:string],
59
+ desc: "Additional attributes for the user profile model (default: name:string)"
60
+
55
61
  class_option :api_client, type: :string, default: nil,
56
62
  desc: "Generate an API client model (e.g., ApiClient)"
57
63
 
@@ -130,9 +136,15 @@ module Pu
130
136
  end
131
137
 
132
138
  def generate_profile
133
- generate "pu:profile:setup",
134
- "--user-model=#{options[:user]} --dest=main_app" \
135
- "#{" --portal=#{portal_package}" if options[:portal]}"
139
+ klass = Rails::Generators.find_by_namespace("pu:profile:setup")
140
+ profile_options = {
141
+ user_model: options[:user],
142
+ dest: options[:dest],
143
+ force: options[:force],
144
+ skip: options[:skip]
145
+ }
146
+ profile_options[:portal] = portal_package if options[:portal]
147
+ klass.new([nil, *options[:profile_attributes]], profile_options).invoke_all
136
148
  end
137
149
 
138
150
  def generate_welcome
@@ -145,7 +157,7 @@ module Pu
145
157
 
146
158
  def generate_invites
147
159
  generate "pu:invites:install",
148
- "--entity-model=#{options[:entity]} --user-model=#{options[:user]} --dest=main_app" \
160
+ "--entity-model=#{options[:entity]} --user-model=#{options[:user]} --dest=#{options[:dest]}" \
149
161
  " --rodauth=#{rodauth_config}"
150
162
  end
151
163
 
@@ -63,7 +63,7 @@ class WelcomeController < AuthenticatedController
63
63
  <% end -%>
64
64
 
65
65
  def portal_root_path(<%= entity_table %>)
66
- <%= portal_engine %>::Engine.routes.url_helpers.<%= entity_table %>_root_path(<%= entity_table %>: <%= entity_table %>)
66
+ <%= portal_engine %>::Engine.routes.url_helpers.<%= entity_table %>_scoped_root_path(<%= entity_table %>_scoped: <%= entity_table %>)
67
67
  end
68
68
  helper_method :portal_root_path
69
69
  end
@@ -19,6 +19,25 @@ module Plutonium
19
19
  def remote_modal_frame_tag(&)
20
20
  turbo_frame_tag(Plutonium::REMOTE_MODAL_FRAME, &)
21
21
  end
22
+
23
+ # Returns a turbo-frame-scoped element id. Two identically-named forms
24
+ # can be on the page simultaneously (e.g. a primary modal opens a
25
+ # secondary modal, each rendering an `id="resource-form"`). When the
26
+ # server later replies with `turbo_stream.replace("resource-form", ...)`,
27
+ # Turbo would pick the FIRST element matching the id — which is rarely
28
+ # the one the user actually submitted. Append a frame suffix so each
29
+ # frame's form has a unique id and the controller can target precisely.
30
+ #
31
+ # @param base [String, Symbol] the base id
32
+ # @return [String] the scoped id (no suffix outside any modal frame)
33
+ def turbo_scoped_dom_id(base)
34
+ base = base.to_s
35
+ case current_turbo_frame
36
+ when Plutonium::REMOTE_MODAL_FRAME then "#{base}-primary"
37
+ when Plutonium::REMOTE_MODAL_SECONDARY_FRAME then "#{base}-secondary"
38
+ else base
39
+ end
40
+ end
22
41
  end
23
42
  end
24
43
  end
@@ -53,7 +53,7 @@ module Plutonium
53
53
 
54
54
  respond_to do |format|
55
55
  if params[:pre_submit]
56
- format.turbo_stream { render turbo_stream: turbo_stream.replace("resource-form", view_context.render(build_form(action: :new))) }
56
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("resource-form"), view_context.render(build_form(action: :new))) }
57
57
  format.html { render :new, status: :unprocessable_content }
58
58
  elsif resource_record!.save
59
59
  format.turbo_stream do
@@ -71,7 +71,7 @@ module Plutonium
71
71
  location: redirect_url_after_submit
72
72
  end
73
73
  else
74
- format.turbo_stream { render turbo_stream: turbo_stream.replace("resource-form", view_context.render(build_form(action: :new))), status: :unprocessable_content }
74
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("resource-form"), view_context.render(build_form(action: :new))), status: :unprocessable_content }
75
75
  format.html { render :new, status: :unprocessable_content }
76
76
  format.any do
77
77
  @errors = resource_record!.errors
@@ -100,7 +100,7 @@ module Plutonium
100
100
 
101
101
  respond_to do |format|
102
102
  if params[:pre_submit]
103
- format.turbo_stream { render turbo_stream: turbo_stream.replace("resource-form", view_context.render(build_form(action: :edit))) }
103
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("resource-form"), view_context.render(build_form(action: :edit))) }
104
104
  format.html { render :edit, status: :unprocessable_content }
105
105
  elsif resource_record!.save
106
106
  format.turbo_stream do
@@ -116,7 +116,7 @@ module Plutonium
116
116
  render :show, status: :ok, location: redirect_url_after_submit
117
117
  end
118
118
  else
119
- format.turbo_stream { render turbo_stream: turbo_stream.replace("resource-form", view_context.render(build_form(action: :edit))), status: :unprocessable_content }
119
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("resource-form"), view_context.render(build_form(action: :edit))), status: :unprocessable_content }
120
120
  format.html { render :edit, status: :unprocessable_content }
121
121
  format.any do
122
122
  @errors = resource_record!.errors
@@ -38,7 +38,7 @@ module Plutonium
38
38
 
39
39
  if params[:pre_submit]
40
40
  respond_to do |format|
41
- format.turbo_stream { render turbo_stream: turbo_stream.replace("interaction-form", view_context.render(@interaction.build_form)) }
41
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("interaction-form"), view_context.render(@interaction.build_form)) }
42
42
  format.html { render :interactive_record_action, formats: [:html], status: :unprocessable_content }
43
43
  end
44
44
  return
@@ -87,7 +87,7 @@ module Plutonium
87
87
 
88
88
  if params[:pre_submit]
89
89
  respond_to do |format|
90
- format.turbo_stream { render turbo_stream: turbo_stream.replace("interaction-form", view_context.render(@interaction.build_form)) }
90
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("interaction-form"), view_context.render(@interaction.build_form)) }
91
91
  format.html { render :interactive_resource_action, status: :unprocessable_content }
92
92
  end
93
93
  return
@@ -134,7 +134,7 @@ module Plutonium
134
134
 
135
135
  if params[:pre_submit]
136
136
  respond_to do |format|
137
- format.turbo_stream { render turbo_stream: turbo_stream.replace("interaction-form", view_context.render(@interaction.build_form)) }
137
+ format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("interaction-form"), view_context.render(@interaction.build_form)) }
138
138
  format.html { render :interactive_bulk_action, formats: [:html], status: :unprocessable_content }
139
139
  end
140
140
  return
@@ -48,6 +48,7 @@ module Plutonium
48
48
  :in_frame?,
49
49
  :in_modal?,
50
50
  :in_secondary_modal?,
51
+ :turbo_scoped_dom_id,
51
52
  :current_interactive_action,
52
53
  :current_engine,
53
54
  :policy_for,
@@ -149,9 +149,25 @@ module Plutonium
149
149
  def initialize_attributes
150
150
  super
151
151
 
152
- attributes[:id] ||= :resource_form
152
+ attributes[:id] ||= "resource-form"
153
153
  attributes["data-controller"] = "form"
154
154
  end
155
+
156
+ # Scope the form id to the current turbo frame at render time (we
157
+ # can't do this in `initialize_attributes` — Phlex hasn't started
158
+ # rendering yet, so `view_context` and the request headers aren't
159
+ # accessible). Primary and secondary modals can each host a form
160
+ # without colliding on document-level turbo-stream `replace target=`
161
+ # lookups. See Helpers::TurboHelper#turbo_scoped_dom_id.
162
+ #
163
+ # Also force-replace the id (Phlexi's `mix` would otherwise prepend
164
+ # `@namespace.dom_id`, producing space-separated ids like
165
+ # "q filter-form" which break document.getElementById lookups).
166
+ def form_attributes
167
+ attrs = super
168
+ attrs[:id] = turbo_scoped_dom_id(attributes[:id]) if attributes[:id]
169
+ attrs
170
+ end
155
171
  end
156
172
  end
157
173
  end
@@ -24,15 +24,20 @@ module Plutonium
24
24
  def render_add_button
25
25
  return if @add_action == false
26
26
 
27
+ # Two stacking levels are supported (primary + secondary modal).
28
+ # Hide the "+" entirely once we're already inside the secondary —
29
+ # there's no tertiary frame to escalate to.
30
+ return if in_secondary_modal?
31
+
27
32
  url, turbo_frame = add_url_and_frame
28
33
  return unless url
29
34
 
30
- # When the parent form is already inside a modal, route the
31
- # "+" to the secondary frame so the stacked dialog opens on
32
- # top of the original form rather than replacing it. The
33
- # crud controller mirrors this on success — closing the
34
- # secondary modal and reloading the primary so the
35
- # association select picks up the new record.
35
+ # When the parent form is already inside the primary modal,
36
+ # route the "+" to the secondary frame so the stacked dialog
37
+ # opens on top of the original form rather than replacing it.
38
+ # The crud controller mirrors this on success — closing the
39
+ # secondary modal and reloading the primary so the association
40
+ # select picks up the new record.
36
41
  if turbo_frame == Plutonium::REMOTE_MODAL_FRAME && in_modal?
37
42
  turbo_frame = Plutonium::REMOTE_MODAL_SECONDARY_FRAME
38
43
  end
@@ -47,7 +47,7 @@ module Plutonium
47
47
 
48
48
  def initialize_attributes
49
49
  super
50
- attributes[:id] = :interaction_form
50
+ attributes[:id] = "interaction-form"
51
51
  attributes.fetch(:data_turbo) { attributes[:data_turbo] = object.turbo.to_s }
52
52
  end
53
53
 
@@ -14,7 +14,7 @@ module Plutonium
14
14
  inner_wrapper: "w-full",
15
15
 
16
16
  # Form errors
17
- form_errors_wrapper: "flex items-start gap-3 p-4 mb-6 text-base text-danger-800 rounded-[var(--pu-radius-lg)] bg-danger-50 border border-danger-200 dark:bg-danger-950/30 dark:border-danger-800 dark:text-danger-300",
17
+ form_errors_wrapper: "flex items-start gap-3 m-4 p-4 text-base text-danger-800 rounded-[var(--pu-radius-lg)] bg-danger-50 border border-danger-200 dark:bg-danger-950/30 dark:border-danger-800 dark:text-danger-300",
18
18
  form_errors_message: "font-semibold",
19
19
  form_errors_list: "mt-2 list-disc list-inside text-sm",
20
20
 
@@ -7,7 +7,7 @@ module Plutonium
7
7
  private
8
8
 
9
9
  def page_title
10
- current_definition.edit_page_title || super || "Edit"
10
+ current_definition.edit_page_title || super || "Edit #{resource_name(resource_class, 1)}"
11
11
  end
12
12
 
13
13
  def page_description
@@ -7,7 +7,7 @@ module Plutonium
7
7
  private
8
8
 
9
9
  def page_title
10
- current_definition.new_page_title || super || "New"
10
+ current_definition.new_page_title || super || "New #{resource_name(resource_class, 1)}"
11
11
  end
12
12
 
13
13
  def page_description
@@ -14,10 +14,7 @@ module Plutonium
14
14
  def initialize(*, query_object:, search_url:, search_param: :q, search_value: nil, attributes: {}, **opts, &)
15
15
  opts[:as] = :q
16
16
  opts[:method] = :get
17
- attributes = attributes.deep_merge(
18
- id: "filter-form",
19
- data: {turbo_frame: nil}
20
- )
17
+ attributes = attributes.deep_merge(data: {turbo_frame: nil})
21
18
  super(*, attributes:, **opts, &)
22
19
  @query_object = query_object
23
20
  @search_url = search_url
@@ -25,6 +22,17 @@ module Plutonium
25
22
  @search_value = search_value
26
23
  end
27
24
 
25
+ # The Filters slideover renders on every index page (off-screen
26
+ # until opened) — same DOM as any CRUD modal that might appear.
27
+ # Base defaults the form id to "resource-form"; without this
28
+ # override, document-level `turbo_stream.replace("resource-form", …)`
29
+ # from a CRUD submit would clobber the wrong form. Pick a distinct
30
+ # id so the two never collide.
31
+ def initialize_attributes
32
+ super
33
+ attributes[:id] = "filter-form"
34
+ end
35
+
28
36
  def form_class
29
37
  "flex-1 flex flex-col min-h-0"
30
38
  end
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.51.0"
2
+ VERSION = "0.52.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
@@ -30,15 +30,18 @@
30
30
  "marked": "^15.0.3"
31
31
  },
32
32
  "devDependencies": {
33
+ "@tabler/icons-vue": "^3.44.0",
33
34
  "@tailwindcss/forms": "^0.5.10",
34
35
  "@tailwindcss/postcss": "^4.3.0",
35
36
  "@tailwindcss/typography": "^0.5.16",
37
+ "asciinema-player": "^3.15.1",
36
38
  "chokidar-cli": "^3.0.0",
37
39
  "concurrently": "^8.2.2",
38
40
  "cssnano": "^7.0.2",
39
41
  "esbuild": "^0.28.0",
40
42
  "esbuild-plugin-manifest": "^1.0.3",
41
43
  "flowbite-typography": "^1.0.5",
44
+ "medium-zoom": "^1.1.0",
42
45
  "mermaid": "^11.15.0",
43
46
  "postcss": "^8.5.14",
44
47
  "postcss-cli": "^11.0.1",
@@ -6,19 +6,20 @@ export default class extends Controller {
6
6
  }
7
7
 
8
8
  preSubmit() {
9
- // Create a hidden input field
9
+ // Some widgets (e.g. slim-select) dispatch their own change event on top
10
+ // of the native one, so this can fire twice per user action. Remove any
11
+ // prior hidden field before appending a fresh one to avoid duplicates.
12
+ this.element.querySelectorAll('input[name="pre_submit"]').forEach(n => n.remove());
13
+
10
14
  const hiddenField = document.createElement('input');
11
15
  hiddenField.type = 'hidden';
12
16
  hiddenField.name = 'pre_submit';
13
17
  hiddenField.value = 'true';
14
-
15
- // Append it to the form
16
18
  this.element.appendChild(hiddenField);
17
19
 
18
20
  // Skip validation by setting novalidate attribute
19
21
  this.element.setAttribute('novalidate', '');
20
22
 
21
- // Submit the form
22
23
  this.submit();
23
24
  }
24
25
 
data/yarn.lock CHANGED
@@ -876,6 +876,7 @@ __metadata:
876
876
  "@hotwired/stimulus": "npm:^3.2.2"
877
877
  "@hotwired/turbo": "npm:^8.0.4"
878
878
  "@popperjs/core": "npm:^2.11.8"
879
+ "@tabler/icons-vue": "npm:^3.44.0"
879
880
  "@tailwindcss/forms": "npm:^0.5.10"
880
881
  "@tailwindcss/postcss": "npm:^4.3.0"
881
882
  "@tailwindcss/typography": "npm:^0.5.16"
@@ -883,6 +884,7 @@ __metadata:
883
884
  "@uppy/dashboard": "npm:^4.1.3"
884
885
  "@uppy/image-editor": "npm:^3.2.1"
885
886
  "@uppy/xhr-upload": "npm:^4.2.3"
887
+ asciinema-player: "npm:^3.15.1"
886
888
  chokidar-cli: "npm:^3.0.0"
887
889
  concurrently: "npm:^8.2.2"
888
890
  cssnano: "npm:^7.0.2"
@@ -892,6 +894,7 @@ __metadata:
892
894
  flowbite-typography: "npm:^1.0.5"
893
895
  lodash.debounce: "npm:^4.0.8"
894
896
  marked: "npm:^15.0.3"
897
+ medium-zoom: "npm:^1.1.0"
895
898
  mermaid: "npm:^11.15.0"
896
899
  postcss: "npm:^8.5.14"
897
900
  postcss-cli: "npm:^11.0.1"
@@ -1158,6 +1161,53 @@ __metadata:
1158
1161
  languageName: node
1159
1162
  linkType: hard
1160
1163
 
1164
+ "@solid-primitives/refs@npm:^1.0.5":
1165
+ version: 1.1.3
1166
+ resolution: "@solid-primitives/refs@npm:1.1.3"
1167
+ dependencies:
1168
+ "@solid-primitives/utils": "npm:^6.4.0"
1169
+ peerDependencies:
1170
+ solid-js: ^1.6.12
1171
+ checksum: 10c0/af1e27b5b38f639e5a3125a982ff8f9c58fe2aea4609718c9383d88d1fc5a13e490fb40c262eb183c695b3efacd89b5328876a615814c0edb54981b58b3804fa
1172
+ languageName: node
1173
+ linkType: hard
1174
+
1175
+ "@solid-primitives/transition-group@npm:^1.0.2":
1176
+ version: 1.1.2
1177
+ resolution: "@solid-primitives/transition-group@npm:1.1.2"
1178
+ peerDependencies:
1179
+ solid-js: ^1.6.12
1180
+ checksum: 10c0/f676cefc38bab6aad7c1214ef2ea145d598ec2e873eca663a79a3a8e46b9ee8f6ea57f3e11c0acf5156ffa63ba727e9676c46c6fd0cf9130e034da79ead12402
1181
+ languageName: node
1182
+ linkType: hard
1183
+
1184
+ "@solid-primitives/utils@npm:^6.4.0":
1185
+ version: 6.4.0
1186
+ resolution: "@solid-primitives/utils@npm:6.4.0"
1187
+ peerDependencies:
1188
+ solid-js: ^1.6.12
1189
+ checksum: 10c0/fdac336c74be180251ac40df280571534d427c773b207e19a51aa01f013e16864f15c5c829f53a8e7d0033543bef07a6410c2dbaf364410dc29783966e14fcac
1190
+ languageName: node
1191
+ linkType: hard
1192
+
1193
+ "@tabler/icons-vue@npm:^3.44.0":
1194
+ version: 3.44.0
1195
+ resolution: "@tabler/icons-vue@npm:3.44.0"
1196
+ dependencies:
1197
+ "@tabler/icons": "npm:3.44.0"
1198
+ peerDependencies:
1199
+ vue: ">=3.0.1"
1200
+ checksum: 10c0/7730f3cd00056584ad322ab9d7b740d0f62632e5480a89dfd458f916af278010a940bfb62bcd1067b32177bf8ff5c101534aa898666501e47dc0ddaf3f0f9c9f
1201
+ languageName: node
1202
+ linkType: hard
1203
+
1204
+ "@tabler/icons@npm:3.44.0":
1205
+ version: 3.44.0
1206
+ resolution: "@tabler/icons@npm:3.44.0"
1207
+ checksum: 10c0/0d5c1f9d6e68aa04c4a661e96035190e0884e0bc0022d31922efad3cfba210a25b2f013c0b6c0ab6aeb3d5a402a9b85a168966bd11a30ba6087f614f8521ccf3
1208
+ languageName: node
1209
+ linkType: hard
1210
+
1161
1211
  "@tailwindcss/forms@npm:^0.5.10":
1162
1212
  version: 0.5.10
1163
1213
  resolution: "@tailwindcss/forms@npm:0.5.10"
@@ -2184,6 +2234,17 @@ __metadata:
2184
2234
  languageName: node
2185
2235
  linkType: hard
2186
2236
 
2237
+ "asciinema-player@npm:^3.15.1":
2238
+ version: 3.15.1
2239
+ resolution: "asciinema-player@npm:3.15.1"
2240
+ dependencies:
2241
+ "@babel/runtime": "npm:^7.21.0"
2242
+ solid-js: "npm:^1.3.0"
2243
+ solid-transition-group: "npm:^0.2.3"
2244
+ checksum: 10c0/36638e9804a94866d6c6ae25cdbf867313873e68838b2630eed73229819170bc67667098865fe38b061012c3fa78e8235322725c5e1bda258ec82787d470f8a4
2245
+ languageName: node
2246
+ linkType: hard
2247
+
2187
2248
  "balanced-match@npm:^4.0.2":
2188
2249
  version: 4.0.4
2189
2250
  resolution: "balanced-match@npm:4.0.4"
@@ -2647,7 +2708,7 @@ __metadata:
2647
2708
  languageName: node
2648
2709
  linkType: hard
2649
2710
 
2650
- "csstype@npm:^3.2.3":
2711
+ "csstype@npm:^3.1.0, csstype@npm:^3.2.3":
2651
2712
  version: 3.2.3
2652
2713
  resolution: "csstype@npm:3.2.3"
2653
2714
  checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce
@@ -4153,6 +4214,13 @@ __metadata:
4153
4214
  languageName: node
4154
4215
  linkType: hard
4155
4216
 
4217
+ "medium-zoom@npm:^1.1.0":
4218
+ version: 1.1.0
4219
+ resolution: "medium-zoom@npm:1.1.0"
4220
+ checksum: 10c0/7d1f05e8eab045c33d7c04d4ee7bf04f5246cf7a720d7b5f5a51c36ab23666e363bcbb6bffae50b5948d5eb19361914cb0e26a1fce5c1fff7a266bc0217893f3
4221
+ languageName: node
4222
+ linkType: hard
4223
+
4156
4224
  "mermaid@npm:^11.15.0":
4157
4225
  version: 11.15.0
4158
4226
  resolution: "mermaid@npm:11.15.0"
@@ -5355,6 +5423,22 @@ __metadata:
5355
5423
  languageName: node
5356
5424
  linkType: hard
5357
5425
 
5426
+ "seroval-plugins@npm:~1.5.0":
5427
+ version: 1.5.4
5428
+ resolution: "seroval-plugins@npm:1.5.4"
5429
+ peerDependencies:
5430
+ seroval: ^1.0
5431
+ checksum: 10c0/f8843ff12c2fcbf0d1124d02addcffc1a727f55b500ac24218a528e95e540c42052e2e6f6b3dfaad2aa8105fd0985dff722c3ae774723b9899e0fafe7d4698be
5432
+ languageName: node
5433
+ linkType: hard
5434
+
5435
+ "seroval@npm:~1.5.0":
5436
+ version: 1.5.4
5437
+ resolution: "seroval@npm:1.5.4"
5438
+ checksum: 10c0/6191e27f21000f7693ab923fde69c47a3ce5fbb86e585e5a8fc072d70db52ebc3c4dab83c3b2ab67311ec646b2064df089a3a155c49b21846438aaf510d4b964
5439
+ languageName: node
5440
+ linkType: hard
5441
+
5358
5442
  "set-blocking@npm:^2.0.0":
5359
5443
  version: 2.0.0
5360
5444
  resolution: "set-blocking@npm:2.0.0"
@@ -5434,6 +5518,29 @@ __metadata:
5434
5518
  languageName: node
5435
5519
  linkType: hard
5436
5520
 
5521
+ "solid-js@npm:^1.3.0":
5522
+ version: 1.9.13
5523
+ resolution: "solid-js@npm:1.9.13"
5524
+ dependencies:
5525
+ csstype: "npm:^3.1.0"
5526
+ seroval: "npm:~1.5.0"
5527
+ seroval-plugins: "npm:~1.5.0"
5528
+ checksum: 10c0/1c407da820435771ec6fd65e605fd804fc1faf74ee84af2d3dce2bc5c223563017a9e15746eb86d27237e6d0d6ac8660685c560eb1f1decdc6f3c7b913927928
5529
+ languageName: node
5530
+ linkType: hard
5531
+
5532
+ "solid-transition-group@npm:^0.2.3":
5533
+ version: 0.2.3
5534
+ resolution: "solid-transition-group@npm:0.2.3"
5535
+ dependencies:
5536
+ "@solid-primitives/refs": "npm:^1.0.5"
5537
+ "@solid-primitives/transition-group": "npm:^1.0.2"
5538
+ peerDependencies:
5539
+ solid-js: ^1.6.12
5540
+ checksum: 10c0/584656bedefb03fd91801d858c9abf0de5afc175e9a7bea2023000faa5ed3af4c6e4b8b99dd7ed1069595d362b002d4ae8ca08d030e422b65dc23742fb2ac681
5541
+ languageName: node
5542
+ linkType: hard
5543
+
5437
5544
  "source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1":
5438
5545
  version: 1.2.1
5439
5546
  resolution: "source-map-js@npm:1.2.1"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.51.0
4
+ version: 0.52.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-14 00:00:00.000000000 Z
10
+ date: 2026-05-21 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk
@@ -532,6 +532,14 @@ files:
532
532
  - config/initializers/rabl.rb
533
533
  - config/initializers/sqlite_alias.rb
534
534
  - docs/.vitepress/config.ts
535
+ - docs/.vitepress/theme/components/HomeAudienceSplit.vue
536
+ - docs/.vitepress/theme/components/HomeCta.vue
537
+ - docs/.vitepress/theme/components/HomeHero.vue
538
+ - docs/.vitepress/theme/components/HomeInTheBox.vue
539
+ - docs/.vitepress/theme/components/HomePillars.vue
540
+ - docs/.vitepress/theme/components/HomeStopWriting.vue
541
+ - docs/.vitepress/theme/components/HomeWalkthrough.vue
542
+ - docs/.vitepress/theme/components/SectionLanding.vue
535
543
  - docs/.vitepress/theme/custom.css
536
544
  - docs/.vitepress/theme/index.ts
537
545
  - docs/getting-started/index.md
@@ -550,6 +558,7 @@ files:
550
558
  - docs/guides/authorization.md
551
559
  - docs/guides/creating-packages.md
552
560
  - docs/guides/custom-actions.md
561
+ - docs/guides/customizing-ui.md
553
562
  - docs/guides/index.md
554
563
  - docs/guides/multi-tenancy.md
555
564
  - docs/guides/nested-resources.md
@@ -564,9 +573,39 @@ files:
564
573
  - docs/public/android-chrome-192x192.png
565
574
  - docs/public/android-chrome-512x512.png
566
575
  - docs/public/apple-touch-icon.png
576
+ - docs/public/asciinema/home-scaffold.cast
567
577
  - docs/public/favicon-16x16.png
568
578
  - docs/public/favicon-32x32.png
569
579
  - docs/public/favicon.ico
580
+ - docs/public/images/guides/custom-actions-bulk.png
581
+ - docs/public/images/guides/multi-tenancy-dashboard.png
582
+ - docs/public/images/guides/multi-tenancy-welcome.png
583
+ - docs/public/images/guides/nested-inputs.png
584
+ - docs/public/images/guides/nested-resources-tab.png
585
+ - docs/public/images/guides/search-filtering-index.png
586
+ - docs/public/images/guides/search-filtering-panel.png
587
+ - docs/public/images/guides/theming-after.png
588
+ - docs/public/images/guides/theming-before.png
589
+ - docs/public/images/guides/user-invites-landing.png
590
+ - docs/public/images/guides/user-profile-edit.png
591
+ - docs/public/images/guides/user-profile-show.png
592
+ - docs/public/images/home-index.png
593
+ - docs/public/images/home-new.png
594
+ - docs/public/images/home-show.png
595
+ - docs/public/images/tutorial/02-empty-index.png
596
+ - docs/public/images/tutorial/02-index-with-posts.png
597
+ - docs/public/images/tutorial/02-new-form-modal.png
598
+ - docs/public/images/tutorial/02-new-form.png
599
+ - docs/public/images/tutorial/03-create-account.png
600
+ - docs/public/images/tutorial/03-login.png
601
+ - docs/public/images/tutorial/04-admin-index.png
602
+ - docs/public/images/tutorial/05-actions-menu.png
603
+ - docs/public/images/tutorial/05-row-actions.png
604
+ - docs/public/images/tutorial/06-comments-tab.png
605
+ - docs/public/images/tutorial/06-post-with-comments.png
606
+ - docs/public/images/tutorial/07-author-dashboard.png
607
+ - docs/public/images/tutorial/07-author-portal.png
608
+ - docs/public/images/tutorial/08-customized-index.png
570
609
  - docs/public/og-image.png
571
610
  - docs/public/plutonium.png
572
611
  - docs/public/site.webmanifest
@@ -620,12 +659,15 @@ files:
620
659
  - docs/superpowers/plans/2026-05-06-multi-invite-model-support.md.tasks.json
621
660
  - docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md
622
661
  - docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json
662
+ - docs/superpowers/plans/2026-05-15-public-pages-overhaul.md
663
+ - docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json
623
664
  - docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md
624
665
  - docs/superpowers/specs/2026-04-14-plutonium-testing-design.md
625
666
  - docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md
626
667
  - docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md
627
668
  - docs/superpowers/specs/2026-05-12-skill-compaction-design.md
628
669
  - docs/superpowers/specs/2026-05-13-docs-restructure-design.md
670
+ - docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md
629
671
  - esbuild.config.js
630
672
  - exe/pug
631
673
  - gemfiles/rails_7.gemfile
@@ -1183,7 +1225,7 @@ metadata:
1183
1225
  homepage_uri: https://radioactive-labs.github.io/plutonium-core/
1184
1226
  source_code_uri: https://github.com/radioactive-labs/plutonium-core
1185
1227
  post_install_message: |
1186
- ⚠️ Plutonium 0.51.0 — breaking change
1228
+ ⚠️ Plutonium 0.52.0 — breaking change
1187
1229
 
1188
1230
  Entity-scoped URL helpers and path params have been renamed from
1189
1231
  `<entity>_scope_*` to `<entity>_scoped_*`.