plutonium 0.51.0 → 0.53.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 (160) 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-resource/SKILL.md +6 -4
  6. data/.claude/skills/plutonium-tenancy/SKILL.md +31 -7
  7. data/.claude/skills/plutonium-testing/SKILL.md +3 -1
  8. data/.claude/skills/plutonium-ui/SKILL.md +32 -8
  9. data/CHANGELOG.md +33 -0
  10. data/app/assets/plutonium.css +1 -1
  11. data/app/assets/plutonium.js +258 -11
  12. data/app/assets/plutonium.js.map +4 -4
  13. data/app/assets/plutonium.min.js +39 -39
  14. data/app/assets/plutonium.min.js.map +4 -4
  15. data/app/views/plutonium/_resource_header.html.erb +2 -1
  16. data/docs/.vitepress/config.ts +2 -2
  17. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  18. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  19. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  20. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  21. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  22. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  23. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  24. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  25. data/docs/.vitepress/theme/custom.css +144 -0
  26. data/docs/.vitepress/theme/index.ts +58 -1
  27. data/docs/getting-started/index.md +33 -50
  28. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  29. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  30. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  31. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  32. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  33. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  34. data/docs/guides/authentication.md +11 -6
  35. data/docs/guides/authorization.md +3 -3
  36. data/docs/guides/creating-packages.md +8 -11
  37. data/docs/guides/custom-actions.md +8 -2
  38. data/docs/guides/customizing-ui.md +259 -0
  39. data/docs/guides/index.md +49 -32
  40. data/docs/guides/multi-tenancy.md +14 -6
  41. data/docs/guides/nested-resources.md +69 -0
  42. data/docs/guides/search-filtering.md +6 -0
  43. data/docs/guides/testing.md +5 -1
  44. data/docs/guides/theming.md +14 -1
  45. data/docs/guides/user-invites.md +10 -4
  46. data/docs/guides/user-profile.md +8 -0
  47. data/docs/index.md +10 -219
  48. data/docs/public/asciinema/home-scaffold.cast +305 -0
  49. data/docs/public/images/components/avatar.png +0 -0
  50. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  51. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  52. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  53. data/docs/public/images/guides/nested-inputs.png +0 -0
  54. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  55. data/docs/public/images/guides/search-filtering-index.png +0 -0
  56. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  57. data/docs/public/images/guides/theming-after.png +0 -0
  58. data/docs/public/images/guides/theming-before.png +0 -0
  59. data/docs/public/images/guides/user-invites-landing.png +0 -0
  60. data/docs/public/images/guides/user-profile-edit.png +0 -0
  61. data/docs/public/images/guides/user-profile-show.png +0 -0
  62. data/docs/public/images/home-index.png +0 -0
  63. data/docs/public/images/home-new.png +0 -0
  64. data/docs/public/images/home-show.png +0 -0
  65. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  66. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  67. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  68. data/docs/public/images/tutorial/02-new-form.png +0 -0
  69. data/docs/public/images/tutorial/03-create-account.png +0 -0
  70. data/docs/public/images/tutorial/03-login.png +0 -0
  71. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  72. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  73. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  74. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  75. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  76. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  77. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  78. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  79. data/docs/reference/app/generators.md +4 -4
  80. data/docs/reference/auth/accounts.md +7 -8
  81. data/docs/reference/auth/index.md +1 -1
  82. data/docs/reference/behavior/policies.md +2 -2
  83. data/docs/reference/configuration.md +61 -0
  84. data/docs/reference/index.md +67 -55
  85. data/docs/reference/resource/actions.md +2 -1
  86. data/docs/reference/resource/definition.md +5 -4
  87. data/docs/reference/tenancy/entity-scoping.md +14 -8
  88. data/docs/reference/tenancy/index.md +1 -1
  89. data/docs/reference/tenancy/invites.md +12 -5
  90. data/docs/reference/ui/components.md +53 -0
  91. data/docs/reference/ui/forms.md +1 -1
  92. data/docs/reference/ui/pages.md +6 -5
  93. data/docs/reference/ui/tables.md +8 -4
  94. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  95. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  96. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  97. data/docs/superpowers/specs/2026-05-29-avatar-component-design.md +153 -0
  98. data/gemfiles/rails_7.gemfile.lock +1 -1
  99. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  100. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  101. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  102. data/lib/generators/pu/invites/install_generator.rb +44 -0
  103. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  104. data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +7 -3
  105. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  106. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  107. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  108. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  109. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  110. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  111. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  112. data/lib/generators/pu/saas/membership/USAGE +4 -1
  113. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  114. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  115. data/lib/plutonium/action/base.rb +43 -63
  116. data/lib/plutonium/configuration.rb +7 -0
  117. data/lib/plutonium/definition/actions.rb +10 -11
  118. data/lib/plutonium/definition/base.rb +29 -0
  119. data/lib/plutonium/helpers/assets_helper.rb +0 -30
  120. data/lib/plutonium/helpers/content_helper.rb +0 -44
  121. data/lib/plutonium/helpers/display_helper.rb +0 -62
  122. data/lib/plutonium/helpers/turbo_helper.rb +17 -2
  123. data/lib/plutonium/helpers.rb +0 -2
  124. data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
  125. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  126. data/lib/plutonium/resource/definition.rb +0 -42
  127. data/lib/plutonium/ui/action_button.rb +4 -3
  128. data/lib/plutonium/ui/avatar.rb +182 -0
  129. data/lib/plutonium/ui/component/kit.rb +2 -0
  130. data/lib/plutonium/ui/component/methods.rb +1 -0
  131. data/lib/plutonium/ui/form/base.rb +32 -2
  132. data/lib/plutonium/ui/form/components/secure_association.rb +14 -8
  133. data/lib/plutonium/ui/form/interaction.rb +1 -1
  134. data/lib/plutonium/ui/form/resource.rb +58 -0
  135. data/lib/plutonium/ui/form/theme.rb +8 -4
  136. data/lib/plutonium/ui/grid/card.rb +10 -26
  137. data/lib/plutonium/ui/modal/base.rb +36 -1
  138. data/lib/plutonium/ui/modal/centered.rb +24 -6
  139. data/lib/plutonium/ui/modal/slideover.rb +26 -11
  140. data/lib/plutonium/ui/nav_user.rb +3 -23
  141. data/lib/plutonium/ui/page/edit.rb +7 -4
  142. data/lib/plutonium/ui/page/interactive_action.rb +5 -3
  143. data/lib/plutonium/ui/page/new.rb +7 -4
  144. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +1 -1
  145. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  146. data/lib/plutonium/version.rb +1 -1
  147. data/package.json +4 -1
  148. data/src/css/components.css +38 -1
  149. data/src/css/slim_select.css +3 -2
  150. data/src/js/controllers/dirty_form_guard_controller.js +165 -0
  151. data/src/js/controllers/form_controller.js +5 -4
  152. data/src/js/controllers/register_controllers.js +2 -0
  153. data/src/js/controllers/remote_modal_controller.js +53 -19
  154. data/src/js/turbo/index.js +1 -0
  155. data/src/js/turbo/turbo_confirm.js +128 -0
  156. data/yarn.lock +108 -1
  157. metadata +52 -6
  158. data/lib/plutonium/helpers/attachment_helper.rb +0 -73
  159. data/lib/plutonium/helpers/table_helper.rb +0 -35
  160. /data/lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/{password_changed.text.erb → change_password_notify.text.erb} +0 -0
