feat(ssr): restore forward/back step slide in the wizard engine
The engine migration replaced the old mm/* modal-stack wizards (which slid forward/back between steps) with wizard2, but never carried the slide over — step transitions went flat. Restore the original mechanism in the shared engine so all wizards (new-invoice, vendor, client, pay, transaction-rule) get it: - wizard2/step-slide-classes: the group-[.forward]/transition:htmx-* and group-[.backward]/* slide variants, applied to the swapped <form>. - wizard2/transitioner: the #transitioner wrapper whose @htmx:after-request hook reads the x-transition-type response header and toggles group/transition + forward|backward on itself. All 5 configs' :open-response now use it. - wizard2/handle-step-submit sets x-transition-type (forward on advance, backward on Back, none on a same-step validation re-render) + HX-reswap "outerHTML swap:0.16s" so the slide-out plays before the swap. Direction computed from step order (transition-type). - Removed the interim per-card fade-in in favor of this. - Rebuilt output.css so the 16 fwd + 16 back slide variants are compiled. REPL-verified: open-wizard emits the transitioner, the form carries the slide classes, and submit responses carry the transition headers. Live verification needs a server refresh (the dev server froze its route table at startup). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -517,7 +517,7 @@
|
||||
[{:keys [title active all-data nav body]}]
|
||||
(com/modal-card-advanced
|
||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
||||
:class " md:w-[820px] md:h-[560px] w-full h-full fade-in transition-opacity duration-300"
|
||||
:class " md:w-[820px] md:h-[560px] w-full h-full"
|
||||
:x-data (hx/json {"clientName" (:client/name all-data)})}
|
||||
(com/modal-header {} [:div.flex [:div.p-2 title]
|
||||
[:p.ml-2.rounded.bg-gray-50.p-2.dark:bg-gray-600 [:span {:x-text "clientName"}]]])
|
||||
@@ -1216,7 +1216,7 @@
|
||||
:form-id "wizard-form"
|
||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
||||
:form-attrs {:hx-ext "response-targets" :hx-target-400 "#form-errors"}
|
||||
:open-response (fn [form] (modal-response [:div#transitioner.flex-1 form]))
|
||||
:open-response (fn [form] (modal-response (wizard2/transitioner form)))
|
||||
:init-fn client-init-fn
|
||||
:steps [{:key :info :decode decode-info :validate (partial validate-with info-schema) :render render-info :next (fn [_] :matches)}
|
||||
{:key :matches :decode (partial decode-with matches-schema) :validate (partial validate-with matches-schema) :render render-matches :next (fn [_] :contact)}
|
||||
|
||||
@@ -827,7 +827,7 @@
|
||||
;; new/edit routes are just (partial wizard2/open-wizard config) -- no hand-rolled
|
||||
;; create!/render/wrap/thread boilerplate.
|
||||
:open-response (fn [form]
|
||||
(modal-response [:div#transitioner.flex-1 form]))
|
||||
(modal-response (wizard2/transitioner form)))
|
||||
:steps [{:key :edit
|
||||
:decode decode-rule-form
|
||||
:validate rule-form-errors
|
||||
|
||||
@@ -373,7 +373,7 @@
|
||||
[{:keys [title active all-data nav body]}]
|
||||
(com/modal-card-advanced
|
||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
||||
:class " md:w-[760px] md:h-[520px] w-full h-full fade-in transition-opacity duration-300"
|
||||
:class " md:w-[760px] md:h-[520px] w-full h-full"
|
||||
:x-data (hx/json {"vendorName" (:vendor/name all-data)
|
||||
"showPrintAs" (boolean (not-empty (:vendor/print-as all-data)))
|
||||
"printAs" (:vendor/print-as all-data)})}
|
||||
@@ -672,7 +672,7 @@
|
||||
:form-id "wizard-form"
|
||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
||||
:form-attrs {:hx-ext "response-targets" :hx-target-400 "#form-errors"}
|
||||
:open-response (fn [form] (modal-response [:div#transitioner.flex-1 form]))
|
||||
:open-response (fn [form] (modal-response (wizard2/transitioner form)))
|
||||
:init-fn vendor-init-fn
|
||||
:steps [{:key :info :decode (partial decode-with info-schema) :validate (partial validate-with info-schema) :render render-info :next (fn [_] :terms)}
|
||||
{:key :terms :decode (partial decode-with terms-schema) :validate (partial validate-with terms-schema) :render render-terms :next (fn [_] :account)}
|
||||
|
||||
@@ -33,11 +33,15 @@
|
||||
(:require
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.wizard-state :as ws]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.utils :refer [html-response]]))
|
||||
|
||||
(defn- step-by-key [config k]
|
||||
(first (filter #(= (:key %) k) (:steps config))))
|
||||
|
||||
(defn- step-index [config k]
|
||||
(.indexOf (mapv :key (:steps config)) k))
|
||||
|
||||
(defn- prev-step
|
||||
"The step key before `k` in the linear step order (or `k` itself if first)."
|
||||
[config k]
|
||||
@@ -45,13 +49,68 @@
|
||||
i (.indexOf keys k)]
|
||||
(if (pos? i) (nth keys (dec i)) k)))
|
||||
|
||||
(defn- transition-type
|
||||
"forward when advancing to a later step, backward when returning to an earlier one, nil
|
||||
when staying on the same step (a validation re-render) — used to drive the slide."
|
||||
[config from-key to-key]
|
||||
(let [fi (step-index config from-key)
|
||||
ti (step-index config to-key)]
|
||||
(cond
|
||||
(or (neg? fi) (neg? ti) (= fi ti)) nil
|
||||
(> fi ti) "backward"
|
||||
:else "forward")))
|
||||
|
||||
(def step-slide-classes
|
||||
"Forward/back slide variants applied to the swapped wizard <form>. The #transitioner
|
||||
ancestor (see `transitioner`) carries `group/transition` + `forward`|`backward`; during a
|
||||
step swap the outgoing form gets `htmx-swapping` and the incoming one `htmx-added`, so
|
||||
these variants animate the card sliding/fading in the matching direction."
|
||||
(str "group-[.forward]/transition:htmx-swapping:opacity-0 "
|
||||
"group-[.forward]/transition:htmx-swapping:-translate-x-1/4 "
|
||||
"group-[.forward]/transition:htmx-swapping:scale-75 "
|
||||
"group-[.forward]/transition:htmx-swapping:ease-in "
|
||||
"group-[.forward]/transition:htmx-added:opacity-0 "
|
||||
"group-[.forward]/transition:htmx-added:scale-75 "
|
||||
"group-[.forward]/transition:htmx-added:translate-x-1/4 "
|
||||
"group-[.forward]/transition:htmx-added:ease-out "
|
||||
"group-[.backward]/transition:htmx-swapping:opacity-0 "
|
||||
"group-[.backward]/transition:htmx-swapping:translate-x-1/4 "
|
||||
"group-[.backward]/transition:htmx-swapping:scale-75 "
|
||||
"group-[.backward]/transition:htmx-swapping:ease-in "
|
||||
"group-[.backward]/transition:htmx-added:opacity-0 "
|
||||
"group-[.backward]/transition:htmx-added:scale-75 "
|
||||
"group-[.backward]/transition:htmx-added:-translate-x-1/4 "
|
||||
"group-[.backward]/transition:htmx-added:ease-out "
|
||||
"opacity-100 translate-x-0 scale-100 transition duration-150"))
|
||||
|
||||
(defn transitioner
|
||||
"Wrap the opened wizard form in the #transitioner the slide animation hooks onto. After
|
||||
each step swap it reads the `x-transition-type` response header and toggles
|
||||
`group/transition` + `forward`|`backward` on itself, which drives `step-slide-classes` on
|
||||
the swapped form. Rendered once on open; the form swaps itself inside it, so this node
|
||||
(and its Alpine state) persists across steps."
|
||||
[form]
|
||||
[:div#transitioner.flex-1
|
||||
{:x-data (hx/json {"transitionType" "none"})
|
||||
:x-ref "transitioner"
|
||||
"@htmx:after-request" (str "if (event.detail.xhr.getResponseHeader('x-transition-type') && "
|
||||
"event.detail.xhr.getResponseHeader('x-transition-type') !== 'none') {"
|
||||
" $refs.transitioner.classList.remove('forward');"
|
||||
" $refs.transitioner.classList.remove('backward');"
|
||||
" $refs.transitioner.classList.add('group/transition');"
|
||||
" $refs.transitioner.classList.add(event.detail.xhr.getResponseHeader('x-transition-type'));"
|
||||
"} else { $refs.transitioner.classList.remove('group/transition'); }")}
|
||||
form])
|
||||
|
||||
(defn wizard-form
|
||||
"Wrap a step body in the wizard <form>: the form posts to the submit route, and only the
|
||||
wizard-id + current-step ride along (no accumulated data — that lives in the session).
|
||||
Enter is guarded so it triggers the step's primary nav button (the one marked
|
||||
`data-primary`) rather than whichever submit button the browser picks first."
|
||||
`data-primary`) rather than whichever submit button the browser picks first. The form
|
||||
carries `step-slide-classes` so the whole-form swap animates as a directional slide."
|
||||
[config wizard-id current-step body]
|
||||
[:form (merge {:id (:form-id config "wizard-form")
|
||||
:class step-slide-classes
|
||||
:hx-post (:submit-route config)
|
||||
:hx-target "this"
|
||||
:hx-swap "outerHTML"
|
||||
@@ -115,6 +174,17 @@
|
||||
extra)))
|
||||
(assoc :session session)))
|
||||
|
||||
(defn- with-transition
|
||||
"Attach the slide headers the #transitioner reads: `x-transition-type` (forward/backward)
|
||||
plus an `HX-reswap` swap delay so the outgoing form's slide-out is visible before the
|
||||
swap. nil tt -> `none` (a same-step validation re-render does not animate)."
|
||||
[resp tt]
|
||||
(if tt
|
||||
(-> resp
|
||||
(assoc-in [:headers "x-transition-type"] tt)
|
||||
(assoc-in [:headers "HX-reswap"] "outerHTML swap:0.16s"))
|
||||
(assoc-in resp [:headers "x-transition-type"] "none")))
|
||||
|
||||
(defn open-wizard
|
||||
"Create a wizard instance in the session, render its first step, and return a Ring
|
||||
response with the updated session threaded. `:init-fn` returns {:context ..., :init-data
|
||||
@@ -158,9 +228,11 @@
|
||||
(expired-response config request)
|
||||
|
||||
(= direction "back")
|
||||
(render-response config wizard-id
|
||||
(ws/set-step session wizard-id (prev-step config current-step))
|
||||
request)
|
||||
(let [prev (prev-step config current-step)]
|
||||
(-> (render-response config wizard-id
|
||||
(ws/set-step session wizard-id prev)
|
||||
request)
|
||||
(with-transition (transition-type config current-step prev))))
|
||||
|
||||
:else
|
||||
(let [step (step-by-key config current-step)
|
||||
@@ -171,13 +243,16 @@
|
||||
posted ((:decode step) clean)
|
||||
errors (when-let [v (:validate step)] (v posted request))]
|
||||
(if (seq errors)
|
||||
(render-response config wizard-id session request
|
||||
{:step-errors errors :step-posted posted})
|
||||
;; same step -> no slide (with-transition nil => "none")
|
||||
(-> (render-response config wizard-id session request
|
||||
{:step-errors errors :step-posted posted})
|
||||
(with-transition nil))
|
||||
(let [session' (ws/put-step session wizard-id current-step posted)
|
||||
nxt ((:next step) posted)]
|
||||
(if (= nxt :done)
|
||||
(-> ((:done-fn config) (ws/get-all session' wizard-id) request)
|
||||
(assoc :session (ws/forget session' wizard-id)))
|
||||
(render-response config wizard-id
|
||||
(ws/set-step session' wizard-id nxt)
|
||||
request))))))))
|
||||
(-> (render-response config wizard-id
|
||||
(ws/set-step session' wizard-id nxt)
|
||||
request)
|
||||
(with-transition (transition-type config current-step nxt))))))))))
|
||||
|
||||
@@ -333,7 +333,7 @@
|
||||
client-val (or (:invoice/client data) client-from-req)]
|
||||
(com/modal-card-advanced
|
||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
||||
:class " md:w-[750px] md:h-[600px] w-full h-full fade-in transition-opacity duration-300"
|
||||
:class " md:w-[750px] md:h-[600px] w-full h-full"
|
||||
"x-data" ""}
|
||||
(com/modal-header {} [:div.p-2 (if extant? "Edit invoice" "New invoice")])
|
||||
(com/modal-body
|
||||
@@ -526,7 +526,7 @@
|
||||
total (expense-accounts-total* rows)]
|
||||
(com/modal-card-advanced
|
||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
||||
:class " md:w-[750px] md:h-[600px] w-full h-full fade-in transition-opacity duration-300"
|
||||
:class " md:w-[750px] md:h-[600px] w-full h-full"
|
||||
"x-data" ""}
|
||||
(com/modal-header {} [:div.p-2 "Invoice accounts "])
|
||||
(com/modal-body
|
||||
@@ -778,7 +778,7 @@
|
||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit)
|
||||
:form-attrs {:hx-ext "response-targets"
|
||||
:hx-target-400 "#form-errors"}
|
||||
:open-response (fn [form] (modal-response [:div#transitioner.flex-1 form]))
|
||||
:open-response (fn [form] (modal-response (wizard2/transitioner form)))
|
||||
:init-fn new-init-fn
|
||||
:steps [{:key :basic-details
|
||||
:decode decode-basic-details
|
||||
|
||||
@@ -1366,7 +1366,7 @@
|
||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/pay-submit)
|
||||
:form-attrs {:hx-ext "response-targets"
|
||||
:hx-target-400 "#form-errors"}
|
||||
:open-response (fn [form] (modal-response [:div#transitioner.flex-1 form]))
|
||||
:open-response (fn [form] (modal-response (wizard2/transitioner form)))
|
||||
:init-fn pay-init-fn
|
||||
:steps [{:key :choose-method
|
||||
:decode decode-choose-method
|
||||
|
||||
Reference in New Issue
Block a user