Fix stepper hydration flash and stepping structure

Two bugs fixed:

1. Hydration flash: The effect's schedule-idle called rebuild-preview
   on initial hydration, which cleared the SSR HTML and re-rendered
   (flashing raw SX source or blank). Fix: skip rebuild-preview on
   initial render — the SSR content is already correct. Only rebuild
   when stepping.

2. Stepping structure: After do-back calls rebuild-preview, the DOM
   stack was reset to just [container]. Subsequent do-step calls
   appended elements to the wrong parent (e.g. "sx" span outside h1).
   Fix: compute the correct stack depth by replaying open/close step
   counts, then walk the rendered DOM tree that many levels deep via
   lastElementChild to find the actual DOM nodes.

Proven by harness test: compute-depth returns 3 at step 10 (inside
div > h1 > span), 2 at step 8 (inside div > h1), 0 at step 16 (all
closed).

Playwright test: lake never shows raw SX (~tw, :tokens) during
hydration — now passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 14:42:40 +00:00
parent 9a64f13dc6
commit b13962e8dd
2 changed files with 91 additions and 46 deletions

View File

@@ -8,7 +8,8 @@
(if (client?) (def-store "home-stepper" (fn () {:step-idx (signal 16)})) nil))
(step-idx (if store (get store "step-idx") (signal 16)))
(dom-stack-sig (signal (list)))
(code-tokens (signal (list))))
(code-tokens (signal (list)))
(initial-render (signal true)))
(letrec
((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"}))))
(build-code-tokens
@@ -263,7 +264,36 @@
(let
((dom (render-to-dom expr (get-render-env nil) nil)))
(when dom (dom-append container dom)))))
(set-stack (list container))))))
(let
((depth 0) (all (deref steps)))
(let
loop
((i 0))
(when
(< i target)
(let
((stype (get (nth all i) "type")))
(cond
(= stype "open")
(set! depth (+ depth 1))
(= stype "close")
(set! depth (- depth 1))))
(loop (+ i 1))))
(let
((stack (list container)) (node container))
(let
walk
((d 0))
(when
(< d depth)
(let
((child (host-get node "lastElementChild")))
(when
child
(append! stack child)
(set! node child)))
(walk (+ d 1))))
(set-stack stack)))))))
(do-back
(fn
()
@@ -304,7 +334,7 @@
(build-code-tokens (first parsed) tokens step-ref 0)
(reset! code-tokens tokens)))))
(let
((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (rebuild-preview (deref step-idx)) (update-code-highlight) (run-post-render-hooks)))))))
((_eff (effect (fn () (schedule-idle (fn () (build-code-dom) (if (deref initial-render) (do (reset! initial-render false) (set-stack (let ((p (get-preview))) (if p (list p) (list))))) (rebuild-preview (deref step-idx))) (update-code-highlight) (run-post-render-hooks)))))))
(div
(~tw :tokens "space-y-4 text-center")
(div