@@ -32,6 +32,7 @@ import CaptureUrlController from "./capture_url_controller.js"
32
32
  import RowClickController from "./row_click_controller.js"
33
33
  import ViewSwitcherController from "./view_switcher_controller.js"
34
34
  import AutosubmitController from "./autosubmit_controller.js"
35
+ import DirtyFormGuardController from "./dirty_form_guard_controller.js"
35
36
 
36
37
  export default function (application) {
37
38
  // Register controllers here
@@ -68,4 +69,5 @@ export default function (application) {
68
69
  application.register("row-click", RowClickController)
69
70
  application.register("view-switcher", ViewSwitcherController)
70
71
  application.register("autosubmit", AutosubmitController)
72
+ application.register("dirty-form-guard", DirtyFormGuardController)
71
73
  }
@@ -1,45 +1,79 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  // Connects to data-controller="remote-modal"
4
+ // Drives the open/close lifecycle of a turbo-fetched <dialog>.
5
+ //
6
+ // Entry is animated by deferring `data-open` to the frame after
7
+ // showModal() — the dialog renders one frame with its closed-state
8
+ // transform/opacity, then transitions into the open state. Exit
9
+ // reverses it: remove `data-open`, wait for the dialog's animations
10
+ // to settle, then call close(). This avoids the @starting-style /
11
+ // allow-discrete spec dance, which is unreliable across browsers.
4
12
  export default class extends Controller {
5
13
  connect() {
6
- // Store original scroll position and body overflow
7
14
  this.originalScrollPosition = window.scrollY;
8
15
  this.originalOverflow = document.body.style.overflow;
9
16
  this.bodyStateRestored = false;
10
-
11
- // Lock body scroll
17
+ this._closing = false;
12
18
  document.body.style.overflow = "hidden";
13
19
 
14
- // Show the modal
15
20
  this.element.showModal();
16
- // Add close event listener
17
- this.element.addEventListener("close", this.handleClose.bind(this));
21
+ // Double rAF ensures the closed-state styles paint before we flip
22
+ // data-open, so the transition actually fires.
23
+ requestAnimationFrame(() => {
24
+ requestAnimationFrame(() => {
25
+ this.element.setAttribute("data-open", "");
26
+ });
27
+ });
28
+
29
+ this.onCancel = this.#onCancel.bind(this);
30
+ this.onClose = this.#onClose.bind(this);
31
+ this.onRequestClose = () => this.#animateClose();
32
+
33
+ this.element.addEventListener("cancel", this.onCancel);
34
+ this.element.addEventListener("close", this.onClose);
35
+ this.element.addEventListener("modal:request-close", this.onRequestClose);
36
+ }
37
+
38
+ disconnect() {
39
+ this.element.removeEventListener("cancel", this.onCancel);
40
+ this.element.removeEventListener("close", this.onClose);
41
+ this.element.removeEventListener("modal:request-close", this.onRequestClose);
42
+ this.#restoreBodyState();
18
43
  }
19
44
 
20
45
  close() {
21
- // Close the modal
22
- this.element.close();
23
- this.restoreBodyState();
46
+ this.#animateClose();
24
47
  }
25
48
 
26
- disconnect() {
27
- // Clean up event listener when controller is disconnected
28
- this.element.removeEventListener("close", this.handleClose);
29
- this.restoreBodyState();
49
+ #onCancel(event) {
50
+ // Another listener (typically dirty-form-guard) already handled
51
+ // this — don't double-process.
52
+ if (event.defaultPrevented) return;
53
+ event.preventDefault();
54
+ this.#animateClose();
30
55
  }
31
56
 
32
- handleClose() {
33
- this.restoreBodyState();
57
+ #onClose() {
58
+ this.#restoreBodyState();
34
59
  }
35
60
 
36
- restoreBodyState() {
61
+ async #animateClose() {
62
+ if (this._closing) return;
63
+ this._closing = true;
64
+
65
+ this.element.removeAttribute("data-open");
66
+
67
+ const animations = this.element.getAnimations({ subtree: true });
68
+ await Promise.allSettled(animations.map((a) => a.finished));
69
+
70
+ this.element.close();
71
+ }
72
+
73
+ #restoreBodyState() {
37
74
  if (this.bodyStateRestored) return;
38
75
  this.bodyStateRestored = true;
39
-
40
- // Restore body overflow
41
76
  document.body.style.overflow = this.originalOverflow || "";
42
- // Restore the original scroll position
43
77
  window.scrollTo(0, this.originalScrollPosition);
44
78
  }
45
79
  }
