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