diff --git a/sx/sx/docs-content.sx b/sx/sx/docs-content.sx index ae1a6c5..ca14d94 100644 --- a/sx/sx/docs-content.sx +++ b/sx/sx/docs-content.sx @@ -2,7 +2,7 @@ (defcomp ~docs-content/home-content () (div :id "main-content" :class "max-w-3xl mx-auto px-4 py-6" - (~docs/code :code (highlight (component-source "~layouts/header") "lisp")))) + (~home/stepper))) (defcomp ~docs-content/docs-introduction-content () (~docs/page :title "Introduction" diff --git a/sx/sx/home-stepper.sx b/sx/sx/home-stepper.sx new file mode 100644 index 0000000..3dc4463 --- /dev/null +++ b/sx/sx/home-stepper.sx @@ -0,0 +1,206 @@ +(defisland ~home/stepper () + (let ((source "(div (~cssx/tw :tokens \"p-6 rounded-lg border border-stone-200 bg-white 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))) + (step-idx (signal 0)) + (dom-stack-sig (signal (list))) + ;; Code view: list of {text class step-index} tokens + (code-tokens (signal (list)))) + (letrec + ((split-tag (fn (expr result step-counter) + (cond + (not (list? expr)) + (do (append! result {"type" "leaf" "expr" expr}) + (set! step-counter (+ step-counter 1)) + step-counter) + (empty? expr) step-counter + (not (= (type-of (first expr)) "symbol")) + (do (append! result {"type" "leaf" "expr" expr}) + (set! step-counter (+ step-counter 1)) + step-counter) + (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}) + (let ((open-step step-counter)) + (set! step-counter (+ step-counter 1)) + (for-each (fn (c) (set! step-counter (split-tag c result step-counter))) cch) + (append! result {"type" "close" "tag" ctag}) + (set! step-counter (+ step-counter 1)) + step-counter)) + :else + (do (append! result {"type" "expr" "expr" expr}) + (set! step-counter (+ step-counter 1)) + step-counter)))) + ;; Build code tokens from source AST — each token tagged with its step index + (build-code-tokens (fn (expr tokens step-ref indent) + (cond + (string? expr) + (do (append! tokens {"text" (str "\"" expr "\"") "cls" "text-emerald-700" "step" (get step-ref "v")}) + (dict-set! step-ref "v" (+ (get step-ref "v") 1))) + (number? expr) + (do (append! tokens {"text" (str expr) "cls" "text-blue-600" "step" (get step-ref "v")}) + (dict-set! step-ref "v" (+ (get step-ref "v") 1))) + (= (type-of expr) "keyword") + (append! tokens {"text" (str ":" (keyword-name expr)) "cls" "text-violet-600" "step" (get step-ref "v")}) + (= (type-of expr) "symbol") + (let ((name (symbol-name expr))) + (append! tokens {"text" name "cls" + (cond + (is-html-tag? name) "text-rose-600 font-semibold" + (starts-with? name "~") "text-violet-700" + :else "text-blue-800") + "step" (get step-ref "v")})) + (list? expr) + (if (empty? expr) + (append! tokens {"text" "()" "cls" "text-stone-400" "step" (get step-ref "v")}) + (let ((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"))) + ;; Opening paren + (append! tokens {"text" "(" "cls" "text-stone-400" "step" open-step}) + ;; For tags: open step covers tag name + attrs + (when (or is-tag is-comp) + (dict-set! step-ref "v" (+ (get step-ref "v") 1))) + ;; Head + (build-code-tokens head tokens step-ref indent) + ;; Args + (let ((args (rest expr)) + (is-first-line true)) + (for-each (fn (a) + (let ((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)) "~"))))) + (if (and is-tag is-child) + ;; Child element on new line with indent + (do (append! tokens {"text" (str "\n" (join "" (map (fn (_) " ") (range 0 (+ indent 1))))) "cls" "" "step" -1}) + (build-code-tokens a tokens step-ref (+ indent 1))) + ;; Inline arg + (do (append! tokens {"text" " " "cls" "" "step" -1}) + (build-code-tokens a tokens step-ref indent))))) + args)) + ;; Close paren — matches the close step for tags + (when is-tag + (append! tokens {"text" "\n" "cls" "" "step" -1})) + (append! tokens {"text" ")" "cls" "text-stone-400" "step" (if is-tag (get step-ref "v") open-step)}) + (when is-tag + (dict-set! step-ref "v" (+ (get step-ref "v") 1))))) + :else + (append! tokens {"text" (str expr) "cls" "text-stone-500" "step" (get step-ref "v")})))) + (get-preview (fn () (dom-query "[data-sx-lake=\"home-preview\"]"))) + (get-stack (fn () (deref dom-stack-sig))) + (set-stack (fn (v) (reset! dom-stack-sig v))) + (push-stack (fn (el) (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))))))) + (do-step (fn () + (when (< (deref step-idx) (len (deref steps))) + (when (empty? (get-stack)) + (let ((p (get-preview))) + (when p (set-stack (list p))))) + (let ((step (nth (deref steps) (deref step-idx))) + (step-type (get step "type")) + (parent (if (empty? (get-stack)) (get-preview) (last (get-stack))))) + (cond + (= step-type "open") + (let ((el (dom-create-element (get step "tag") nil)) + (attrs (get step "attrs")) + (spreads (or (get step "spreads") (list)))) + (let loop ((i 0)) + (when (< i (len attrs)) + (dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1))) + (loop (+ i 2)))) + (for-each (fn (sp) + (let ((result (eval-expr sp (make-env)))) + (when (and result (spread? result)) + (let ((sattrs (spread-attrs result))) + (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)))) + (keys sattrs)))))) + spreads) + (when parent (dom-append parent el)) + (push-stack el)) + (= step-type "close") + (pop-stack) + (= step-type "leaf") + (when parent + (let ((val (get step "expr"))) + (dom-append parent (create-text-node (if (string? val) val (str val)))))) + (= step-type "expr") + (let ((rendered (render-to-dom (get step "expr") (make-env) nil))) + (when (and parent rendered) + (dom-append parent rendered))))) + (swap! step-idx inc)))) + (do-back (fn () + (when (> (deref step-idx) 0) + (let ((target (- (deref step-idx) 1)) + (container (get-preview))) + (when container (dom-set-prop container "innerHTML" "")) + (set-stack (list (get-preview))) + (reset! step-idx 0) + (for-each (fn (_) (do-step)) (slice (deref steps) 0 target))))))) + ;; Auto-parse via effect + (effect (fn () + (let ((parsed (sx-parse source))) + (when (not (empty? parsed)) + (let ((result (list)) + (step-ref (signal 0))) + (split-tag (first parsed) result 0) + (reset! steps result) + ;; Build code tokens + (let ((tokens (list))) + (dict-set! step-ref "v" 0) + (build-code-tokens (first parsed) tokens step-ref 0) + (reset! code-tokens tokens))))))) + (div :class "space-y-4" + ;; Reactive code view — tokens highlight as you step + (pre :class "text-sm font-mono bg-stone-50 rounded p-4 overflow-x-auto leading-relaxed" + (map (fn (tok) + (let ((step-num (get tok "step")) + (cur (deref step-idx))) + (span :class (str (get tok "cls") + (cond + (= step-num -1) "" + (= step-num cur) " bg-violet-100 rounded font-bold" + (< step-num cur) " opacity-50" + :else "")) + (get tok "text")))) + (deref code-tokens))) + ;; Controls + (div :class "flex items-center justify-center gap-2" + (button :on-click (fn (e) (do-back)) + :class (str "px-3 py-1.5 rounded text-sm " + (if (> (deref step-idx) 0) + "bg-stone-200 text-stone-700 hover:bg-stone-300" + "bg-stone-100 text-stone-300 cursor-not-allowed")) + "\u25c0") + (button :on-click (fn (e) (do-step)) + :class (str "px-3 py-1.5 rounded text-sm " + (if (< (deref step-idx) (len (deref steps))) + "bg-violet-500 text-white hover:bg-violet-600" + "bg-violet-200 text-violet-400 cursor-not-allowed")) + "Step \u25b6") + (span :class "text-xs text-stone-400" (deref step-idx)) + (span :class "text-xs text-stone-400" " / 16")) + ;; Live preview + (lake :id "home-preview")))))