@@ -1,3 +1,4 @@
1
1
  import "./turbo_actions"
2
+ import "./turbo_confirm"
2
3
  // import "./turbo_debug"
3
4
  // import "./turbo_frame_monkey_patch"
@@ -0,0 +1,128 @@
1
+ // Themed replacement for Turbo's default window.confirm. The dialog is
2
+ // built lazily and reused so per-call cost is just a textContent swap.
3
+
4
+ let dialog;
5
+ let messageEl;
6
+ let confirmButton;
7
+ let cancelButton;
8
+
9
+ function ensureDialog() {
10
+ // Turbo Drive replaces document.body on full-page navigation, which
11
+ // detaches the cached dialog. showModal() then throws InvalidStateError
12
+ // ("not in a Document"). Re-attach if detached; the node itself plus
13
+ // its listeners survive, so we don't have to rebuild.
14
+ if (dialog) {
15
+ if (!dialog.isConnected) document.body.appendChild(dialog);
16
+ return;
17
+ }
18
+
19
+ dialog = document.createElement("dialog");
20
+ // Surface (bg, border, radius, backdrop) comes from .pu-dialog; the
21
+ // remaining utilities are positioning, size, and the opacity/scale
22
+ // animation hooks driven by [data-open]. Matches Modal::Centered.
23
+ dialog.className = [
24
+ "pu-dialog",
25
+ "top-1/2",
26
+ "-translate-y-1/2",
27
+ "left-1/2",
28
+ "-translate-x-1/2",
29
+ "w-full",
30
+ "max-w-md",
31
+ "p-0",
32
+ "open:flex",
33
+ "flex-col",
34
+ "opacity-0",
35
+ "scale-95",
36
+ "data-[open]:opacity-100",
37
+ "data-[open]:scale-100",
38
+ "transition-[opacity,transform]",
39
+ "duration-200",
40
+ "ease-out",
41
+ ].join(" ");
42
+ dialog.setAttribute("aria-labelledby", "pu-turbo-confirm-message");
43
+
44
+ const header = document.createElement("div");
45
+ header.className = "px-6 pt-5 pb-4 border-b border-[var(--pu-border)]";
46
+
47
+ messageEl = document.createElement("h2");
48
+ messageEl.id = "pu-turbo-confirm-message";
49
+ messageEl.className = "text-lg font-semibold text-[var(--pu-text)]";
50
+ header.appendChild(messageEl);
51
+
52
+ const footer = document.createElement("div");
53
+ footer.className = "flex items-center justify-end gap-2 px-6 py-4";
54
+
55
+ cancelButton = document.createElement("button");
56
+ cancelButton.type = "button";
57
+ cancelButton.className = "pu-btn pu-btn-md pu-btn-outline";
58
+ cancelButton.textContent = "Cancel";
59
+
60
+ confirmButton = document.createElement("button");
61
+ confirmButton.type = "button";
62
+ confirmButton.className = "pu-btn pu-btn-md pu-btn-primary";
63
+ confirmButton.textContent = "Confirm";
64
+
65
+ footer.appendChild(cancelButton);
66
+ footer.appendChild(confirmButton);
67
+
68
+ dialog.appendChild(header);
69
+ dialog.appendChild(footer);
70
+ document.body.appendChild(dialog);
71
+ }
72
+
73
+ async function animateClose() {
74
+ dialog.removeAttribute("data-open");
75
+ const animations = dialog.getAnimations({ subtree: true });
76
+ await Promise.allSettled(animations.map((a) => a.finished));
77
+ if (dialog.open) dialog.close();
78
+ }
79
+
80
+ function themedConfirm(message) {
81
+ ensureDialog();
82
+ messageEl.textContent = message || "Are you sure?";
83
+
84
+ return new Promise((resolve) => {
85
+ let settled = false;
86
+
87
+ const settle = (value) => {
88
+ if (settled) return;
89
+ settled = true;
90
+ cleanup();
91
+ resolve(value);
92
+ animateClose();
93
+ };
94
+
95
+ const onConfirm = () => settle(true);
96
+ const onCancel = () => settle(false);
97
+ const onClose = () => settle(false);
98
+
99
+ const cleanup = () => {
100
+ confirmButton.removeEventListener("click", onConfirm);
101
+ cancelButton.removeEventListener("click", onCancel);
102
+ dialog.removeEventListener("close", onClose);
103
+ };
104
+
105
+ confirmButton.addEventListener("click", onConfirm);
106
+ cancelButton.addEventListener("click", onCancel);
107
+ // Esc / backdrop / programmatic close — all resolve as cancel.
108
+ dialog.addEventListener("close", onClose);
109
+
110
+ dialog.showModal();
111
+ // Double rAF so the closed-state styles paint before [data-open]
112
+ // flips — same rationale as remote_modal_controller.
113
+ requestAnimationFrame(() => {
114
+ requestAnimationFrame(() => dialog.setAttribute("data-open", ""));
115
+ });
116
+ confirmButton.focus();
117
+ });
118
+ }
119
+
120
+ if (typeof window !== "undefined" && window.Turbo) {
121
+ // Turbo 8 deprecated setConfirmMethod in favor of config.forms.confirm.
122
+ // Prefer the new path; fall back for older Turbo versions still in use.
123
+ if (window.Turbo.config?.forms) {
124
+ window.Turbo.config.forms.confirm = themedConfirm;
125
+ } else if (window.Turbo.setConfirmMethod) {
126
+ window.Turbo.setConfirmMethod(themedConfirm);
127
+ }
128
+ }
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.53.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-31 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,40 @@ 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/components/avatar.png
581
+ - docs/public/images/guides/custom-actions-bulk.png
582
+ - docs/public/images/guides/multi-tenancy-dashboard.png
583
+ - docs/public/images/guides/multi-tenancy-welcome.png
584
+ - docs/public/images/guides/nested-inputs.png
585
+ - docs/public/images/guides/nested-resources-tab.png
586
+ - docs/public/images/guides/search-filtering-index.png
587
+ - docs/public/images/guides/search-filtering-panel.png
588
+ - docs/public/images/guides/theming-after.png
589
+ - docs/public/images/guides/theming-before.png
590
+ - docs/public/images/guides/user-invites-landing.png
591
+ - docs/public/images/guides/user-profile-edit.png
592
+ - docs/public/images/guides/user-profile-show.png
593
+ - docs/public/images/home-index.png
594
+ - docs/public/images/home-new.png
595
+ - docs/public/images/home-show.png
596
+ - docs/public/images/tutorial/02-empty-index.png
597
+ - docs/public/images/tutorial/02-index-with-posts.png
598
+ - docs/public/images/tutorial/02-new-form-modal.png
599
+ - docs/public/images/tutorial/02-new-form.png
600
+ - docs/public/images/tutorial/03-create-account.png
601
+ - docs/public/images/tutorial/03-login.png
602
+ - docs/public/images/tutorial/04-admin-index.png
603
+ - docs/public/images/tutorial/05-actions-menu.png
604
+ - docs/public/images/tutorial/05-row-actions.png
605
+ - docs/public/images/tutorial/06-comments-tab.png
606
+ - docs/public/images/tutorial/06-post-with-comments.png
607
+ - docs/public/images/tutorial/07-author-dashboard.png
608
+ - docs/public/images/tutorial/07-author-portal.png
609
+ - docs/public/images/tutorial/08-customized-index.png
570
610
  - docs/public/og-image.png
