plutonium 0.60.5 → 0.62.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 (199) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +19 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +41 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +40 -0
  5. data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
  6. data/.claude/skills/plutonium-kanban/SKILL.md +378 -0
  7. data/.claude/skills/plutonium-resource/SKILL.md +40 -0
  8. data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
  9. data/.claude/skills/plutonium-testing/SKILL.md +38 -0
  10. data/.claude/skills/plutonium-ui/SKILL.md +51 -0
  11. data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
  12. data/.cliff.toml +6 -0
  13. data/Appraisals +3 -0
  14. data/CHANGELOG.md +576 -439
  15. data/CLAUDE.md +15 -7
  16. data/app/assets/plutonium.css +1 -1
  17. data/app/assets/plutonium.js +1173 -194
  18. data/app/assets/plutonium.js.map +4 -4
  19. data/app/assets/plutonium.min.js +53 -53
  20. data/app/assets/plutonium.min.js.map +4 -4
  21. data/app/views/layouts/basic.html.erb +7 -0
  22. data/app/views/plutonium/_flash_toasts.html.erb +2 -46
  23. data/app/views/plutonium/_toast.html.erb +52 -0
  24. data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
  25. data/app/views/resource/_resource_kanban.html.erb +1 -0
  26. data/app/views/resource/kanban_move_form.html.erb +1 -0
  27. data/config/brakeman.ignore +2 -2
  28. data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
  29. data/docs/.vitepress/config.ts +45 -1
  30. data/docs/.vitepress/sync-skills.mjs +45 -0
  31. data/docs/ai.md +99 -0
  32. data/docs/guides/index.md +2 -0
  33. data/docs/guides/kanban.md +557 -0
  34. data/docs/guides/wizards.md +447 -0
  35. data/docs/public/images/guides/kanban-after-move.png +0 -0
  36. data/docs/public/images/guides/kanban-board-light.png +0 -0
  37. data/docs/public/images/guides/kanban-board.png +0 -0
  38. data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
  39. data/docs/public/images/guides/kanban-wip-toast.png +0 -0
  40. data/docs/public/images/guides/wizards-chooser.png +0 -0
  41. data/docs/public/images/guides/wizards-completed.png +0 -0
  42. data/docs/public/images/guides/wizards-index-action.png +0 -0
  43. data/docs/public/images/guides/wizards-repeater.png +0 -0
  44. data/docs/public/images/guides/wizards-review.png +0 -0
  45. data/docs/public/images/guides/wizards-step.png +0 -0
  46. data/docs/reference/behavior/policies.md +1 -1
  47. data/docs/reference/index.md +14 -0
  48. data/docs/reference/kanban/authorization.md +82 -0
  49. data/docs/reference/kanban/dsl.md +334 -0
  50. data/docs/reference/kanban/index.md +40 -0
  51. data/docs/reference/kanban/positioning.md +162 -0
  52. data/docs/reference/resource/definition.md +26 -1
  53. data/docs/reference/resource/model.md +26 -0
  54. data/docs/reference/ui/forms.md +77 -0
  55. data/docs/reference/ui/pages.md +2 -0
  56. data/docs/reference/wizard/anchoring-resume.md +194 -0
  57. data/docs/reference/wizard/dsl.md +337 -0
  58. data/docs/reference/wizard/index.md +33 -0
  59. data/docs/reference/wizard/one-time.md +129 -0
  60. data/docs/reference/wizard/registration-launch.md +177 -0
  61. data/docs/reference/wizard/storage-config.md +151 -0
  62. data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
  63. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
  64. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
  65. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
  66. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
  67. data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md +714 -0
  68. data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md.tasks.json +68 -0
  69. data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
  70. data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
  71. data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
  72. data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
  73. data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
  74. data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
  75. data/docs/superpowers/specs/2026-07-03-kanban-auth-simplification.md +159 -0
  76. data/gemfiles/postgres.gemfile +8 -0
  77. data/gemfiles/postgres.gemfile.lock +321 -0
  78. data/gemfiles/rails_7.gemfile +1 -0
  79. data/gemfiles/rails_7.gemfile.lock +1 -1
  80. data/gemfiles/rails_8.0.gemfile +1 -0
  81. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  82. data/gemfiles/rails_8.1.gemfile +1 -0
  83. data/gemfiles/rails_8.1.gemfile.lock +14 -1
  84. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
  85. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
  86. data/lib/plutonium/action/base.rb +17 -0
  87. data/lib/plutonium/auth/rodauth.rb +1 -2
  88. data/lib/plutonium/configuration.rb +16 -0
  89. data/lib/plutonium/core/controller.rb +20 -1
  90. data/lib/plutonium/definition/base.rb +25 -0
  91. data/lib/plutonium/definition/form_layout.rb +54 -35
  92. data/lib/plutonium/definition/index_views.rb +70 -1
  93. data/lib/plutonium/definition/wizards.rb +209 -0
  94. data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
  95. data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
  96. data/lib/plutonium/invites/controller.rb +4 -1
  97. data/lib/plutonium/kanban/action.rb +7 -0
  98. data/lib/plutonium/kanban/board.rb +40 -0
  99. data/lib/plutonium/kanban/broadcaster.rb +54 -0
  100. data/lib/plutonium/kanban/column.rb +122 -0
  101. data/lib/plutonium/kanban/context.rb +15 -0
  102. data/lib/plutonium/kanban/dsl.rb +71 -0
  103. data/lib/plutonium/kanban/grouping.rb +51 -0
  104. data/lib/plutonium/kanban/positioning.rb +75 -0
  105. data/lib/plutonium/kanban.rb +11 -0
  106. data/lib/plutonium/migrations.rb +40 -0
  107. data/lib/plutonium/models/has_cents.rb +30 -2
  108. data/lib/plutonium/positioning.rb +146 -0
  109. data/lib/plutonium/railtie.rb +33 -0
  110. data/lib/plutonium/resource/controller.rb +24 -1
  111. data/lib/plutonium/resource/controllers/crud_actions.rb +9 -1
  112. data/lib/plutonium/resource/controllers/kanban_actions.rb +851 -0
  113. data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
  114. data/lib/plutonium/resource/policy.rb +14 -0
  115. data/lib/plutonium/routing/mapper_extensions.rb +45 -0
  116. data/lib/plutonium/routing/wizard_registration.rb +289 -0
  117. data/lib/plutonium/ui/display/components/currency.rb +41 -9
  118. data/lib/plutonium/ui/display/options/inferred_types.rb +2 -5
  119. data/lib/plutonium/ui/display/resource.rb +17 -12
  120. data/lib/plutonium/ui/form/base.rb +25 -5
  121. data/lib/plutonium/ui/form/components/currency.rb +64 -0
  122. data/lib/plutonium/ui/form/components/intl_tel_input.rb +27 -1
  123. data/lib/plutonium/ui/form/components/password.rb +126 -0
  124. data/lib/plutonium/ui/form/components/uppy.rb +26 -5
  125. data/lib/plutonium/ui/form/kanban_move.rb +46 -0
  126. data/lib/plutonium/ui/form/options/inferred_types.rb +26 -0
  127. data/lib/plutonium/ui/form/resource.rb +13 -1
  128. data/lib/plutonium/ui/form/theme.rb +7 -0
  129. data/lib/plutonium/ui/form/wizard.rb +63 -0
  130. data/lib/plutonium/ui/grid/card.rb +56 -18
  131. data/lib/plutonium/ui/kanban/card.rb +67 -0
  132. data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
  133. data/lib/plutonium/ui/kanban/column.rb +411 -0
  134. data/lib/plutonium/ui/kanban/resource.rb +319 -0
  135. data/lib/plutonium/ui/layout/base.rb +1 -1
  136. data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
  137. data/lib/plutonium/ui/modal/base.rb +30 -3
  138. data/lib/plutonium/ui/modal/centered.rb +5 -2
  139. data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
  140. data/lib/plutonium/ui/page/index.rb +2 -1
  141. data/lib/plutonium/ui/page/interactive_action.rb +12 -2
  142. data/lib/plutonium/ui/page/kanban_move.rb +20 -0
  143. data/lib/plutonium/ui/page/show.rb +29 -1
  144. data/lib/plutonium/ui/page/wizard.rb +371 -0
  145. data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
  146. data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
  147. data/lib/plutonium/ui/table/base.rb +1 -1
  148. data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
  149. data/lib/plutonium/ui/table/resource.rb +1 -1
  150. data/lib/plutonium/ui/wizard/review.rb +196 -0
  151. data/lib/plutonium/ui/wizard/stepper.rb +122 -0
  152. data/lib/plutonium/ui/wizard/summary_display.rb +92 -0
  153. data/lib/plutonium/version.rb +1 -1
  154. data/lib/plutonium/wizard/attachment_data.rb +42 -0
  155. data/lib/plutonium/wizard/attachments.rb +226 -0
  156. data/lib/plutonium/wizard/base.rb +216 -0
  157. data/lib/plutonium/wizard/base_controller.rb +31 -0
  158. data/lib/plutonium/wizard/configuration.rb +42 -0
  159. data/lib/plutonium/wizard/controller.rb +162 -0
  160. data/lib/plutonium/wizard/data.rb +134 -0
  161. data/lib/plutonium/wizard/driving.rb +639 -0
  162. data/lib/plutonium/wizard/dsl.rb +336 -0
  163. data/lib/plutonium/wizard/errors.rb +27 -0
  164. data/lib/plutonium/wizard/field_capture.rb +157 -0
  165. data/lib/plutonium/wizard/field_importer.rb +208 -0
  166. data/lib/plutonium/wizard/gate.rb +171 -0
  167. data/lib/plutonium/wizard/instance_key.rb +97 -0
  168. data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
  169. data/lib/plutonium/wizard/resume.rb +250 -0
  170. data/lib/plutonium/wizard/review_step.rb +48 -0
  171. data/lib/plutonium/wizard/route_resolution.rb +40 -0
  172. data/lib/plutonium/wizard/runner.rb +684 -0
  173. data/lib/plutonium/wizard/session.rb +53 -0
  174. data/lib/plutonium/wizard/state.rb +35 -0
  175. data/lib/plutonium/wizard/step.rb +61 -0
  176. data/lib/plutonium/wizard/step_adapter.rb +103 -0
  177. data/lib/plutonium/wizard/store/active_record.rb +174 -0
  178. data/lib/plutonium/wizard/store/base.rb +42 -0
  179. data/lib/plutonium/wizard/store/memory.rb +44 -0
  180. data/lib/plutonium/wizard/sweep_job.rb +76 -0
  181. data/lib/plutonium/wizard.rb +86 -0
  182. data/lib/plutonium.rb +5 -0
  183. data/lib/rodauth/features/case_insensitive_login.rb +1 -1
  184. data/lib/tasks/release.rake +144 -191
  185. data/package.json +7 -5
  186. data/src/css/components.css +137 -0
  187. data/src/js/controllers/attachment_input_controller.js +15 -1
  188. data/src/js/controllers/currency_input_controller.js +39 -0
  189. data/src/js/controllers/dirty_form_guard_controller.js +155 -27
  190. data/src/js/controllers/intl_tel_input_controller.js +4 -0
  191. data/src/js/controllers/kanban_controller.js +717 -0
  192. data/src/js/controllers/password_sentinel_controller.js +39 -0
  193. data/src/js/controllers/register_controllers.js +8 -0
  194. data/src/js/controllers/remote_modal_controller.js +10 -0
  195. data/src/js/controllers/row_click_controller.js +14 -1
  196. data/src/js/controllers/wizard_controller.js +54 -0
  197. data/src/js/turbo/turbo_confirm.js +1 -1
  198. data/yarn.lock +945 -286
  199. metadata +113 -2
