Stepper: innerHTML batch update replaces per-span DOM manipulation

Replace update-code-highlight's O(n²) for-each loop (79 nth calls +
79 dom-set-attr FFI crossings) with a single innerHTML set via join/map.
Builds the HTML string in WASM, one FFI call to set innerHTML.

858ms → 431ms (WASM) → 101ms (JIT) → 76ms (innerHTML batch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 17:12:56 +00:00
parent 226c01bbf6
commit f33eaf8f3a

View File

@@ -1,310 +1,353 @@
(defisland ~home/stepper () (defisland
(let ((source "(div (~cssx/tw :tokens \"text-center\")\n (h1 (~cssx/tw :tokens \"text-3xl font-bold mb-2\")\n (span (~cssx/tw :tokens \"text-rose-500\") \"the \")\n (span (~cssx/tw :tokens \"text-amber-500\") \"joy \")\n (span (~cssx/tw :tokens \"text-emerald-500\") \"of \")\n (span (~cssx/tw :tokens \"text-violet-600 text-4xl\") \"sx\")))") ~home/stepper
(steps (signal (list))) ()
(step-idx (signal 9)) (let
(dom-stack-sig (signal (list))) ((source "(div (~cssx/tw :tokens \"text-center\")\n (h1 (~cssx/tw :tokens \"text-3xl font-bold mb-2\")\n (span (~cssx/tw :tokens \"text-rose-500\") \"the \")\n (span (~cssx/tw :tokens \"text-amber-500\") \"joy \")\n (span (~cssx/tw :tokens \"text-emerald-500\") \"of \")\n (span (~cssx/tw :tokens \"text-violet-600 text-4xl\") \"sx\")))")
(code-tokens (signal (list))) (steps (signal (list)))
) (step-idx (signal 9))
(dom-stack-sig (signal (list)))
(code-tokens (signal (list))))
(letrec (letrec
((split-tag (fn (expr result) ((split-tag (fn (expr result) (cond (not (list? expr)) (append! result {:expr expr :type "leaf"}) (empty? expr) nil (not (= (type-of (first expr)) "symbol")) (append! result {:expr expr :type "leaf"}) (is-html-tag? (symbol-name (first expr))) (let ((ctag (symbol-name (first expr))) (cargs (rest expr)) (cch (list)) (cat (list)) (spreads (list)) (ckw false)) (for-each (fn (a) (cond (= (type-of a) "keyword") (do (set! ckw true) (append! cat a)) ckw (do (set! ckw false) (append! cat a)) (and (list? a) (not (empty? a)) (= (type-of (first a)) "symbol") (starts-with? (symbol-name (first a)) "~")) (do (set! ckw false) (append! spreads a)) :else (do (set! ckw false) (append! cch a)))) cargs) (append! result {:spreads spreads :tag ctag :type "open" :attrs cat}) (for-each (fn (c) (split-tag c result)) cch) (append! result {:open-attrs cat :open-spreads spreads :tag ctag :type "close"})) :else (append! result {:expr expr :type "expr"}))))
(cond (build-code-tokens
(not (list? expr)) (fn
(append! result {"type" "leaf" "expr" expr}) (expr tokens step-ref indent)
(empty? expr) nil (cond
(not (= (type-of (first expr)) "symbol")) (string? expr)
(append! result {"type" "leaf" "expr" expr}) (do
(is-html-tag? (symbol-name (first expr))) (append! tokens {:cls "text-emerald-700" :step (get step-ref "v") :text (str "\"" expr "\"")})
(let ((ctag (symbol-name (first expr))) (dict-set! step-ref "v" (+ (get step-ref "v") 1)))
(cargs (rest expr)) (number? expr)
(cch (list)) (do
(cat (list)) (append! tokens {:cls "text-amber-700" :step (get step-ref "v") :text (str expr)})
(spreads (list)) (dict-set! step-ref "v" (+ (get step-ref "v") 1)))
(ckw false)) (= (type-of expr) "keyword")
(for-each (fn (a) (append! tokens {:cls "text-violet-600" :step (get step-ref "v") :text (str ":" (keyword-name expr))})
(cond (= (type-of expr) "symbol")
(= (type-of a) "keyword") (do (set! ckw true) (append! cat a)) (let ((name (symbol-name expr))) (append! tokens {:cls (cond (is-html-tag? name) "text-sky-700 font-semibold" (starts-with? name "~") "text-rose-600 font-semibold" :else "text-stone-700") :step (get step-ref "v") :text name}))
ckw (do (set! ckw false) (append! cat a)) (list? expr)
(and (list? a) (not (empty? a)) (when
(= (type-of (first a)) "symbol") (not (empty? expr))
(starts-with? (symbol-name (first a)) "~")) (let
(do (set! ckw false) (append! spreads a)) ((head (first expr))
:else (do (set! ckw false) (append! cch a)))) (is-tag
cargs) (and
(append! result {"type" "open" "tag" ctag "attrs" cat "spreads" spreads}) (= (type-of head) "symbol")
(for-each (fn (c) (split-tag c result)) cch) (is-html-tag? (symbol-name head))))
(append! result {"type" "close" "tag" ctag "open-attrs" cat "open-spreads" spreads})) (is-comp
:else (and
(append! result {"type" "expr" "expr" expr})))) (= (type-of head) "symbol")
(build-code-tokens (fn (expr tokens step-ref indent) (starts-with? (symbol-name head) "~")))
(cond (open-step (get step-ref "v")))
(string? expr) (append! tokens {:cls "text-stone-400" :step open-step :text "("})
(do (append! tokens {"text" (str "\"" expr "\"") "cls" "text-emerald-700" "step" (get step-ref "v")}) (build-code-tokens head tokens step-ref indent)
(dict-set! step-ref "v" (+ (get step-ref "v") 1))) (when
(number? expr) is-tag
(do (append! tokens {"text" (str expr) "cls" "text-amber-700" "step" (get step-ref "v")}) (dict-set! step-ref "v" (+ (get step-ref "v") 1)))
(dict-set! step-ref "v" (+ (get step-ref "v") 1))) (for-each
(= (type-of expr) "keyword") (fn
(append! tokens {"text" (str ":" (keyword-name expr)) "cls" "text-violet-600" "step" (get step-ref "v")}) (a)
(= (type-of expr) "symbol") (let
(let ((name (symbol-name expr))) ((is-child (and (list? a) (not (empty? a)) (= (type-of (first a)) "symbol") (or (is-html-tag? (symbol-name (first a))) (starts-with? (symbol-name (first a)) "~"))))
(append! tokens {"text" name "cls" (is-spread
(cond (and
(is-html-tag? name) "text-sky-700 font-semibold" (list? a)
(starts-with? name "~") "text-rose-600 font-semibold" (not (empty? a))
:else "text-stone-700") (= (type-of (first a)) "symbol")
"step" (get step-ref "v")})) (starts-with? (symbol-name (first a)) "~"))))
(list? expr) (if
(when (not (empty? expr)) is-spread
(let ((head (first expr)) (let
(is-tag (and (= (type-of head) "symbol") (is-html-tag? (symbol-name head)))) ((saved (get step-ref "v"))
(is-comp (and (= (type-of head) "symbol") (starts-with? (symbol-name head) "~"))) (saved-tokens-len (len tokens)))
(open-step (get step-ref "v"))) (append! tokens {:cls "" :step -1 :text " "})
(append! tokens {"text" "(" "cls" "text-stone-400" "step" open-step}) (build-code-tokens a tokens step-ref indent)
(build-code-tokens head tokens step-ref indent) (let
(when is-tag mark-loop
(dict-set! step-ref "v" (+ (get step-ref "v") 1))) ((j saved-tokens-len))
(for-each (fn (a) (when
(let ((is-child (and (list? a) (not (empty? a)) (< j (len tokens))
(= (type-of (first a)) "symbol") (dict-set! (nth tokens j) "spread" true)
(or (is-html-tag? (symbol-name (first a))) (mark-loop (+ j 1))))
(starts-with? (symbol-name (first a)) "~")))) (dict-set! step-ref "v" saved))
(is-spread (and (list? a) (not (empty? a)) (if
(= (type-of (first a)) "symbol") (and is-tag is-child)
(starts-with? (symbol-name (first a)) "~")))) (do
(if is-spread (append! tokens {:cls "" :step -1 :text (str "\n" (join "" (map (fn (_) " ") (range 0 (+ indent 1)))))})
;; Component spread: save counter, process, restore (build-code-tokens
;; All tokens inside share parent open-step a
(let ((saved (get step-ref "v")) tokens
(saved-tokens-len (len tokens))) step-ref
(append! tokens {"text" " " "cls" "" "step" -1}) (+ indent 1)))
(build-code-tokens a tokens step-ref indent) (do
;; Mark all tokens added during spread as spread tokens (append! tokens {:cls "" :step -1 :text " "})
(let mark-loop ((j saved-tokens-len)) (build-code-tokens a tokens step-ref indent))))))
(when (< j (len tokens)) (rest expr))
(dict-set! (nth tokens j) "spread" true) (append! tokens {:cls "text-stone-400" :step open-step :text ")"})
(mark-loop (+ j 1)))) (when
(dict-set! step-ref "v" saved)) is-tag
(if (and is-tag is-child) (dict-set! step-ref "v" (+ (get step-ref "v") 1)))))
(do (append! tokens {"text" (str "\n" (join "" (map (fn (_) " ") (range 0 (+ indent 1))))) "cls" "" "step" -1}) :else nil)))
(build-code-tokens a tokens step-ref (+ indent 1))) (steps-to-preview
(do (append! tokens {"text" " " "cls" "" "step" -1}) (fn
(build-code-tokens a tokens step-ref indent)))))) (all-steps target)
(rest expr)) (if
(append! tokens {"text" ")" "cls" "text-stone-400" "step" open-step}) (or (empty? all-steps) (<= target 0))
(when is-tag nil
(dict-set! step-ref "v" (+ (get step-ref "v") 1))))) (let
:else nil))) ((pos (dict "i" 0)) (max-i (min target (len all-steps))))
(steps-to-preview (fn (all-steps target) (letrec
;; Recursive descent: build SX expression tree from steps[0..target-1]. ((bc-loop (fn (children) (if (>= (get pos "i") max-i) children (let ((step (nth all-steps (get pos "i"))) (stype (get step "type"))) (cond (= stype "open") (do (dict-set! pos "i" (+ (get pos "i") 1)) (let ((tag (get step "tag")) (attrs (or (get step "attrs") (list))) (spreads (or (get step "spreads") (list))) (inner (bc-loop (list)))) (append! children (concat (list (make-symbol tag)) spreads attrs inner))) (bc-loop children)) (= stype "close") (do (dict-set! pos "i" (+ (get pos "i") 1)) children) (= stype "leaf") (do (dict-set! pos "i" (+ (get pos "i") 1)) (append! children (get step "expr")) (bc-loop children)) (= stype "expr") (do (dict-set! pos "i" (+ (get pos "i") 1)) (append! children (get step "expr")) (bc-loop children)) :else (do (dict-set! pos "i" (+ (get pos "i") 1)) (bc-loop children))))))))
;; "open" recurses for children; "close"/end-of-steps returns. (let
;; Unclosed elements close naturally when steps run out. ((root (bc-loop (list))))
(if (or (empty? all-steps) (<= target 0)) (cond
nil (= (len root) 1)
(let ((pos (dict "i" 0)) (first root)
(max-i (min target (len all-steps)))) (empty? root)
(letrec nil
((bc-loop (fn (children) :else (concat (list (make-symbol "<>")) root))))))))
(if (>= (get pos "i") max-i) (get-preview (fn () (dom-query "[data-sx-lake=\"home-preview\"]")))
children (get-code-view (fn () (dom-query "[data-code-view]")))
(let ((step (nth all-steps (get pos "i"))) (get-stack (fn () (deref dom-stack-sig)))
(stype (get step "type"))) (set-stack (fn (v) (reset! dom-stack-sig v)))
(cond (push-stack
(= stype "open") (fn
(do (el)
(dict-set! pos "i" (+ (get pos "i") 1)) (reset! dom-stack-sig (append (deref dom-stack-sig) (list el)))))
(let ((tag (get step "tag")) (pop-stack
(attrs (or (get step "attrs") (list))) (fn
(spreads (or (get step "spreads") (list))) ()
(inner (bc-loop (list)))) (let
(append! children ((s (deref dom-stack-sig)))
(concat (list (make-symbol tag)) spreads attrs inner))) (when
(bc-loop children)) (> (len s) 1)
(= stype "close") (reset! dom-stack-sig (slice s 0 (- (len s) 1)))))))
(do (dict-set! pos "i" (+ (get pos "i") 1)) (build-code-dom (fn () nil))
children) (update-code-highlight
(= stype "leaf") (fn
(do (dict-set! pos "i" (+ (get pos "i") 1)) ()
(append! children (get step "expr")) (let
(bc-loop children)) ((code-el (get-code-view))
(= stype "expr") (cur (deref step-idx))
(do (dict-set! pos "i" (+ (get pos "i") 1)) (tokens (deref code-tokens)))
(append! children (get step "expr")) (when
(bc-loop children)) (and code-el (not (empty? tokens)))
:else (dom-set-prop
(do (dict-set! pos "i" (+ (get pos "i") 1)) code-el
(bc-loop children)))))))) "innerHTML"
(join
(let ((root (bc-loop (list)))) ""
(cond (map
(= (len root) 1) (first root) (fn
(empty? root) nil (tok)
:else (concat (list (make-symbol "<>")) root)))))))) (let
(get-preview (fn () (dom-query "[data-sx-lake=\"home-preview\"]"))) ((step-num (get tok "step"))
(get-code-view (fn () (dom-query "[data-code-view]"))) (base (get tok "cls"))
(get-stack (fn () (deref dom-stack-sig))) (text (replace (get tok "text") "&" "&amp;")))
(set-stack (fn (v) (reset! dom-stack-sig v))) (str
(push-stack (fn (el) (reset! dom-stack-sig (append (deref dom-stack-sig) (list el))))) "<span class=\""
(pop-stack (fn () base
(let ((s (deref dom-stack-sig))) (cond
(when (> (len s) 1) (= step-num -1)
(reset! dom-stack-sig (slice s 0 (- (len s) 1))))))) ""
;; build-code-dom and update-code-highlight removed — (= step-num cur)
;; the code view is now reactive DOM bound to step-idx signal. " bg-amber-100 rounded px-0.5 font-bold text-sm"
;; No imperative DOM manipulation needed. (< step-num cur)
(build-code-dom (fn () nil)) " font-bold text-xs"
(update-code-highlight (fn () :else " opacity-40")
(let ((code-el (get-code-view)) "\">"
(cur (deref step-idx)) text
(tokens (deref code-tokens))) "</span>")))
(when (and code-el (not (empty? tokens))) tokens)))))))
(let ((spans (dom-query-all code-el "span"))) (do-step
(for-each (fn (i) (fn
(when (< i (len tokens)) ()
(let ((sp (nth spans i)) (build-code-dom)
(tok (nth tokens i)) (when
(step-num (get tok "step")) (< (deref step-idx) (len (deref steps)))
(base (get tok "cls")) (when
(is-spread (get tok "spread"))) (empty? (get-stack))
(when (not (= step-num -1)) (let ((p (get-preview))) (when p (set-stack (list p)))))
(dom-set-attr sp "class" (let
(str base ((step (nth (deref steps) (deref step-idx)))
(cond (step-type (get step "type"))
(= step-num cur) " bg-amber-100 rounded px-0.5 font-bold text-sm" (parent
(< step-num cur) " font-bold text-xs" (if
:else " opacity-40"))))))) (empty? (get-stack))
(range 0 (min (len spans) (len tokens))))))))) (get-preview)
(do-step (fn () (last (get-stack)))))
(build-code-dom) (cond
(when (< (deref step-idx) (len (deref steps))) (= step-type "open")
(when (empty? (get-stack)) (let
(let ((p (get-preview))) ((el (dom-create-element (get step "tag") nil))
(when p (set-stack (list p))))) (attrs (get step "attrs"))
(let ((step (nth (deref steps) (deref step-idx))) (spreads (or (get step "spreads") (list))))
(step-type (get step "type")) (let
(parent (if (empty? (get-stack)) (get-preview) (last (get-stack))))) loop
(cond ((i 0))
(= step-type "open") (when
(let ((el (dom-create-element (get step "tag") nil)) (< i (len attrs))
(attrs (get step "attrs")) (dom-set-attr
(spreads (or (get step "spreads") (list)))) el
(let loop ((i 0)) (keyword-name (nth attrs i))
(when (< i (len attrs)) (nth attrs (+ i 1)))
(dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1))) (loop (+ i 2))))
(loop (+ i 2)))) (for-each
;; Evaluate spreads via ~cssx/tw (in scope from island env) (fn
(for-each (fn (sp) (sp)
(when (and (list? sp) (>= (len sp) 3) (when
(= (type-of (nth sp 1)) "keyword") (and
(= (keyword-name (nth sp 1)) "tokens") (list? sp)
(string? (nth sp 2))) (>= (len sp) 3)
(let ((result (trampoline (~cssx/tw :tokens (nth sp 2))))) (= (type-of (nth sp 1)) "keyword")
(when (spread? result) (= (keyword-name (nth sp 1)) "tokens")
(let ((sattrs (spread-attrs result))) (string? (nth sp 2)))
(for-each (fn (k) (let
(if (= k "class") ((result (trampoline (~cssx/tw :tokens (nth sp 2)))))
(dom-set-attr el "class" (when
(str (or (dom-get-attr el "class") "") " " (get sattrs k))) (spread? result)
(dom-set-attr el k (get sattrs k)))) (let
(keys sattrs))))))) ((sattrs (spread-attrs result)))
spreads) (for-each
(when parent (dom-append parent el)) (fn
(push-stack el)) (k)
(= step-type "close") (if
(pop-stack) (= k "class")
(= step-type "leaf") (dom-set-attr
(when parent el
(let ((val (get step "expr"))) "class"
(dom-append parent (create-text-node (if (string? val) val (str val)))))) (str
(= step-type "expr") (or (dom-get-attr el "class") "")
;; Component expressions handled by lake's reactive render " "
nil)) (get sattrs k)))
(swap! step-idx inc) (dom-set-attr el k (get sattrs k))))
(update-code-highlight)))) (keys sattrs)))))))
(rebuild-preview (fn (target) spreads)
;; Rebuild preview in one shot from steps-to-preview (pure SX→DOM) (when parent (dom-append parent el))
(let ((container (get-preview))) (push-stack el))
(when container (= step-type "close")
(dom-set-prop container "innerHTML" "") (pop-stack)
(let ((expr (steps-to-preview (deref steps) target))) (= step-type "leaf")
(when expr (when
(let ((dom (render-to-dom expr (get-render-env nil) nil))) parent
(when dom (let
(dom-append container dom))))) ((val (get step "expr")))
(set-stack (list container)))))) (dom-append
(do-back (fn () parent
(when (> (deref step-idx) 0) (create-text-node (if (string? val) val (str val))))))
(let ((target (- (deref step-idx) 1))) (= step-type "expr")
(rebuild-preview target) nil))
(reset! step-idx target) (swap! step-idx inc)
(update-code-highlight) (update-code-highlight))))
(set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))))))) (rebuild-preview
;; Freeze scope for persistence — same mechanism, cookie storage (fn
(freeze-scope "home-stepper" (fn () (target)
(freeze-signal "step" step-idx))) (let
;; Restore from cookie on mount (server reads cookie too for SSR) ((container (get-preview)))
(let ((saved (get-cookie "sx-home-stepper"))) (when
(when saved container
(dom-set-prop container "innerHTML" "")
(let
((expr (steps-to-preview (deref steps) target)))
(when
expr
(let
((dom (render-to-dom expr (get-render-env nil) nil)))
(when dom (dom-append container dom)))))
(set-stack (list container))))))
(do-back
(fn
()
(when
(> (deref step-idx) 0)
(let
((target (- (deref step-idx) 1)))
(rebuild-preview target)
(reset! step-idx target)
(update-code-highlight)
(set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper")))))))
(freeze-scope "home-stepper" (fn () (freeze-signal "step" step-idx)))
(let
((saved (get-cookie "sx-home-stepper")))
(when
saved
(thaw-from-sx saved) (thaw-from-sx saved)
(when (or (< (deref step-idx) 0) (> (deref step-idx) 16)) (when
(or (< (deref step-idx) 0) (> (deref step-idx) 16))
(reset! step-idx 9)))) (reset! step-idx 9))))
;; Parse source eagerly (pure computation — works in SSR and client) (let
(let ((parsed (sx-parse source))) ((parsed (sx-parse source)))
(when (not (empty? parsed)) (when
(let ((result (list)) (not (empty? parsed))
(step-ref (dict "v" 0))) (let
((result (list)) (step-ref (dict "v" 0)))
(split-tag (first parsed) result) (split-tag (first parsed) result)
(reset! steps result) (reset! steps result)
(let ((tokens (list))) (let
((tokens (list)))
(dict-set! step-ref "v" 0) (dict-set! step-ref "v" 0)
(build-code-tokens (first parsed) tokens step-ref 0) (build-code-tokens (first parsed) tokens step-ref 0)
(reset! code-tokens tokens))))) (reset! code-tokens tokens)))))
;; DOM build via effect (client-only — needs live DOM) (let
(let ((_eff (effect (fn () ((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (rebuild-preview (deref step-idx)) (update-code-highlight) (run-post-render-hooks)))))))
(schedule-idle (fn () (div
(build-code-dom) :class "space-y-4"
(rebuild-preview (deref step-idx)) (div
(update-code-highlight) :data-code-view true
(run-post-render-hooks))))))) (~cssx/tw
(div :class "space-y-4" :tokens "font-mono bg-stone-50 rounded p-2 overflow-x-auto leading-relaxed whitespace-pre-wrap")
;; Code view lake — SSR renders tokenized code with highlighting :style "font-size:0.5rem"
(div :data-code-view true (map
(~cssx/tw :tokens "font-mono bg-stone-50 rounded p-2 overflow-x-auto leading-relaxed whitespace-pre-wrap") (fn
:style "font-size:0.5rem" (tok)
(map (fn (tok) (let
(let ((step (get tok "step")) ((step (get tok "step"))
(cur (deref step-idx)) (cur (deref step-idx))
(is-spread (get tok "spread")) (is-spread (get tok "spread"))
(cls (str (get tok "cls") (cls
(cond (str
(= step -1) "" (get tok "cls")
(= step cur) " bg-amber-100 rounded px-0.5 font-bold text-sm" (cond
(< step cur) " font-bold text-xs" (= step -1)
:else " opacity-40")))) ""
(span :class cls (get tok "text")))) (= step cur)
" bg-amber-100 rounded px-0.5 font-bold text-sm"
(< step cur)
" font-bold text-xs"
:else " opacity-40"))))
(span :class cls (get tok "text"))))
(deref code-tokens))) (deref code-tokens)))
;; Controls (div
(div :class "flex items-center justify-center gap-2 md:gap-3" :class "flex items-center justify-center gap-2 md:gap-3"
(button :on-click (fn (e) (button
(do-back) :on-click (fn
(set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))) (e)
:class (str "px-2 py-1 rounded text-3xl " (do-back)
(if (> (deref step-idx) 0) (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper")))
"text-stone-600 hover:text-stone-800 hover:bg-stone-100" :class (str
"text-stone-300 cursor-not-allowed")) "px-2 py-1 rounded text-3xl "
"\u25c0") (if
(span :class "text-sm text-stone-500 font-mono tabular-nums" (> (deref step-idx) 0)
(deref step-idx) " / " (len (deref steps))) "text-stone-600 hover:text-stone-800 hover:bg-stone-100"
(button :on-click (fn (e) "text-stone-300 cursor-not-allowed"))
(do-step) "◀")
(set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper"))) (span
:class (str "px-2 py-1 rounded text-3xl " :class "text-sm text-stone-500 font-mono tabular-nums"
(if (< (deref step-idx) (len (deref steps))) (deref step-idx)
"text-violet-600 hover:text-violet-800 hover:bg-violet-50" " / "
"text-violet-300 cursor-not-allowed")) (len (deref steps)))
"\u25b6")) (button
;; Live preview — shows partial result up to current step. :on-click (fn
;; Same SX rendered by server (HTML) and client (DOM). (e)
(lake :id "home-preview" (do-step)
(steps-to-preview (deref steps) (deref step-idx)))))))) (set-cookie "sx-home-stepper" (freeze-to-sx "home-stepper")))
:class (str
"px-2 py-1 rounded text-3xl "
(if
(< (deref step-idx) (len (deref steps)))
"text-violet-600 hover:text-violet-800 hover:bg-violet-50"
"text-violet-300 cursor-not-allowed"))
"▶"))
(lake
:id "home-preview"
(steps-to-preview (deref steps) (deref step-idx))))))))