571
611
  - docs/public/plutonium.png
572
612
  - docs/public/site.webmanifest
@@ -594,6 +634,7 @@ files:
594
634
  - docs/reference/behavior/index.md
595
635
  - docs/reference/behavior/interactions.md
596
636
  - docs/reference/behavior/policies.md
637
+ - docs/reference/configuration.md
597
638
  - docs/reference/index.md
598
639
  - docs/reference/resource/actions.md
599
640
  - docs/reference/resource/definition.md
@@ -620,12 +661,16 @@ files:
620
661
  - docs/superpowers/plans/2026-05-06-multi-invite-model-support.md.tasks.json
621
662
  - docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md
622
663
  - docs/superpowers/plans/2026-05-07-ui-layout-overhaul.md.tasks.json
664
+ - docs/superpowers/plans/2026-05-15-public-pages-overhaul.md
665
+ - docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json
623
666
  - docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md
624
667
  - docs/superpowers/specs/2026-04-14-plutonium-testing-design.md
625
668
  - docs/superpowers/specs/2026-05-07-ui-layout-overhaul-design.md
626
669
  - docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md
627
670
  - docs/superpowers/specs/2026-05-12-skill-compaction-design.md
628
671
  - docs/superpowers/specs/2026-05-13-docs-restructure-design.md