@@ -0,0 +1,39 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Guards a password field pre-filled with the server "unchanged" sentinel — a
4
+ // stored secret the user hasn't touched, masked behind a fixed placeholder.
5
+ //
6
+ // The sentinel must be edited all-or-nothing: a partial edit (e.g. one
7
+ // backspace) would leave a truncated sentinel that no longer matches on the
8
+ // server and gets saved as a literal new password. So the first edit gesture
9
+ // replaces the whole field — Backspace/Delete empties it (keep-or-clear), a
10
+ // typed/pasted character starts a fresh value. After that first edit the field
11
+ // behaves natively.
12
+ export default class extends Controller {
13
+ static values = { sentinel: String };
14
+
15
+ connect() {
16
+ this.armed = this.element.value === this.sentinelValue;
17
+ }
18
+
19
+ beforeinput(event) {
20
+ if (!this.armed) return;
21
+
22
+ // Replace the whole sentinel regardless of caret position, so a click into
23
+ // the middle followed by a keystroke can't corrupt it.
24
+ event.preventDefault();
25
+ this.armed = false;
26
+
27
+ let next = "";
28
+ if (event.inputType === "insertText" && event.data != null) {
29
+ next = event.data;
30
+ } else if (event.inputType === "insertFromPaste" && event.dataTransfer) {
31
+ next = event.dataTransfer.getData("text");
32
+ }
33
+ // Backspace/Delete (deleteContent*) and anything else collapse to "".
34
+
35
+ this.element.value = next;
36
+ this.element.setSelectionRange(next.length, next.length);
37
+ this.element.dispatchEvent(new Event("input", { bubbles: true }));
38
+ }
39
+ }
@@ -19,6 +19,7 @@ import AttachmentPreviewController from "./attachment_preview_controller.js"
19
19
  import AttachmentPreviewContainerController from "./attachment_preview_container_controller.js"
