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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +19 -1
- data/.claude/skills/plutonium-app/SKILL.md +41 -0
- data/.claude/skills/plutonium-auth/SKILL.md +40 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
- data/.claude/skills/plutonium-kanban/SKILL.md +378 -0
- data/.claude/skills/plutonium-resource/SKILL.md +40 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
- data/.claude/skills/plutonium-testing/SKILL.md +38 -0
- data/.claude/skills/plutonium-ui/SKILL.md +51 -0
- data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
- data/.cliff.toml +6 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +576 -439
- data/CLAUDE.md +15 -7
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +1173 -194
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +53 -53
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/layouts/basic.html.erb +7 -0
- data/app/views/plutonium/_flash_toasts.html.erb +2 -46
- data/app/views/plutonium/_toast.html.erb +52 -0
- data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
- data/app/views/resource/_resource_kanban.html.erb +1 -0
- data/app/views/resource/kanban_move_form.html.erb +1 -0
- data/config/brakeman.ignore +2 -2
- data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
- data/docs/.vitepress/config.ts +45 -1
- data/docs/.vitepress/sync-skills.mjs +45 -0
- data/docs/ai.md +99 -0
- data/docs/guides/index.md +2 -0
- data/docs/guides/kanban.md +557 -0
- data/docs/guides/wizards.md +447 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +14 -0
- data/docs/reference/kanban/authorization.md +82 -0
- data/docs/reference/kanban/dsl.md +334 -0
- data/docs/reference/kanban/index.md +40 -0
- data/docs/reference/kanban/positioning.md +162 -0
- data/docs/reference/resource/definition.md +26 -1
- data/docs/reference/resource/model.md +26 -0
- data/docs/reference/ui/forms.md +77 -0
- data/docs/reference/ui/pages.md +2 -0
- data/docs/reference/wizard/anchoring-resume.md +194 -0
- data/docs/reference/wizard/dsl.md +337 -0
- data/docs/reference/wizard/index.md +33 -0
- data/docs/reference/wizard/one-time.md +129 -0
- data/docs/reference/wizard/registration-launch.md +177 -0
- data/docs/reference/wizard/storage-config.md +151 -0
- data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
- data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md +714 -0
- data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md.tasks.json +68 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
- data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
- data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
- data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
- data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
- data/docs/superpowers/specs/2026-07-03-kanban-auth-simplification.md +159 -0
- data/gemfiles/postgres.gemfile +8 -0
- data/gemfiles/postgres.gemfile.lock +321 -0
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +14 -1
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
- data/lib/plutonium/action/base.rb +17 -0
- data/lib/plutonium/auth/rodauth.rb +1 -2
- data/lib/plutonium/configuration.rb +16 -0
- data/lib/plutonium/core/controller.rb +20 -1
- data/lib/plutonium/definition/base.rb +25 -0
- data/lib/plutonium/definition/form_layout.rb +54 -35
- data/lib/plutonium/definition/index_views.rb +70 -1
- data/lib/plutonium/definition/wizards.rb +209 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
- data/lib/plutonium/invites/controller.rb +4 -1
- data/lib/plutonium/kanban/action.rb +7 -0
- data/lib/plutonium/kanban/board.rb +40 -0
- data/lib/plutonium/kanban/broadcaster.rb +54 -0
- data/lib/plutonium/kanban/column.rb +122 -0
- data/lib/plutonium/kanban/context.rb +15 -0
- data/lib/plutonium/kanban/dsl.rb +71 -0
- data/lib/plutonium/kanban/grouping.rb +51 -0
- data/lib/plutonium/kanban/positioning.rb +75 -0
- data/lib/plutonium/kanban.rb +11 -0
- data/lib/plutonium/migrations.rb +40 -0
- data/lib/plutonium/models/has_cents.rb +30 -2
- data/lib/plutonium/positioning.rb +146 -0
- data/lib/plutonium/railtie.rb +33 -0
- data/lib/plutonium/resource/controller.rb +24 -1
- data/lib/plutonium/resource/controllers/crud_actions.rb +9 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +851 -0
- data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
- data/lib/plutonium/resource/policy.rb +14 -0
- data/lib/plutonium/routing/mapper_extensions.rb +45 -0
- data/lib/plutonium/routing/wizard_registration.rb +289 -0
- data/lib/plutonium/ui/display/components/currency.rb +41 -9
- data/lib/plutonium/ui/display/options/inferred_types.rb +2 -5
- data/lib/plutonium/ui/display/resource.rb +17 -12
- data/lib/plutonium/ui/form/base.rb +25 -5
- data/lib/plutonium/ui/form/components/currency.rb +64 -0
- data/lib/plutonium/ui/form/components/intl_tel_input.rb +27 -1
- data/lib/plutonium/ui/form/components/password.rb +126 -0
- data/lib/plutonium/ui/form/components/uppy.rb +26 -5
- data/lib/plutonium/ui/form/kanban_move.rb +46 -0
- data/lib/plutonium/ui/form/options/inferred_types.rb +26 -0
- data/lib/plutonium/ui/form/resource.rb +13 -1
- data/lib/plutonium/ui/form/theme.rb +7 -0
- data/lib/plutonium/ui/form/wizard.rb +63 -0
- data/lib/plutonium/ui/grid/card.rb +56 -18
- data/lib/plutonium/ui/kanban/card.rb +67 -0
- data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
- data/lib/plutonium/ui/kanban/column.rb +411 -0
- data/lib/plutonium/ui/kanban/resource.rb +319 -0
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
- data/lib/plutonium/ui/modal/base.rb +30 -3
- data/lib/plutonium/ui/modal/centered.rb +5 -2
- data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
- data/lib/plutonium/ui/page/index.rb +2 -1
- data/lib/plutonium/ui/page/interactive_action.rb +12 -2
- data/lib/plutonium/ui/page/kanban_move.rb +20 -0
- data/lib/plutonium/ui/page/show.rb +29 -1
- data/lib/plutonium/ui/page/wizard.rb +371 -0
- data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
- data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
- data/lib/plutonium/ui/table/base.rb +1 -1
- data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/ui/wizard/review.rb +196 -0
- data/lib/plutonium/ui/wizard/stepper.rb +122 -0
- data/lib/plutonium/ui/wizard/summary_display.rb +92 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachment_data.rb +42 -0
- data/lib/plutonium/wizard/attachments.rb +226 -0
- data/lib/plutonium/wizard/base.rb +216 -0
- data/lib/plutonium/wizard/base_controller.rb +31 -0
- data/lib/plutonium/wizard/configuration.rb +42 -0
- data/lib/plutonium/wizard/controller.rb +162 -0
- data/lib/plutonium/wizard/data.rb +134 -0
- data/lib/plutonium/wizard/driving.rb +639 -0
- data/lib/plutonium/wizard/dsl.rb +336 -0
- data/lib/plutonium/wizard/errors.rb +27 -0
- data/lib/plutonium/wizard/field_capture.rb +157 -0
- data/lib/plutonium/wizard/field_importer.rb +208 -0
- data/lib/plutonium/wizard/gate.rb +171 -0
- data/lib/plutonium/wizard/instance_key.rb +97 -0
- data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
- data/lib/plutonium/wizard/resume.rb +250 -0
- data/lib/plutonium/wizard/review_step.rb +48 -0
- data/lib/plutonium/wizard/route_resolution.rb +40 -0
- data/lib/plutonium/wizard/runner.rb +684 -0
- data/lib/plutonium/wizard/session.rb +53 -0
- data/lib/plutonium/wizard/state.rb +35 -0
- data/lib/plutonium/wizard/step.rb +61 -0
- data/lib/plutonium/wizard/step_adapter.rb +103 -0
- data/lib/plutonium/wizard/store/active_record.rb +174 -0
- data/lib/plutonium/wizard/store/base.rb +42 -0
- data/lib/plutonium/wizard/store/memory.rb +44 -0
- data/lib/plutonium/wizard/sweep_job.rb +76 -0
- data/lib/plutonium/wizard.rb +86 -0
- data/lib/plutonium.rb +5 -0
- data/lib/rodauth/features/case_insensitive_login.rb +1 -1
- data/lib/tasks/release.rake +144 -191
- data/package.json +7 -5
- data/src/css/components.css +137 -0
- data/src/js/controllers/attachment_input_controller.js +15 -1
- data/src/js/controllers/currency_input_controller.js +39 -0
- data/src/js/controllers/dirty_form_guard_controller.js +155 -27
- data/src/js/controllers/intl_tel_input_controller.js +4 -0
- data/src/js/controllers/kanban_controller.js +717 -0
- data/src/js/controllers/password_sentinel_controller.js +39 -0
- data/src/js/controllers/register_controllers.js +8 -0
- data/src/js/controllers/remote_modal_controller.js +10 -0
- data/src/js/controllers/row_click_controller.js +14 -1
- data/src/js/controllers/wizard_controller.js +54 -0
- data/src/js/turbo/turbo_confirm.js +1 -1
- data/yarn.lock +945 -286
- 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
|
-
|
|
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
|
+
}
|