672
+ - docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md
673
+ - docs/superpowers/specs/2026-05-29-avatar-component-design.md
629
674
  - esbuild.config.js
630
675
  - exe/pug
631
676
  - gemfiles/rails_7.gemfile
@@ -837,13 +882,13 @@ files:
837
882
  - lib/generators/pu/rodauth/templates/app/rodauth/rodauth_app.rb.tt
838
883
  - lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt
839
884
  - lib/generators/pu/rodauth/templates/app/views/_login_form_footer.html.erb.tt
885
+ - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/change_password_notify.text.erb
840
886
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/email_auth.text.erb
841
887
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/otp_disabled.text.erb
842
888
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/otp_locked_out.text.erb
843
889
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/otp_setup.text.erb
844
890
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/otp_unlock_failed.text.erb
845
891
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/otp_unlocked.text.erb
846
- - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/password_changed.text.erb
847
892
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/reset_password.text.erb
848
893
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/reset_password_notify.text.erb
849
894
  - lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb
@@ -933,10 +978,8 @@ files:
933
978
  - lib/plutonium/helpers.rb
934
979
  - lib/plutonium/helpers/application_helper.rb
935
980
  - lib/plutonium/helpers/assets_helper.rb
936
- - lib/plutonium/helpers/attachment_helper.rb
937
981
  - lib/plutonium/helpers/content_helper.rb
