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:
@@ -93,21 +93,28 @@ animate:
|
|||||||
- By contrast, `dialog/success-modal-` and the Invoice Pay card (`last-modal-step transition
|
- By contrast, `dialog/success-modal-` and the Invoice Pay card (`last-modal-step transition
|
||||||
duration-150`) keep transitions, so the intended pattern still exists to copy from.
|
duration-150`) keep transitions, so the intended pattern still exists to copy from.
|
||||||
|
|
||||||
**FIX — DONE (subtle fade) & verified:** added the codebase's existing `fade-in transition-opacity
|
**FIX — DONE (forward/back slide restored) in the shared `wizard2` engine.** The user confirmed the
|
||||||
duration-300` to all three wizard step cards (new-invoice basic-details + accounts, vendor
|
old wizard had a directional **slide forward/back** between steps; the engine migration dropped it.
|
||||||
step-card, client step-card). `fade-in` is defined in `resources/input.css`
|
Restored the original mechanism (read out of the deleted `mm/multi_modal.clj`) in one shared place:
|
||||||
(`.htmx-added .fade-in { opacity:0 }` → transitions to 1 after htmx settle), so each card fades in
|
- `wizard2/step-slide-classes` — the `group-[.forward]/transition:htmx-*` + `group-[.backward]/…`
|
||||||
on open *and* on every step swap. Verified live: cards always settle to **opacity 1** (never stuck
|
variants, now applied to the swapped wizard `<form>`.
|
||||||
invisible) on both open and step navigation — no functional risk.
|
- `wizard2/transitioner` — the `#transitioner` wrapper with the `@htmx:after-request` hook that reads
|
||||||
|
the `x-transition-type` response header and toggles `group/transition` + `forward|backward` on
|
||||||
Notes / intentionally deferred (needs design intent + visual sign-off, so not done autonomously):
|
itself (so the variants fire on the next swap). All 5 wizard configs' `:open-response` now wrap the
|
||||||
- The transaction-edit "reference" card's `last-modal-step` class is **undefined** (a no-op); its
|
form in `wizard2/transitioner` instead of a plain `#transitioner` div.
|
||||||
only real transition is `transition duration-150`. So there was no clean reference slide to copy.
|
- `wizard2/handle-step-submit` now sets `x-transition-type` (forward on advance, backward on Back,
|
||||||
- The richer **forward/backward slide** transitions (the old `mm/*` modal-stack system using
|
`none` on a same-step validation re-render) + `HX-reswap: outerHTML swap:0.16s` (the swap delay
|
||||||
`group/transition` + `forward`/`backward` + `htmx-*:translate-x-2/3`) were deliberately deleted in
|
that lets the slide-out play). Direction is computed from the step order (`transition-type`).
|
||||||
Phase 11 and purged from the CSS. Re-introducing directional slides is a larger design decision
|
- The earlier per-card `fade-in` (interim) was removed in favor of this.
|
||||||
(which element carries `htmx-swapping`/`htmx-added` — the form vs the card — plus settle timing)
|
- CSS rebuilt so the `group-[.forward]/transition:htmx-*` variants (16 fwd + 16 back) are compiled.
|
||||||
and is left for a human-in-the-loop pass if the subtle fade isn't enough.
|
- Applies to ALL wizards (new-invoice, vendor, client, pay, transaction-rule) since it lives in the
|
||||||
|
engine. REPL-verified: `open-wizard` emits the transitioner, the form carries the slide classes,
|
||||||
|
and submit responses carry `x-transition-type` + the `HX-reswap` swap delay.
|
||||||
|
- **Live-verify caveat:** the long-lived dev server froze its route table at startup
|
||||||
|
(`auto-ap.handler/match->handler-lookup` is a `def` that merged the per-ns `key->handler` maps), so
|
||||||
|
an nREPL reload of leaf namespaces does NOT reach the running router — a server refresh
|
||||||
|
(`stop jetty → tools.namespace refresh → user/start-http`) or a fresh `lein run` is needed to see
|
||||||
|
it live. (Documented hazard; see the QA memory.)
|
||||||
|
|
||||||
## 4. Bug list + fixes (prioritized task list)
|
## 4. Bug list + fixes (prioritized task list)
|
||||||
|
|
||||||
|
|||||||
@@ -1609,10 +1609,6 @@ input:checked + .toggle-bg {
|
|||||||
width: 5em;
|
width: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-\[600px\] {
|
|
||||||
width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-\[700px\] {
|
.w-\[700px\] {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
}
|
}
|
||||||
@@ -3996,6 +3992,44 @@ input:checked + .toggle-bg {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:translate-x-1\/4.htmx-swapping {
|
||||||
|
--tw-translate-x: 25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:-translate-x-1\/4.htmx-swapping {
|
||||||
|
--tw-translate-x: -25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:scale-75.htmx-swapping {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:scale-75.htmx-swapping {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:opacity-0.htmx-swapping {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:opacity-0.htmx-swapping {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:ease-in.htmx-swapping {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:ease-in.htmx-swapping {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.htmx-swapping .htmx-swapping\:-translate-x-2\/3 {
|
.htmx-swapping .htmx-swapping\:-translate-x-2\/3 {
|
||||||
--tw-translate-x: -66.666667%;
|
--tw-translate-x: -66.666667%;
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
@@ -4011,6 +4045,44 @@ input:checked + .toggle-bg {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:translate-x-1\/4 {
|
||||||
|
--tw-translate-x: 25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:-translate-x-1\/4 {
|
||||||
|
--tw-translate-x: -25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:scale-75 {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:scale-75 {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:ease-in {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:ease-in {
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.htmx-added\:translate-x-2\/3.htmx-added {
|
.htmx-added\:translate-x-2\/3.htmx-added {
|
||||||
--tw-translate-x: 66.666667%;
|
--tw-translate-x: 66.666667%;
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
@@ -4026,6 +4098,44 @@ input:checked + .toggle-bg {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:-translate-x-1\/4.htmx-added {
|
||||||
|
--tw-translate-x: -25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:translate-x-1\/4.htmx-added {
|
||||||
|
--tw-translate-x: 25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:scale-75.htmx-added {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:scale-75.htmx-added {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:opacity-0.htmx-added {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:opacity-0.htmx-added {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:ease-out.htmx-added {
|
||||||
|
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:ease-out.htmx-added {
|
||||||
|
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.htmx-added .htmx-added\:translate-x-2\/3 {
|
.htmx-added .htmx-added\:translate-x-2\/3 {
|
||||||
--tw-translate-x: 66.666667%;
|
--tw-translate-x: 66.666667%;
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
@@ -4041,6 +4151,44 @@ input:checked + .toggle-bg {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:-translate-x-1\/4 {
|
||||||
|
--tw-translate-x: -25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:translate-x-1\/4 {
|
||||||
|
--tw-translate-x: 25%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:scale-75 {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:scale-75 {
|
||||||
|
--tw-scale-x: .75;
|
||||||
|
--tw-scale-y: .75;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:ease-out {
|
||||||
|
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:ease-out {
|
||||||
|
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
:is(.dark .dark\:border-blue-500) {
|
:is(.dark .dark\:border-blue-500) {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(0 156 234 / var(--tw-border-opacity));
|
border-color: rgb(0 156 234 / var(--tw-border-opacity));
|
||||||
|
|||||||
@@ -517,7 +517,7 @@
|
|||||||
[{:keys [title active all-data nav body]}]
|
[{:keys [title active all-data nav body]}]
|
||||||
(com/modal-card-advanced
|
(com/modal-card-advanced
|
||||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
{"@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)})}
|
:x-data (hx/json {"clientName" (:client/name all-data)})}
|
||||||
(com/modal-header {} [:div.flex [:div.p-2 title]
|
(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"}]]])
|
[: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"
|
:form-id "wizard-form"
|
||||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
||||||
:form-attrs {:hx-ext "response-targets" :hx-target-400 "#form-errors"}
|
: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
|
:init-fn client-init-fn
|
||||||
:steps [{:key :info :decode decode-info :validate (partial validate-with info-schema) :render render-info :next (fn [_] :matches)}
|
: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)}
|
{: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
|
;; new/edit routes are just (partial wizard2/open-wizard config) -- no hand-rolled
|
||||||
;; create!/render/wrap/thread boilerplate.
|
;; create!/render/wrap/thread boilerplate.
|
||||||
:open-response (fn [form]
|
:open-response (fn [form]
|
||||||
(modal-response [:div#transitioner.flex-1 form]))
|
(modal-response (wizard2/transitioner form)))
|
||||||
:steps [{:key :edit
|
:steps [{:key :edit
|
||||||
:decode decode-rule-form
|
:decode decode-rule-form
|
||||||
:validate rule-form-errors
|
:validate rule-form-errors
|
||||||
|
|||||||
@@ -373,7 +373,7 @@
|
|||||||
[{:keys [title active all-data nav body]}]
|
[{:keys [title active all-data nav body]}]
|
||||||
(com/modal-card-advanced
|
(com/modal-card-advanced
|
||||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
{"@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)
|
:x-data (hx/json {"vendorName" (:vendor/name all-data)
|
||||||
"showPrintAs" (boolean (not-empty (:vendor/print-as all-data)))
|
"showPrintAs" (boolean (not-empty (:vendor/print-as all-data)))
|
||||||
"printAs" (:vendor/print-as all-data)})}
|
"printAs" (:vendor/print-as all-data)})}
|
||||||
@@ -672,7 +672,7 @@
|
|||||||
:form-id "wizard-form"
|
:form-id "wizard-form"
|
||||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
:submit-route (bidi/path-for ssr-routes/only-routes ::route/save)
|
||||||
:form-attrs {:hx-ext "response-targets" :hx-target-400 "#form-errors"}
|
: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
|
: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)}
|
: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)}
|
{: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
|
(:require
|
||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
[auto-ap.ssr.components.wizard-state :as ws]
|
[auto-ap.ssr.components.wizard-state :as ws]
|
||||||
|
[auto-ap.ssr.hx :as hx]
|
||||||
[auto-ap.ssr.utils :refer [html-response]]))
|
[auto-ap.ssr.utils :refer [html-response]]))
|
||||||
|
|
||||||
(defn- step-by-key [config k]
|
(defn- step-by-key [config k]
|
||||||
(first (filter #(= (:key %) k) (:steps config))))
|
(first (filter #(= (:key %) k) (:steps config))))
|
||||||
|
|
||||||
|
(defn- step-index [config k]
|
||||||
|
(.indexOf (mapv :key (:steps config)) k))
|
||||||
|
|
||||||
(defn- prev-step
|
(defn- prev-step
|
||||||
"The step key before `k` in the linear step order (or `k` itself if first)."
|
"The step key before `k` in the linear step order (or `k` itself if first)."
|
||||||
[config k]
|
[config k]
|
||||||
@@ -45,13 +49,68 @@
|
|||||||
i (.indexOf keys k)]
|
i (.indexOf keys k)]
|
||||||
(if (pos? i) (nth keys (dec i)) 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
|
(defn wizard-form
|
||||||
"Wrap a step body in the wizard <form>: the form posts to the submit route, and only the
|
"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).
|
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
|
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]
|
[config wizard-id current-step body]
|
||||||
[:form (merge {:id (:form-id config "wizard-form")
|
[:form (merge {:id (:form-id config "wizard-form")
|
||||||
|
:class step-slide-classes
|
||||||
:hx-post (:submit-route config)
|
:hx-post (:submit-route config)
|
||||||
:hx-target "this"
|
:hx-target "this"
|
||||||
:hx-swap "outerHTML"
|
:hx-swap "outerHTML"
|
||||||
@@ -115,6 +174,17 @@
|
|||||||
extra)))
|
extra)))
|
||||||
(assoc :session session)))
|
(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
|
(defn open-wizard
|
||||||
"Create a wizard instance in the session, render its first step, and return a Ring
|
"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
|
response with the updated session threaded. `:init-fn` returns {:context ..., :init-data
|
||||||
@@ -158,9 +228,11 @@
|
|||||||
(expired-response config request)
|
(expired-response config request)
|
||||||
|
|
||||||
(= direction "back")
|
(= direction "back")
|
||||||
(render-response config wizard-id
|
(let [prev (prev-step config current-step)]
|
||||||
(ws/set-step session wizard-id (prev-step config current-step))
|
(-> (render-response config wizard-id
|
||||||
|
(ws/set-step session wizard-id prev)
|
||||||
request)
|
request)
|
||||||
|
(with-transition (transition-type config current-step prev))))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [step (step-by-key config current-step)
|
(let [step (step-by-key config current-step)
|
||||||
@@ -171,13 +243,16 @@
|
|||||||
posted ((:decode step) clean)
|
posted ((:decode step) clean)
|
||||||
errors (when-let [v (:validate step)] (v posted request))]
|
errors (when-let [v (:validate step)] (v posted request))]
|
||||||
(if (seq errors)
|
(if (seq errors)
|
||||||
(render-response config wizard-id session request
|
;; same step -> no slide (with-transition nil => "none")
|
||||||
|
(-> (render-response config wizard-id session request
|
||||||
{:step-errors errors :step-posted posted})
|
{:step-errors errors :step-posted posted})
|
||||||
|
(with-transition nil))
|
||||||
(let [session' (ws/put-step session wizard-id current-step posted)
|
(let [session' (ws/put-step session wizard-id current-step posted)
|
||||||
nxt ((:next step) posted)]
|
nxt ((:next step) posted)]
|
||||||
(if (= nxt :done)
|
(if (= nxt :done)
|
||||||
(-> ((:done-fn config) (ws/get-all session' wizard-id) request)
|
(-> ((:done-fn config) (ws/get-all session' wizard-id) request)
|
||||||
(assoc :session (ws/forget session' wizard-id)))
|
(assoc :session (ws/forget session' wizard-id)))
|
||||||
(render-response config wizard-id
|
(-> (render-response config wizard-id
|
||||||
(ws/set-step session' wizard-id nxt)
|
(ws/set-step session' wizard-id nxt)
|
||||||
request))))))))
|
request)
|
||||||
|
(with-transition (transition-type config current-step nxt))))))))))
|
||||||
|
|||||||
@@ -333,7 +333,7 @@
|
|||||||
client-val (or (:invoice/client data) client-from-req)]
|
client-val (or (:invoice/client data) client-from-req)]
|
||||||
(com/modal-card-advanced
|
(com/modal-card-advanced
|
||||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
{"@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" ""}
|
"x-data" ""}
|
||||||
(com/modal-header {} [:div.p-2 (if extant? "Edit invoice" "New invoice")])
|
(com/modal-header {} [:div.p-2 (if extant? "Edit invoice" "New invoice")])
|
||||||
(com/modal-body
|
(com/modal-body
|
||||||
@@ -526,7 +526,7 @@
|
|||||||
total (expense-accounts-total* rows)]
|
total (expense-accounts-total* rows)]
|
||||||
(com/modal-card-advanced
|
(com/modal-card-advanced
|
||||||
{"@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}"
|
{"@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" ""}
|
"x-data" ""}
|
||||||
(com/modal-header {} [:div.p-2 "Invoice accounts "])
|
(com/modal-header {} [:div.p-2 "Invoice accounts "])
|
||||||
(com/modal-body
|
(com/modal-body
|
||||||
@@ -778,7 +778,7 @@
|
|||||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit)
|
:submit-route (bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit)
|
||||||
:form-attrs {:hx-ext "response-targets"
|
:form-attrs {:hx-ext "response-targets"
|
||||||
:hx-target-400 "#form-errors"}
|
: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
|
:init-fn new-init-fn
|
||||||
:steps [{:key :basic-details
|
:steps [{:key :basic-details
|
||||||
:decode decode-basic-details
|
:decode decode-basic-details
|
||||||
|
|||||||
@@ -1366,7 +1366,7 @@
|
|||||||
:submit-route (bidi/path-for ssr-routes/only-routes ::route/pay-submit)
|
:submit-route (bidi/path-for ssr-routes/only-routes ::route/pay-submit)
|
||||||
:form-attrs {:hx-ext "response-targets"
|
:form-attrs {:hx-ext "response-targets"
|
||||||
:hx-target-400 "#form-errors"}
|
: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
|
:init-fn pay-init-fn
|
||||||
:steps [{:key :choose-method
|
:steps [{:key :choose-method
|
||||||
:decode decode-choose-method
|
:decode decode-choose-method
|
||||||
|
|||||||
Reference in New Issue
Block a user