20
20
  import SidebarController from "./sidebar_controller.js"
21
21
  import PasswordVisibilityController from "./password_visibility_controller.js"
22
+ import PasswordSentinelController from "./password_sentinel_controller.js"
22
23
  import RemoteModalController from "./remote_modal_controller.js"
23
24
  import KeyValueStoreController from "./key_value_store_controller.js"
24
25
  import BulkActionsController from "./bulk_actions_controller.js"
@@ -34,10 +35,14 @@ import RowClickController from "./row_click_controller.js"
34
35
  import ViewSwitcherController from "./view_switcher_controller.js"
35
36
  import AutosubmitController from "./autosubmit_controller.js"
36
37
  import DirtyFormGuardController from "./dirty_form_guard_controller.js"
38
+ import WizardController from "./wizard_controller.js"
39
+ import KanbanController from "./kanban_controller.js"
40
+ import CurrencyInputController from "./currency_input_controller.js"
37
41
 
38
42
  export default function (application) {
39
43
  // Register controllers here
40
44
  application.register("password-visibility", PasswordVisibilityController)
45
+ application.register("password-sentinel", PasswordSentinelController)
41
46
  application.register("sidebar", SidebarController)
42
47
  application.register("resource-header", ResourceHeaderController)
43
48
  application.register("nested-resource-form-fields", NestedResourceFormFieldsController)
@@ -72,4 +77,7 @@ export default function (application) {
72
77
  application.register("view-switcher", ViewSwitcherController)
73
78
  application.register("autosubmit", AutosubmitController)
74
79
  application.register("dirty-form-guard", DirtyFormGuardController)
80
+ application.register("wizard", WizardController)
81
+ application.register("kanban", KanbanController)
82
+ application.register("currency-input", CurrencyInputController)
75
83
  }
@@ -67,6 +67,16 @@ export default class extends Controller {
67
67
  if (this._closing) return;
68
68
  this._closing = true;
69
69
 
70
+ // Commit any in-flight enter transition to its open end-state before
71
+ // reversing it. Removing data-open while the enter is still running
72
+ // reverses that transition, and CSS shortens the reverse duration in
73
+ // proportion to how far the enter got — so a quick open→close snaps
74
+ // shut instead of animating, which reads as choppy. finish() jumps to
75
+ // the open state so the exit always plays its full duration. Scoped to
76
+ // the dialog's own transitions (not the subtree) so a descendant's
77
+ // infinite animation can't throw on finish().
78
+ this.element.getAnimations().forEach((animation) => animation.finish());
79
+
70
80
  this.element.removeAttribute("data-open");
71
81
 
72
82
  const animations = this.element.getAnimations({ subtree: true });
@@ -16,6 +16,19 @@ export default class extends Controller {
16
16
  if (event.target.closest("a, button, input, label, select, textarea, [data-row-click-ignore]")) {
17
17
  return
18
18
  }
19
- this.element.querySelector('[data-row-click-target="show"]')?.click()
19
+
20
+ const show = this.element.querySelector('[data-row-click-target="show"]')
21
+ if (!show) return
22
+
23
+ // Modifier-click (⌘/Ctrl) or middle-click opens the record's full page in a
24
+ // new tab — the standard "open in new tab" gesture — instead of following
25
+ // the show link's configured target (which may be a modal frame). A new
26
+ // browsing context sends no Turbo-Frame header, so it renders full-page.
27
+ if (event.metaKey || event.ctrlKey || event.button === 1) {
28
+ window.open(show.href, "_blank", "noopener")
29
+ return
30
+ }
31
+
32
+ show.click()
20
33
  }
21
34
  }
@@ -0,0 +1,54 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="wizard"
4
+ //
5
+ // Lightweight glue for the wizard step page. Navigation itself is plain HTML —
6
+ // each Back/Next/Finish/Cancel button submits with its own `name="_direction"`
7
+ // value, so the wizard works without JavaScript. This controller only:
8
+ //
9
+ // - exposes the hidden `_direction` field (the `direction` target) so future
10
+ // client-side affordances (e.g. an autosave or a "save & exit") can set the
11
+ // direction before submitting;
12
+ // - guards against a double submit once a nav button has been pressed.
13
+ export default class extends Controller {
14
+ static targets = ["direction"]
15
+
16
+ connect() {
17
+ this.submitting = false
18
+ this.element.addEventListener("submit", this.onSubmit)
19
+ // Re-enable when the submission's request SETTLES (success or failure), so a
20
+ // real later submit works while a double-click mid-flight stays blocked. A
21
+ // bare setTimeout(0) fired on the next tick — sub-millisecond, long before a
22
+ // human's second click — so it never actually guarded a double-click.
23
+ this.element.addEventListener("turbo:submit-end", this.onSettled)
24
+ // A non-Turbo full-page submit navigates away; on a Turbo back/forward
25
+ // (bfcache) restore the cached form returns with `submitting` still set —
26
+ // clear it so the restored page is usable.
27
+ document.addEventListener("turbo:load", this.onSettled)
28
+ window.addEventListener("pageshow", this.onSettled)
29
+ }
30
+
31
+ disconnect() {
32
+ this.element.removeEventListener("submit", this.onSubmit)
33
+ this.element.removeEventListener("turbo:submit-end", this.onSettled)
34
+ document.removeEventListener("turbo:load", this.onSettled)
35
+ window.removeEventListener("pageshow", this.onSettled)
36
+ }
37
+
38
+ // Set the hidden `_direction` value programmatically (optional helper).
39
+ setDirection(value) {
40
+ if (this.hasDirectionTarget) this.directionTarget.value = value
41
+ }
42
+
43
+ onSubmit = (event) => {
44
+ if (this.submitting) {
45
+ event.preventDefault()
46
+ return
47
+ }
48
+ this.submitting = true
49
+ }
50
+
51
+ onSettled = () => {
52
+ this.submitting = false
53
+ }
54
+ }
@@ -35,7 +35,7 @@ function ensureDialog() {
35
35
  "scale-95",
36
36
  "data-[open]:opacity-100",
37
37
  "data-[open]:scale-100",
38
- "transition-[opacity,transform]",
38
+ "transition-[opacity,scale]",
39
39
  "duration-200",
40
40
  "ease-out",
41
41
  ].join(" ");