938
982
  - lib/plutonium/helpers/display_helper.rb
939
- - lib/plutonium/helpers/table_helper.rb
940
983
  - lib/plutonium/helpers/turbo_helper.rb
941
984
  - lib/plutonium/helpers/turbo_stream_actions_helper.rb
942
985
  - lib/plutonium/interaction/README.md
@@ -1021,6 +1064,7 @@ files:
1021
1064
  - lib/plutonium/ui.rb
1022
1065
  - lib/plutonium/ui/action_button.rb
1023
1066
  - lib/plutonium/ui/actions_dropdown.rb
1067
+ - lib/plutonium/ui/avatar.rb
1024
1068
  - lib/plutonium/ui/block.rb
1025
1069
  - lib/plutonium/ui/breadcrumbs.rb
1026
1070
  - lib/plutonium/ui/color_mode_selector.rb
@@ -1138,6 +1182,7 @@ files:
1138
1182
  - src/js/controllers/capture_url_controller.js
1139
1183
  - src/js/controllers/clipboard_controller.js
1140
1184
  - src/js/controllers/color_mode_controller.js
1185
+ - src/js/controllers/dirty_form_guard_controller.js
1141
1186
  - src/js/controllers/easymde_controller.js
1142
1187
  - src/js/controllers/filter_panel_controller.js
