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,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Wizard
|
|
5
|
+
# Builds the wizard's typed `data` snapshot (§2.6). `data` is **step-keyed**: a
|
|
6
|
+
# container exposing one typed sub-object per step, so fields are addressed as
|
|
7
|
+
# `data.<step>.<field>` (e.g. `data.identity.name`, `data.profile.tier`). Each
|
|
8
|
+
# step sub-object is backed by ActiveModel::Attributes — scalar values are cast
|
|
9
|
+
# to their declared types and uncollected fields read as `nil`. Step namespacing
|
|
10
|
+
# means two steps may declare the same field name without colliding.
|
|
11
|
+
#
|
|
12
|
+
# `structured_input ..., repeat:` collections (which declare no scalar types —
|
|
13
|
+
# their sub-fields come from `input` declarations) are exposed on their step's
|
|
14
|
+
# sub-object as arrays of typed sub-objects responding to the declared sub-field
|
|
15
|
+
# names (`data.members.invites.first.email`).
|
|
16
|
+
module Data
|
|
17
|
+
# A read-only row inside a structured collection. Responds to each declared
|
|
18
|
+
# sub-field; values are exposed as-is (string-typed, since structured inputs
|
|
19
|
+
# carry no scalar type declarations).
|
|
20
|
+
class StructuredRow
|
|
21
|
+
def initialize(fields, values)
|
|
22
|
+
@values = values
|
|
23
|
+
fields.each do |field|
|
|
24
|
+
define_singleton_method(field) { @values[field.to_s] }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key) = @values[key.to_s]
|
|
29
|
+
|
|
30
|
+
def to_h = @values.dup
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param schema [Hash{Symbol=>Symbol}] scalar attribute name => type
|
|
34
|
+
# @param options [Hash{Symbol=>Hash}] scalar attribute name => options (default:, etc.)
|
|
35
|
+
# @param structured [Hash{Symbol=>Array<Symbol>}] structured name => sub-field names
|
|
36
|
+
# @param validations [Array<[Array, Hash]>] inline `validates` declarations
|
|
37
|
+
# ([args, options]) replayed onto the class so the form pipeline can infer
|
|
38
|
+
# required markers via `validators_on` (§7). The class is never `.valid?`'d
|
|
39
|
+
# in the form path — validation runs through the runner — so this only
|
|
40
|
+
# feeds introspection.
|
|
41
|
+
def self.class_for(schema, options: {}, structured: {}, validations: [])
|
|
42
|
+
Class.new do
|
|
43
|
+
include ActiveModel::Model
|
|
44
|
+
include ActiveModel::Attributes
|
|
45
|
+
|
|
46
|
+
# Anonymous classes have no name, which breaks label/error translation
|
|
47
|
+
# lookups (`human_attribute_name` / `errors.full_messages` call
|
|
48
|
+
# `model_name`). Supply a stable one so the form/display pipelines can
|
|
49
|
+
# humanize attribute labels.
|
|
50
|
+
def self.model_name = ActiveModel::Name.new(self, nil, "Wizard")
|
|
51
|
+
|
|
52
|
+
schema.each do |name, type|
|
|
53
|
+
attribute(name, Plutonium::Wizard.safe_attribute_type(type), **(options[name] || {}))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
validations.each { |args, opts| validates(*args, **opts) }
|
|
57
|
+
|
|
58
|
+
structured.each do |name, fields|
|
|
59
|
+
# Backed by a plain accessor (not an ActiveModel attribute) so the raw
|
|
60
|
+
# array survives without coercion, then wrapped on read.
|
|
61
|
+
attr_writer name
|
|
62
|
+
define_method(name) do
|
|
63
|
+
rows = Array(instance_variable_get(:"@#{name}"))
|
|
64
|
+
rows.map do |row|
|
|
65
|
+
values = row.respond_to?(:to_h) ? row.to_h.transform_keys(&:to_s) : {}
|
|
66
|
+
StructuredRow.new(fields, values)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Accept the union of scalar + structured keys, ignoring unknown keys.
|
|
72
|
+
define_method(:initialize) do |attrs = {}|
|
|
73
|
+
attrs = (attrs || {}).symbolize_keys
|
|
74
|
+
scalar = attrs.slice(*schema.keys)
|
|
75
|
+
super(scalar)
|
|
76
|
+
structured.each_key do |name|
|
|
77
|
+
instance_variable_set(:"@#{name}", attrs[name] || [])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Typed plain-hash view: cast scalars + structured rows as hashes.
|
|
82
|
+
define_method(:to_h) do
|
|
83
|
+
h = {}
|
|
84
|
+
schema.each_key { |name| h[name] = public_send(name) }
|
|
85
|
+
structured.each_key { |name| h[name] = public_send(name).map(&:to_h) }
|
|
86
|
+
h
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The step-keyed `data` snapshot — a thin dispatcher over the per-step typed
|
|
92
|
+
# sub-objects (§2.6). `data.identity` (via method_missing) or `data[:identity]`
|
|
93
|
+
# returns the step's typed sub-object, built lazily from its nested data slice
|
|
94
|
+
# and memoized; an unknown step key returns nil. `to_h` gives the nested
|
|
95
|
+
# `{step => {field => value}}` view.
|
|
96
|
+
#
|
|
97
|
+
# A plain object (not a generated class) so it isn't rebuilt every time the
|
|
98
|
+
# runner reassigns `data_attributes`; the per-step typed classes are built
|
|
99
|
+
# once per wizard class and passed in.
|
|
100
|
+
class Container
|
|
101
|
+
# @param step_classes [Hash{Symbol=>Class}] step key => typed sub-object class
|
|
102
|
+
# @param attrs [Hash] nested staged data ({step_key => {field => value}})
|
|
103
|
+
def initialize(step_classes, attrs = {})
|
|
104
|
+
@step_classes = step_classes
|
|
105
|
+
@attrs = (attrs || {}).transform_keys(&:to_sym)
|
|
106
|
+
@objects = {}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The typed sub-object for a step (lazy + memoized); nil for an unknown step.
|
|
110
|
+
def [](key)
|
|
111
|
+
key = key.to_sym
|
|
112
|
+
return nil unless @step_classes.key?(key)
|
|
113
|
+
@objects[key] ||= @step_classes[key].new(@attrs[key] || {})
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The declared step keys, in order.
|
|
117
|
+
def step_keys = @step_classes.keys
|
|
118
|
+
|
|
119
|
+
# Nested typed hash: {step_key => {field => value}}.
|
|
120
|
+
def to_h = @step_classes.keys.index_with { |key| self[key].to_h }
|
|
121
|
+
|
|
122
|
+
def respond_to_missing?(name, include_private = false)
|
|
123
|
+
@step_classes.key?(name.to_sym) || super
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# `data.identity` → the identity step's typed sub-object.
|
|
127
|
+
def method_missing(name, *args)
|
|
128
|
+
return self[name] if args.empty? && @step_classes.key?(name.to_sym)
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|