plutonium 0.60.5 → 0.61.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 (175) 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 +313 -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 +549 -439
  15. data/CLAUDE.md +15 -7
  16. data/app/assets/plutonium.css +1 -1
  17. data/app/assets/plutonium.js +895 -193
  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/_resource_kanban.html.erb +1 -0
  25. data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
  26. data/docs/.vitepress/config.ts +24 -0
  27. data/docs/guides/index.md +2 -0
  28. data/docs/guides/kanban.md +447 -0
  29. data/docs/guides/wizards.md +447 -0
  30. data/docs/public/images/guides/kanban-after-move.png +0 -0
  31. data/docs/public/images/guides/kanban-board-light.png +0 -0
  32. data/docs/public/images/guides/kanban-board.png +0 -0
  33. data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
  34. data/docs/public/images/guides/kanban-wip-toast.png +0 -0
  35. data/docs/public/images/guides/wizards-chooser.png +0 -0
  36. data/docs/public/images/guides/wizards-completed.png +0 -0
  37. data/docs/public/images/guides/wizards-index-action.png +0 -0
  38. data/docs/public/images/guides/wizards-repeater.png +0 -0
  39. data/docs/public/images/guides/wizards-review.png +0 -0
  40. data/docs/public/images/guides/wizards-step.png +0 -0
  41. data/docs/reference/behavior/policies.md +1 -1
  42. data/docs/reference/index.md +14 -0
  43. data/docs/reference/kanban/authorization.md +62 -0
  44. data/docs/reference/kanban/dsl.md +293 -0
  45. data/docs/reference/kanban/index.md +40 -0
  46. data/docs/reference/kanban/positioning.md +162 -0
  47. data/docs/reference/resource/definition.md +16 -0
  48. data/docs/reference/ui/forms.md +36 -0
  49. data/docs/reference/ui/pages.md +2 -0
  50. data/docs/reference/wizard/anchoring-resume.md +194 -0
  51. data/docs/reference/wizard/dsl.md +332 -0
  52. data/docs/reference/wizard/index.md +33 -0
  53. data/docs/reference/wizard/one-time.md +129 -0
  54. data/docs/reference/wizard/registration-launch.md +177 -0
  55. data/docs/reference/wizard/storage-config.md +151 -0
  56. data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
  57. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
  58. data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
  59. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
  60. data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
  61. data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
  62. data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
  63. data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
  64. data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
  65. data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
  66. data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
  67. data/gemfiles/postgres.gemfile +8 -0
  68. data/gemfiles/postgres.gemfile.lock +321 -0
  69. data/gemfiles/rails_7.gemfile +1 -0
  70. data/gemfiles/rails_7.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.0.gemfile +1 -0
  72. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  73. data/gemfiles/rails_8.1.gemfile +1 -0
  74. data/gemfiles/rails_8.1.gemfile.lock +14 -1
  75. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
  76. data/lib/plutonium/action/base.rb +9 -0
  77. data/lib/plutonium/auth/rodauth.rb +1 -2
  78. data/lib/plutonium/configuration.rb +4 -0
  79. data/lib/plutonium/core/controller.rb +20 -1
  80. data/lib/plutonium/definition/base.rb +25 -0
  81. data/lib/plutonium/definition/form_layout.rb +54 -35
  82. data/lib/plutonium/definition/index_views.rb +54 -1
  83. data/lib/plutonium/definition/wizards.rb +209 -0
  84. data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
  85. data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
  86. data/lib/plutonium/invites/controller.rb +4 -1
  87. data/lib/plutonium/kanban/action.rb +7 -0
  88. data/lib/plutonium/kanban/board.rb +40 -0
  89. data/lib/plutonium/kanban/broadcaster.rb +54 -0
  90. data/lib/plutonium/kanban/column.rb +69 -0
  91. data/lib/plutonium/kanban/context.rb +15 -0
  92. data/lib/plutonium/kanban/dsl.rb +71 -0
  93. data/lib/plutonium/kanban/grouping.rb +51 -0
  94. data/lib/plutonium/kanban/positioning.rb +75 -0
  95. data/lib/plutonium/kanban.rb +11 -0
  96. data/lib/plutonium/migrations.rb +40 -0
  97. data/lib/plutonium/positioning.rb +146 -0
  98. data/lib/plutonium/railtie.rb +33 -0
  99. data/lib/plutonium/resource/controller.rb +2 -0
  100. data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
  101. data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
  102. data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
  103. data/lib/plutonium/resource/policy.rb +8 -0
  104. data/lib/plutonium/routing/mapper_extensions.rb +44 -0
  105. data/lib/plutonium/routing/wizard_registration.rb +289 -0
  106. data/lib/plutonium/ui/display/resource.rb +17 -12
  107. data/lib/plutonium/ui/form/base.rb +19 -5
  108. data/lib/plutonium/ui/form/components/password.rb +126 -0
  109. data/lib/plutonium/ui/form/components/uppy.rb +6 -3
  110. data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
  111. data/lib/plutonium/ui/form/resource.rb +1 -1
  112. data/lib/plutonium/ui/form/wizard.rb +63 -0
  113. data/lib/plutonium/ui/grid/card.rb +16 -5
  114. data/lib/plutonium/ui/kanban/card.rb +67 -0
  115. data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
  116. data/lib/plutonium/ui/kanban/column.rb +324 -0
  117. data/lib/plutonium/ui/kanban/resource.rb +212 -0
  118. data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
  119. data/lib/plutonium/ui/modal/base.rb +30 -3
  120. data/lib/plutonium/ui/modal/centered.rb +5 -2
  121. data/lib/plutonium/ui/page/index.rb +1 -0
  122. data/lib/plutonium/ui/page/show.rb +23 -0
  123. data/lib/plutonium/ui/page/wizard.rb +371 -0
  124. data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
  125. data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
  126. data/lib/plutonium/ui/table/base.rb +1 -1
  127. data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
  128. data/lib/plutonium/ui/wizard/review.rb +196 -0
  129. data/lib/plutonium/ui/wizard/stepper.rb +122 -0
  130. data/lib/plutonium/ui/wizard/summary_display.rb +59 -0
  131. data/lib/plutonium/version.rb +1 -1
  132. data/lib/plutonium/wizard/attachment_data.rb +42 -0
  133. data/lib/plutonium/wizard/attachments.rb +226 -0
  134. data/lib/plutonium/wizard/base.rb +216 -0
  135. data/lib/plutonium/wizard/base_controller.rb +31 -0
  136. data/lib/plutonium/wizard/configuration.rb +42 -0
  137. data/lib/plutonium/wizard/controller.rb +162 -0
  138. data/lib/plutonium/wizard/data.rb +134 -0
  139. data/lib/plutonium/wizard/driving.rb +639 -0
  140. data/lib/plutonium/wizard/dsl.rb +336 -0
  141. data/lib/plutonium/wizard/errors.rb +27 -0
  142. data/lib/plutonium/wizard/field_capture.rb +157 -0
  143. data/lib/plutonium/wizard/field_importer.rb +208 -0
  144. data/lib/plutonium/wizard/gate.rb +171 -0
  145. data/lib/plutonium/wizard/instance_key.rb +97 -0
  146. data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
  147. data/lib/plutonium/wizard/resume.rb +250 -0
  148. data/lib/plutonium/wizard/review_step.rb +48 -0
  149. data/lib/plutonium/wizard/route_resolution.rb +40 -0
  150. data/lib/plutonium/wizard/runner.rb +684 -0
  151. data/lib/plutonium/wizard/session.rb +53 -0
  152. data/lib/plutonium/wizard/state.rb +35 -0
  153. data/lib/plutonium/wizard/step.rb +61 -0
  154. data/lib/plutonium/wizard/step_adapter.rb +103 -0
  155. data/lib/plutonium/wizard/store/active_record.rb +174 -0
  156. data/lib/plutonium/wizard/store/base.rb +42 -0
  157. data/lib/plutonium/wizard/store/memory.rb +44 -0
  158. data/lib/plutonium/wizard/sweep_job.rb +76 -0
  159. data/lib/plutonium/wizard.rb +86 -0
  160. data/lib/plutonium.rb +5 -0
  161. data/lib/rodauth/features/case_insensitive_login.rb +1 -1
  162. data/lib/tasks/release.rake +144 -191
  163. data/package.json +3 -3
  164. data/src/css/components.css +132 -0
  165. data/src/js/controllers/attachment_input_controller.js +15 -1
  166. data/src/js/controllers/dirty_form_guard_controller.js +155 -27
  167. data/src/js/controllers/kanban_controller.js +330 -0
  168. data/src/js/controllers/password_sentinel_controller.js +39 -0
  169. data/src/js/controllers/register_controllers.js +6 -0
  170. data/src/js/controllers/remote_modal_controller.js +10 -0
  171. data/src/js/controllers/row_click_controller.js +14 -1
  172. data/src/js/controllers/wizard_controller.js +54 -0
  173. data/src/js/turbo/turbo_confirm.js +1 -1
  174. data/yarn.lock +271 -282
  175. metadata +100 -1
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Wizard
5
+ # Builds the "continue where you left off" listing (§4.5): for every
6
+ # in-progress {Session} row owned by a user (optionally narrowed to a tenant
7
+ # +scope+), an enriched {Entry} carrying the wizard's label/icon, the current
8
+ # step (+ its label), `updated_at`, and a resolved `resume_url`.
9
+ #
10
+ # A host renders this on a dashboard:
11
+ #
12
+ # Plutonium::Wizard.in_progress_for(view_context)
13
+ #
14
+ # Resume URLs are built in the CURRENT portal (the one whose `view_context` is
15
+ # passed), so a run is only ever linked from the portal it belongs to:
16
+ #
17
+ # - A `register_wizard` (portal/public) wizard draws a NAMED route carrying a
18
+ # `wizard_class` route default; we find it and build the URL from its helper,
19
+ # threading the tenant scope segment and (for tokened runs) the `:token`.
20
+ # - A `wizard`-macro (resource-mounted) ANCHORED wizard's member URL is built by
21
+ # the same `resource_url_for(record, wizard:, step:)` machinery the launch
22
+ # button uses — portal- and scope-correct by construction — from the row's
23
+ # anchor + the registering definition's wizard name.
24
+ #
25
+ # When a row's mount can't be resolved in this portal (e.g. a non-anchored
26
+ # resource-mounted wizard, whose resource identity isn't on the row, or a wizard
27
+ # not mounted here), the entry is returned with `resume_url: nil` and a
28
+ # `resume_unresolved_reason`, rather than guessing or raising.
29
+ module Resume
30
+ # One enriched in-progress wizard, ready for a dashboard list item.
31
+ Entry = Struct.new(
32
+ :wizard_class,
33
+ :label,
34
+ :icon,
35
+ :current_step,
36
+ :current_step_label,
37
+ :updated_at,
38
+ :resume_url,
39
+ :resume_unresolved_reason,
40
+ :session
41
+ )
42
+
43
+ module_function
44
+
45
+ # In-progress entries for the run owner and tenant scope derived from the
46
+ # current portal's +view_context+ (the same object interactions take). A run
47
+ # belongs to exactly one portal context, so the scope MATCHES it: a scoped
48
+ # portal narrows to its tenant; a non-scoped portal narrows to runs with no
49
+ # scope (never another portal's entity-scoped runs). Resume URLs are built
50
+ # through that same view_context, so they land in THIS portal. Newest first.
51
+ #
52
+ # Optional +anchor:+/+wizard:+ filters narrow IN THE QUERY (before enrichment)
53
+ # so discarded rows are never URL-resolved or anchor-loaded — cheaper than
54
+ # filtering the returned array. Both compose; the `wizard + anchor` pair is
55
+ # index-covered by `[:wizard, :anchor_type, :anchor_id, :status]`.
56
+ #
57
+ # @param view_context [ActionView::Base] the current view context
58
+ # @param anchor [ActiveRecord::Base, nil] narrow to runs anchored against this record
59
+ # @param wizard [Class, nil] narrow to runs of this wizard class
60
+ # @return [Array<Entry>]
61
+ def entries_for(view_context, anchor: nil, wizard: nil)
62
+ controller = view_context.controller
63
+ owner = controller.helpers.current_user
64
+ # A guest has no owner-tracked runs — anonymous runs are session-keyed and
65
+ # ownerless (§4.5). The public surface stubs `current_user` to "Guest", so
66
+ # bail rather than query `where(owner: "Guest")` (a non-record). And never
67
+ # normalize "Guest" to nil: `where(owner: nil)` would match EVERY guest's
68
+ # ownerless run — a cross-guest leak.
69
+ return [] unless owner.present? && owner != "Guest"
70
+
71
+ # `current_scoped_entity` is a helper_method — read it off the view context.
72
+ scope = controller.scoped_to_entity? ? view_context.current_scoped_entity : nil
73
+ # The portal pins the listing: a run is only shown by the portal it was
74
+ # launched in. `scope` still isolates the tenant WITHIN a scoped portal —
75
+ # `engine` alone can't (one engine serves every tenant via path scoping).
76
+ engine = view_context.current_engine.name
77
+
78
+ relation = Session.status_in_progress.where(owner: owner, engine: engine, scope: scope)
79
+ relation = relation.where(anchor: anchor) if anchor
80
+ relation = relation.where(wizard: wizard.name) if wizard
81
+
82
+ relation
83
+ .order(updated_at: :desc)
84
+ .filter_map { |row| entry_for(row, view_context) }
85
+ end
86
+
87
+ # @return [Entry, nil] nil when the wizard class can't be loaded
88
+ def entry_for(row, view_context)
89
+ wizard_class = row.wizard.to_s.safe_constantize
90
+ return nil unless wizard_class
91
+
92
+ step = resolve_step(wizard_class, row.current_step)
93
+ resolved = ResumeUrl.new(row, wizard_class, view_context).resolve
94
+
95
+ Entry.new(
96
+ wizard_class: wizard_class,
97
+ label: wizard_class.label,
98
+ icon: wizard_class.icon,
99
+ current_step: row.current_step,
100
+ current_step_label: step&.label,
101
+ updated_at: row.updated_at,
102
+ resume_url: resolved[:url],
103
+ resume_unresolved_reason: resolved[:reason],
104
+ session: row
105
+ )
106
+ end
107
+
108
+ def resolve_step(wizard_class, key)
109
+ return nil if key.blank?
110
+
111
+ wizard_class.steps.find { |s| s.key.to_s == key.to_s }
112
+ end
113
+
114
+ # Resolves a single row to its resume URL in the current portal.
115
+ class ResumeUrl
116
+ def initialize(row, wizard_class, view_context)
117
+ @row = row
118
+ @wizard_class = wizard_class
119
+ @view_context = view_context
120
+ end
121
+
122
+ # @return [Hash] {url:, reason:} — exactly one of the two is non-nil.
123
+ def resolve
124
+ if (named = register_wizard_url)
125
+ return {url: named, reason: nil}
126
+ end
127
+
128
+ if (member = resource_member_url)
129
+ return {url: member, reason: nil}
130
+ end
131
+
132
+ {url: nil, reason: unresolved_reason}
133
+ end
134
+
135
+ private
136
+
137
+ # A `register_wizard` route is named and carries `defaults[:wizard_class]`.
138
+ def register_wizard_url
139
+ route_sets.each do |route_set|
140
+ name = Plutonium::Wizard::RouteResolution.route_name(route_set, @wizard_class, action: "show")
141
+ next unless name
142
+
143
+ return build_url(route_set, name, register_wizard_params)
144
+ end
145
+ nil
146
+ end
147
+
148
+ # Params for a `register_wizard` named helper: the current step, the tenant
149
+ # scope path segment (when the run is scoped), and the URL token for a
150
+ # tokened (no concurrency_key) run.
151
+ def register_wizard_params
152
+ {step: @row.current_step}.merge(scope_param).merge(token_param)
153
+ end
154
+
155
+ # A resource-mounted ANCHORED wizard's member URL is built by the SAME
156
+ # `resource_url_for(record, wizard:, step:)` machinery the launch button uses
157
+ # (§5.1) — so it's portal- and scope-correct by construction (it resolves on
158
+ # the current portal's `current_engine`, threads the entity segment when the
159
+ # portal is path-scoped, and singularizes the member helper). We pass the
160
+ # row's anchor as the record, the registering definition's wizard name, and
161
+ # the resumed step; a tokened (non-keyed) run also carries its run token.
162
+ def resource_member_url
163
+ anchor = @row.anchor
164
+ return nil if anchor.nil?
165
+
166
+ wizard_name = registered_wizard_name
167
+ return nil if wizard_name.nil?
168
+
169
+ @view_context.resource_url_for(anchor, wizard: wizard_name, step: @row.current_step, **token_param)
170
+ rescue => e
171
+ Rails.logger.warn { "[Plutonium::Wizard] resume url build failed for #{@wizard_class.name}: #{e.message}" }
172
+ nil
173
+ end
174
+
175
+ # Reverse-lookup the `wizard`-macro name registered for this wizard class on
176
+ # the anchor's resource definition. nil when not found.
177
+ def registered_wizard_name
178
+ definition = definition_for(@row.anchor)
179
+ return nil unless definition.respond_to?(:registered_wizards)
180
+
181
+ definition.registered_wizards.find do |_name, reg|
182
+ reg[:wizard_class] == @wizard_class
183
+ end&.first
184
+ end
185
+
186
+ def definition_for(record)
187
+ "#{record.class.name}Definition".safe_constantize
188
+ end
189
+
190
+ # The scope path segment for an entity-scoped portal, keyed by the portal
191
+ # engine's own +scoped_entity_param_key+ (which honors a custom +param_key:+
192
+ # passed to +scope_to_entity+), valued from the row's scope record.
193
+ def scope_param
194
+ scope = @row.scope
195
+ return {} if scope.nil?
196
+
197
+ {scoped_entity_param_key => scope.to_param}
198
+ end
199
+
200
+ # The route's scope param key comes from the engine the resume URL is built
201
+ # in — NOT re-derived from the scope model, which would diverge from the
202
+ # actual route segment whenever the portal set a custom `param_key:`.
203
+ def scoped_entity_param_key
204
+ @view_context.current_engine.scoped_entity_param_key
205
+ end
206
+
207
+ # A tokened (no concurrency_key) run carries its per-run id in the URL.
208
+ def token_param
209
+ return {} if @wizard_class.concurrency_key?
210
+ return {} if @row.token.blank?
211
+
212
+ {token: @row.token}
213
+ end
214
+
215
+ def build_url(route_set, route_name, params)
216
+ route_set.url_helpers.public_send(:"#{route_name}_path", **params)
217
+ rescue => e
218
+ Rails.logger.warn { "[Plutonium::Wizard] resume url build failed for #{route_name}: #{e.message}" }
219
+ nil
220
+ end
221
+
222
+ def unresolved_reason
223
+ if @row.anchor && registered_wizard_name.nil?
224
+ "no `wizard` macro registration found for #{@wizard_class.name} " \
225
+ "on #{@row.anchor.class.name}Definition"
226
+ elsif @row.anchor.nil? && resource_mounted_candidate?
227
+ "non-anchored resource-mounted wizard — the row carries no resource " \
228
+ "identity to rebuild its collection URL"
229
+ else
230
+ "no route found for #{@wizard_class.name} (not registered via " \
231
+ "register_wizard or a `wizard` macro mount this resolver can reach)"
232
+ end
233
+ end
234
+
235
+ # Heuristic for the reason text only: the wizard isn't a register_wizard
236
+ # mount (no named route) and has no anchor on the row.
237
+ def resource_mounted_candidate?
238
+ register_wizard_url.nil?
239
+ end
240
+
241
+ # The CURRENT portal's route set, plus the main app's (for `public:` mounts).
242
+ # Scoped to this portal so a `register_wizard` wizard mounted in several
243
+ # portals resolves here, not in whichever engine happens to be scanned first.
244
+ def route_sets
245
+ @route_sets ||= [@view_context.current_engine.routes, Rails.application.routes].uniq
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Wizard
5
+ # The built-in terminal review step (§2.5). Declares no fields of its own;
6
+ # auto-summarizes collected `data` and gates Finish → `execute`. Must be the
7
+ # last declared step.
8
+ class ReviewStep < Step
9
+ # Minimal stand-in for a step's field surface — a review step has none.
10
+ class EmptyFields
11
+ def attribute_schema = {}
12
+
13
+ def attribute_options = {}
14
+
15
+ def inputs = {}
16
+
17
+ def validations = []
18
+
19
+ def imported_form_validators = []
20
+
21
+ def imported_validate_fn = nil
22
+
23
+ def form_layout_sections = nil
24
+
25
+ def defined_structured_inputs = {}
26
+ end
27
+
28
+ attr_reader :block, :summary, :header
29
+
30
+ def initialize(key: :review, label: "Review", description: nil, condition: nil, summary: true, header: true, block: nil)
31
+ super(key:, label:, description:, condition:, fields: EmptyFields.new)
32
+ @summary = summary
33
+ @header = header
34
+ @block = block
35
+ end
36
+
37
+ def review? = true
38
+
39
+ # Whether the auto-summary of completed steps renders in the COMPLETE state
40
+ # (see the `review summary:` macro). Always true in the incomplete state.
41
+ def summary? = @summary
42
+
43
+ # Whether the step-header section (label + prompt) renders above the review
44
+ # body (see the `review header:` macro).
45
+ def header? = @header
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Wizard
5
+ # Resolves a `register_wizard` mount's named routes from a route set by the
6
+ # `wizard_class` route default that every wizard route carries (§5.2/§5.3).
7
+ #
8
+ # `register_wizard` names its helpers from the mount path (`at:`) or an explicit
9
+ # `as:`, NOT from the wizard class name — so `register_wizard W, at: "onboarding"`
10
+ # draws `onboarding_wizard*`, regardless of `W`'s class. Re-deriving a slug from
11
+ # the class name (as the gate once did) only works when the two happen to
12
+ # coincide; everywhere a URL is built for a registered wizard, the route must be
13
+ # looked up by the `wizard_class` default instead.
14
+ #
15
+ # Shared by {Plutonium::Wizard::Controller} (per-step URLs), {Gate} (the entry
16
+ # redirect), and {Resume} (the in-progress listing), so all three track the
17
+ # actual `at:`/`as:` used at registration.
18
+ module RouteResolution
19
+ module_function
20
+
21
+ # The name of the route `register_wizard` drew for the given action and wizard
22
+ # class within +route_set+, or nil if none. Actions: "launch" (the bare mount
23
+ # → resolve/PRG to the run's step), "show" (GET a specific step).
24
+ #
25
+ # @param route_set [ActionDispatch::Routing::RouteSet]
26
+ # @param wizard_class [Class]
27
+ # @param action [String, Symbol] "launch" or "show"
28
+ # @return [Symbol, nil] the route name (e.g. :onboarding_wizard_launch)
29
+ def route_name(route_set, wizard_class, action:)
30
+ route = route_set.routes.find do |r|
31
+ d = r.defaults
32
+ r.name.present? &&
33
+ d[:action].to_s == action.to_s &&
34
+ d[:wizard_class].to_s == wizard_class.name
35
+ end
36
+ route&.name&.to_sym
37
+ end
38
+ end
39
+ end
40
+ end