1143
1188
  - src/js/controllers/flatpickr_controller.js
@@ -1170,6 +1215,7 @@ files:
1170
1215
  - src/js/support/mime_icon.js
1171
1216
  - src/js/turbo/index.js
1172
1217
  - src/js/turbo/turbo_actions.js
1218
+ - src/js/turbo/turbo_confirm.js
1173
1219
  - src/js/turbo/turbo_debug.js
1174
1220
  - src/js/turbo/turbo_frame_monkey_patch.js
1175
1221
  - tailwind.config.js
@@ -1183,7 +1229,7 @@ metadata:
1183
1229
  homepage_uri: https://radioactive-labs.github.io/plutonium-core/
1184
1230
  source_code_uri: https://github.com/radioactive-labs/plutonium-core
1185
1231
  post_install_message: |
1186
- ⚠️ Plutonium 0.51.0 — breaking change
1232
+ ⚠️ Plutonium 0.53.0 — breaking change
1187
1233
 
1188
1234
  Entity-scoped URL helpers and path params have been renamed from
1189
1235
  `<entity>_scope_*` to `<entity>_scoped_*`.
@@ -1,73 +0,0 @@
1
- module Plutonium
2
- module Helpers
3
- module AttachmentHelper
4
- def attachment_preview(attachments, **options)
5
- clamp_content begin
6
- tag.div class: [options[:identity_class], "attachment-preview-container d-flex flex-wrap gap-1 my-1"],
7
- data: {controller: "attachment-preview-container"} do
8
- Array(attachments).each do |attachment|
9
- next unless attachment.url.present?
10
-
11
- concat begin
12
- tag.div class: [options[:identity_class], "attachment-preview d-inline-block text-center"],
13
- title: attachment.filename,
14
- data: {
15
- controller: "attachment-preview",
16
- attachment_preview_mime_type_value: attachment.content_type,
17
- attachment_preview_thumbnail_url_value: _attachment_thumbnail_url(attachment)
18
- } do
19
- tag.figure class: "figure my-1", style: "width: 160px;" do
20
- concat attachment_preview_thumnail(attachment)
21
- concat begin
22
- tag.figcaption class: "figure-caption text-truncate" do
23
- if options[:caption]
24
- caption = options[:caption].is_a?(String) ? options[:caption] : attachment.filename
25
- concat link_to(caption, attachment.url, class: "text-decoration-none", target: :blank)
26
- end
27
-
28
- if block_given?
29
- elements = Array(yield attachment).compact
30
- elements.each { |elem| concat elem }
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end
38
- end
39
- end
40
- end
41
-
42
- def attachment_preview_thumnail(attachment)
43
- return unless attachment.url.present?
44
-
45
- # Any changes made here must be reflected in attachment_input_controller#buildPreviewTemplate
46
-
47
- tag.div class: "bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700", data: {attachment_preview_target: "thumbnail"} do
48
- thumbnail_url = _attachment_thumbnail_url(attachment)
49
- link_body = if thumbnail_url
50
- image_tag thumbnail_url, style: "width:100%; height:100%; object-fit: contain;"
51
- else
52
- _attachment_extension(attachment)
53
- end
54
-
55
- link_to link_body, attachment.url, style: "width:150px; height:150px; line-height: 150px;",
56
- class: "d-block text-decoration-none user-select-none fs-5 font-monospace text-body-secondary",
57
- target: :blank,
58
- data: {attachment_preview_target: "thumbnailLink"}
59
- end
60
- end
61
-
62
- private
63
-
64
- def _attachment_thumbnail_url(attachment)
65
- attachment.url if attachment.representable?
66
- end
67
-
68
- def _attachment_extension(attachment)
69
- attachment.try(:extension) || File.extname(attachment.filename.to_s)
70
- end
71
- end
72
- end
73
- end