sx-tools: WASM kernel updates, TW/CSSX rework, content refresh, new debugging tools
Build tooling: updated OCaml bootstrapper, compile-modules, bundle.sh, sx-build-all. WASM browser: rebuilt sx_browser.bc.js/wasm, sx-platform-2.js, .sxbc bytecode files. CSSX/Tailwind: reworked cssx.sx templates and tw-layout, added tw-type support. Content: refreshed essays, plans, geography, reactive islands, docs, demos, handlers. New tools: bisect_sxbc.sh, test-spa.js, render-trace.sx, morph playwright spec. Tests: added test-match.sx, test-examples.sx, updated test-tw.sx and web tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,407 +1,260 @@
|
||||
;; @client — send all define forms to browser for client-side use.
|
||||
;; CSSX — computed CSS from s-expressions.
|
||||
;;
|
||||
;; Tailwind-style utility component using spread + collect primitives.
|
||||
;; Use as a child of any element — injects classes onto the parent:
|
||||
;;
|
||||
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; "content")
|
||||
;;
|
||||
;; (button (~cssx/tw "hover:bg-rose-500 md:text-xl")
|
||||
;; "click me")
|
||||
;;
|
||||
;; Each token becomes a deterministic class + JIT CSS rule.
|
||||
;; Rules are collected into the "cssx" bucket, flushed once by ~cssx/flush.
|
||||
;; No wrapper elements, no per-element <style> tags.
|
||||
;;
|
||||
;; Reusable style variables:
|
||||
;; (define fancy (~cssx/tw "font-bold text-violet-700 text-4xl"))
|
||||
;; (div fancy "styled content")
|
||||
;;
|
||||
;; This is one instance of the CSSX component pattern — other styling
|
||||
;; components are possible with different vocabulary.
|
||||
|
||||
;; =========================================================================
|
||||
;; Colour data — hue/saturation bases + shade-to-lightness curve
|
||||
;; =========================================================================
|
||||
|
||||
(define colour-bases
|
||||
{"violet" {"h" 263 "s" 70}
|
||||
"purple" {"h" 271 "s" 81}
|
||||
"indigo" {"h" 239 "s" 84}
|
||||
"blue" {"h" 217 "s" 91}
|
||||
"sky" {"h" 199 "s" 89}
|
||||
"cyan" {"h" 188 "s" 94}
|
||||
"teal" {"h" 173 "s" 80}
|
||||
"emerald" {"h" 160 "s" 84}
|
||||
"green" {"h" 142 "s" 71}
|
||||
"lime" {"h" 84 "s" 78}
|
||||
"yellow" {"h" 48 "s" 96}
|
||||
"amber" {"h" 38 "s" 92}
|
||||
"orange" {"h" 25 "s" 95}
|
||||
"red" {"h" 0 "s" 72}
|
||||
"rose" {"h" 350 "s" 89}
|
||||
"pink" {"h" 330 "s" 81}
|
||||
"stone" {"h" 25 "s" 6}
|
||||
"slate" {"h" 215 "s" 16}
|
||||
"gray" {"h" 220 "s" 9}
|
||||
"zinc" {"h" 240 "s" 5}
|
||||
"neutral" {"h" 0 "s" 0}
|
||||
"white" {"h" 0 "s" 0}
|
||||
"black" {"h" 0 "s" 0}})
|
||||
(define colour-bases {:orange {:s 95 :h 25} :cyan {:s 94 :h 188} :sky {:s 89 :h 199} :pink {:s 81 :h 330} :zinc {:s 5 :h 240} :amber {:s 92 :h 38} :neutral {:s 0 :h 0} :lime {:s 78 :h 84} :violet {:s 70 :h 263} :stone {:s 6 :h 25} :black {:s 0 :h 0} :teal {:s 80 :h 173} :gray {:s 9 :h 220} :red {:s 72 :h 0} :rose {:s 89 :h 350} :blue {:s 91 :h 217} :emerald {:s 84 :h 160} :green {:s 71 :h 142} :yellow {:s 96 :h 48} :purple {:s 81 :h 271} :indigo {:s 84 :h 239} :white {:s 0 :h 0} :slate {:s 16 :h 215}})
|
||||
|
||||
(define lerp (fn (a b t) (+ a (* t (- b a)))))
|
||||
|
||||
(define shade-to-lightness
|
||||
(fn (shade)
|
||||
(define
|
||||
shade-to-lightness
|
||||
(fn
|
||||
(shade)
|
||||
(cond
|
||||
(<= shade 50) (lerp 100 97 (/ shade 50))
|
||||
(<= shade 100) (lerp 97 93 (/ (- shade 50) 50))
|
||||
(<= shade 200) (lerp 93 87 (/ (- shade 100) 100))
|
||||
(<= shade 300) (lerp 87 77 (/ (- shade 200) 100))
|
||||
(<= shade 400) (lerp 77 64 (/ (- shade 300) 100))
|
||||
(<= shade 500) (lerp 64 53 (/ (- shade 400) 100))
|
||||
(<= shade 600) (lerp 53 45 (/ (- shade 500) 100))
|
||||
(<= shade 700) (lerp 45 38 (/ (- shade 600) 100))
|
||||
(<= shade 800) (lerp 38 30 (/ (- shade 700) 100))
|
||||
(<= shade 900) (lerp 30 21 (/ (- shade 800) 100))
|
||||
(<= shade 950) (lerp 21 13 (/ (- shade 900) 50))
|
||||
true 13)))
|
||||
(<= shade 50)
|
||||
(lerp 100 97 (/ shade 50))
|
||||
(<= shade 100)
|
||||
(lerp 97 93 (/ (- shade 50) 50))
|
||||
(<= shade 200)
|
||||
(lerp 93 87 (/ (- shade 100) 100))
|
||||
(<= shade 300)
|
||||
(lerp 87 77 (/ (- shade 200) 100))
|
||||
(<= shade 400)
|
||||
(lerp 77 64 (/ (- shade 300) 100))
|
||||
(<= shade 500)
|
||||
(lerp 64 53 (/ (- shade 400) 100))
|
||||
(<= shade 600)
|
||||
(lerp 53 45 (/ (- shade 500) 100))
|
||||
(<= shade 700)
|
||||
(lerp 45 38 (/ (- shade 600) 100))
|
||||
(<= shade 800)
|
||||
(lerp 38 30 (/ (- shade 700) 100))
|
||||
(<= shade 900)
|
||||
(lerp 30 21 (/ (- shade 800) 100))
|
||||
(<= shade 950)
|
||||
(lerp 21 13 (/ (- shade 900) 50))
|
||||
true
|
||||
13)))
|
||||
|
||||
(define colour
|
||||
(fn (name shade)
|
||||
(let ((base (get colour-bases name)))
|
||||
(if (nil? base)
|
||||
(define
|
||||
colour
|
||||
(fn
|
||||
(name shade)
|
||||
(let
|
||||
((base (get colour-bases name)))
|
||||
(if
|
||||
(nil? base)
|
||||
name
|
||||
(let ((h (get base "h"))
|
||||
(s (get base "s"))
|
||||
(l (shade-to-lightness shade)))
|
||||
(let
|
||||
((h (get base "h"))
|
||||
(s (get base "s"))
|
||||
(l (shade-to-lightness shade)))
|
||||
(str "hsl(" h "," s "%," (round l) "%)"))))))
|
||||
|
||||
;; =========================================================================
|
||||
;; Lookup tables — all the value vocabularies
|
||||
;; =========================================================================
|
||||
(define cssx-colour-props {:bg "background-color" :border "border-color" :text "color"})
|
||||
|
||||
;; Colour property shorthands → CSS property name
|
||||
(define cssx-colour-props
|
||||
{"bg" "background-color"
|
||||
"text" "color"
|
||||
"border" "border-color"})
|
||||
(define cssx-spacing-props {:ml "margin-left:{v}" :mr "margin-right:{v}" :mt "margin-top:{v}" :mb "margin-bottom:{v}" :pl "padding-left:{v}" :m "margin:{v}" :my "margin-top:{v};margin-bottom:{v}" :px "padding-left:{v};padding-right:{v}" :pb "padding-bottom:{v}" :pr "padding-right:{v}" :p "padding:{v}" :py "padding-top:{v};padding-bottom:{v}" :pt "padding-top:{v}" :mx "margin-left:{v};margin-right:{v}"})
|
||||
|
||||
;; Spacing property shorthands → CSS declaration template ({v} = computed value)
|
||||
(define cssx-spacing-props
|
||||
{"p" "padding:{v}"
|
||||
"px" "padding-left:{v};padding-right:{v}"
|
||||
"py" "padding-top:{v};padding-bottom:{v}"
|
||||
"pt" "padding-top:{v}"
|
||||
"pb" "padding-bottom:{v}"
|
||||
"pl" "padding-left:{v}"
|
||||
"pr" "padding-right:{v}"
|
||||
"m" "margin:{v}"
|
||||
"mx" "margin-left:{v};margin-right:{v}"
|
||||
"my" "margin-top:{v};margin-bottom:{v}"
|
||||
"mt" "margin-top:{v}"
|
||||
"mb" "margin-bottom:{v}"
|
||||
"ml" "margin-left:{v}"
|
||||
"mr" "margin-right:{v}"})
|
||||
(define cssx-sizes {:xs "font-size:0.75rem;line-height:1rem" :3xl "font-size:1.875rem;line-height:2.25rem" :7xl "font-size:4.5rem;line-height:1" :sm "font-size:0.875rem;line-height:1.25rem" :8xl "font-size:6rem;line-height:1" :xl "font-size:1.25rem;line-height:1.75rem" :6xl "font-size:3.75rem;line-height:1" :9xl "font-size:8rem;line-height:1" :5xl "font-size:3rem;line-height:1" :lg "font-size:1.125rem;line-height:1.75rem" :2xl "font-size:1.5rem;line-height:2rem" :base "font-size:1rem;line-height:1.5rem" :4xl "font-size:2.25rem;line-height:2.5rem"})
|
||||
|
||||
;; Named font sizes (Tailwind v3 scale)
|
||||
(define cssx-sizes
|
||||
{"xs" "font-size:0.75rem;line-height:1rem"
|
||||
"sm" "font-size:0.875rem;line-height:1.25rem"
|
||||
"base" "font-size:1rem;line-height:1.5rem"
|
||||
"lg" "font-size:1.125rem;line-height:1.75rem"
|
||||
"xl" "font-size:1.25rem;line-height:1.75rem"
|
||||
"2xl" "font-size:1.5rem;line-height:2rem"
|
||||
"3xl" "font-size:1.875rem;line-height:2.25rem"
|
||||
"4xl" "font-size:2.25rem;line-height:2.5rem"
|
||||
"5xl" "font-size:3rem;line-height:1"
|
||||
"6xl" "font-size:3.75rem;line-height:1"
|
||||
"7xl" "font-size:4.5rem;line-height:1"
|
||||
"8xl" "font-size:6rem;line-height:1"
|
||||
"9xl" "font-size:8rem;line-height:1"})
|
||||
(define cssx-weights {:light "300" :semibold "600" :bold "700" :extrabold "800" :black "900" :extralight "200" :thin "100" :medium "500" :normal "400"})
|
||||
|
||||
;; Named font weights
|
||||
(define cssx-weights
|
||||
{"thin" "100"
|
||||
"extralight" "200"
|
||||
"light" "300"
|
||||
"normal" "400"
|
||||
"medium" "500"
|
||||
"semibold" "600"
|
||||
"bold" "700"
|
||||
"extrabold" "800"
|
||||
"black" "900"})
|
||||
(define cssx-families {:mono "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace" :sans "ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif" :serif "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif"})
|
||||
|
||||
;; Named font families
|
||||
(define cssx-families
|
||||
{"sans" "ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif"
|
||||
"serif" "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif"
|
||||
"mono" "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace"})
|
||||
(define cssx-alignments {:center true :left true :right true :justify true})
|
||||
|
||||
;; Text alignment keywords
|
||||
(define cssx-alignments
|
||||
{"left" true "center" true "right" true "justify" true})
|
||||
(define cssx-displays {:flex "flex" :grid "grid" :inline-block "inline-block" :inline "inline" :hidden "none" :block "block" :inline-flex "inline-flex"})
|
||||
|
||||
;; Display keywords → CSS value
|
||||
(define cssx-displays
|
||||
{"block" "block"
|
||||
"inline" "inline"
|
||||
"inline-block" "inline-block"
|
||||
"flex" "flex"
|
||||
"inline-flex" "inline-flex"
|
||||
"grid" "grid"
|
||||
"hidden" "none"})
|
||||
(define cssx-max-widths {:xs "20rem" :3xl "48rem" :7xl "80rem" :sm "24rem" :xl "36rem" :full "100%" :md "28rem" :6xl "72rem" :prose "65ch" :5xl "64rem" :lg "32rem" :2xl "42rem" :4xl "56rem" :none "none" :screen "100vw"})
|
||||
|
||||
;; Named max-widths (Tailwind scale)
|
||||
(define cssx-max-widths
|
||||
{"xs" "20rem" "sm" "24rem" "md" "28rem"
|
||||
"lg" "32rem" "xl" "36rem" "2xl" "42rem"
|
||||
"3xl" "48rem" "4xl" "56rem" "5xl" "64rem"
|
||||
"6xl" "72rem" "7xl" "80rem"
|
||||
"full" "100%" "none" "none"
|
||||
"prose" "65ch" "screen" "100vw"})
|
||||
(define cssx-breakpoints {:sm "640px" :xl "1280px" :md "768px" :lg "1024px" :2xl "1536px"})
|
||||
|
||||
;; Responsive breakpoints (mobile-first min-width)
|
||||
(define cssx-breakpoints
|
||||
{"sm" "640px"
|
||||
"md" "768px"
|
||||
"lg" "1024px"
|
||||
"xl" "1280px"
|
||||
"2xl" "1536px"})
|
||||
(define cssx-states {:focus ":focus" :first ":first-child" :hover ":hover" :focus-visible ":focus-visible" :last ":last-child" :active ":active" :focus-within ":focus-within"})
|
||||
|
||||
;; Pseudo-class states
|
||||
(define cssx-states
|
||||
{"hover" ":hover"
|
||||
"focus" ":focus"
|
||||
"active" ":active"
|
||||
"focus-within" ":focus-within"
|
||||
"focus-visible" ":focus-visible"
|
||||
"first" ":first-child"
|
||||
"last" ":last-child"})
|
||||
|
||||
;; =========================================================================
|
||||
;; Utility resolver — token string → CSS declarations
|
||||
;; =========================================================================
|
||||
|
||||
;; Spacing value: number → rem, "auto" → "auto", "px" → "1px"
|
||||
(define cssx-spacing-value
|
||||
(fn (v)
|
||||
(define
|
||||
cssx-spacing-value
|
||||
(fn
|
||||
(v)
|
||||
(cond
|
||||
(= v "auto") "auto"
|
||||
(= v "px") "1px"
|
||||
(= v "0") "0px"
|
||||
true (let ((n (parse-int v nil)))
|
||||
(if (nil? n) nil
|
||||
(str (* n 0.25) "rem"))))))
|
||||
(= v "auto")
|
||||
"auto"
|
||||
(= v "px")
|
||||
"1px"
|
||||
(= v "0")
|
||||
"0px"
|
||||
true
|
||||
(let
|
||||
((n (parse-int v nil)))
|
||||
(if (nil? n) nil (str (* n 0.25) "rem"))))))
|
||||
|
||||
;; Replace {v} in a template string with a value
|
||||
(define cssx-template
|
||||
(fn (tmpl v)
|
||||
(let ((i (index-of tmpl "{v}")))
|
||||
(if (< i 0) tmpl
|
||||
(let ((result (str (substring tmpl 0 i) v (substring tmpl (+ i 3) (len tmpl)))))
|
||||
;; Handle templates with multiple {v} (e.g. padding-left:{v};padding-right:{v})
|
||||
(let ((j (index-of result "{v}")))
|
||||
(if (< j 0) result
|
||||
(str (substring result 0 j) v (substring result (+ j 3) (len result))))))))))
|
||||
(define
|
||||
cssx-template
|
||||
(fn
|
||||
(tmpl v)
|
||||
(let
|
||||
((i (index-of tmpl "{v}")))
|
||||
(if
|
||||
(< i 0)
|
||||
tmpl
|
||||
(let
|
||||
((result (str (substring tmpl 0 i) v (substring tmpl (+ i 3) (len tmpl)))))
|
||||
(let
|
||||
((j (index-of result "{v}")))
|
||||
(if
|
||||
(< j 0)
|
||||
result
|
||||
(str
|
||||
(substring result 0 j)
|
||||
v
|
||||
(substring result (+ j 3) (len result))))))))))
|
||||
|
||||
;; Resolve a base utility token (no state/bp prefix) → CSS declaration string or nil.
|
||||
;; Tries matchers in order: colour, text-size, text-align, font, spacing, display, max-w, rounded, opacity.
|
||||
(define cssx-resolve
|
||||
(fn (token)
|
||||
(let ((parts (split token "-")))
|
||||
(if (empty? parts) nil
|
||||
(let ((head (first parts))
|
||||
(rest (slice parts 1)))
|
||||
(define
|
||||
cssx-resolve
|
||||
(fn
|
||||
(token)
|
||||
(let
|
||||
((parts (split token "-")))
|
||||
(if
|
||||
(empty? parts)
|
||||
nil
|
||||
(let
|
||||
((head (first parts)) (rest (slice parts 1)))
|
||||
(cond
|
||||
;; ---------------------------------------------------------
|
||||
;; Colour utilities: bg-{colour}-{shade}, text-{colour}-{shade}, border-{colour}-{shade}
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-colour-props head)
|
||||
(>= (len rest) 2)
|
||||
(not (nil? (parse-int (last rest) nil)))
|
||||
(not (nil? (get colour-bases (join "-" (slice rest 0 (- (len rest) 1)))))))
|
||||
(let ((css-prop (get cssx-colour-props head))
|
||||
(cname (join "-" (slice rest 0 (- (len rest) 1))))
|
||||
(shade (parse-int (last rest) 0)))
|
||||
(and
|
||||
(get cssx-colour-props head)
|
||||
(>= (len rest) 2)
|
||||
(not (nil? (parse-int (last rest) nil)))
|
||||
(not
|
||||
(nil?
|
||||
(get
|
||||
colour-bases
|
||||
(join "-" (slice rest 0 (- (len rest) 1)))))))
|
||||
(let
|
||||
((css-prop (get cssx-colour-props head))
|
||||
(cname (join "-" (slice rest 0 (- (len rest) 1))))
|
||||
(shade (parse-int (last rest) 0)))
|
||||
(str css-prop ":" (colour cname shade)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text size: text-{size-name} (e.g. text-xl, text-2xl)
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "text")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-sizes (first rest)))))
|
||||
(and
|
||||
(= head "text")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-sizes (first rest)))))
|
||||
(get cssx-sizes (first rest))
|
||||
|
||||
;; Also handle text-2xl etc where rest might be ("2xl") — covered above
|
||||
;; But also "text-sm" etc — covered above since rest is ("sm")
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text alignment: text-left, text-center, text-right, text-justify
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "text")
|
||||
(= (len rest) 1)
|
||||
(get cssx-alignments (first rest)))
|
||||
(and
|
||||
(= head "text")
|
||||
(= (len rest) 1)
|
||||
(get cssx-alignments (first rest)))
|
||||
(str "text-align:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Font weight: font-bold, font-semibold, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-weights (first rest)))))
|
||||
(and
|
||||
(= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-weights (first rest)))))
|
||||
(str "font-weight:" (get cssx-weights (first rest)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Font family: font-sans, font-serif, font-mono
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-families (first rest)))))
|
||||
(and
|
||||
(= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-families (first rest)))))
|
||||
(str "font-family:" (get cssx-families (first rest)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Spacing: p-4, px-2, mt-8, mx-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-spacing-props head)
|
||||
(= (len rest) 1))
|
||||
(let ((tmpl (get cssx-spacing-props head))
|
||||
(v (cssx-spacing-value (first rest))))
|
||||
(and (get cssx-spacing-props head) (= (len rest) 1))
|
||||
(let
|
||||
((tmpl (get cssx-spacing-props head))
|
||||
(v (cssx-spacing-value (first rest))))
|
||||
(if (nil? v) nil (cssx-template tmpl v)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Display: block, flex, grid, hidden, inline, inline-block
|
||||
;; ---------------------------------------------------------
|
||||
(and (= (len parts) 1)
|
||||
(not (nil? (get cssx-displays head))))
|
||||
(and (= (len parts) 1) (not (nil? (get cssx-displays head))))
|
||||
(str "display:" (get cssx-displays head))
|
||||
|
||||
;; Inline-block, inline-flex (multi-word)
|
||||
(and (= (len parts) 2)
|
||||
(not (nil? (get cssx-displays token))))
|
||||
(and (= (len parts) 2) (not (nil? (get cssx-displays token))))
|
||||
(str "display:" (get cssx-displays token))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Max-width: max-w-xl, max-w-3xl, max-w-prose
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "max")
|
||||
(>= (len rest) 2)
|
||||
(= (first rest) "w"))
|
||||
(let ((val-name (join "-" (slice rest 1)))
|
||||
(val (get cssx-max-widths val-name)))
|
||||
(and (= head "max") (>= (len rest) 2) (= (first rest) "w"))
|
||||
(let
|
||||
((val-name (join "-" (slice rest 1)))
|
||||
(val (get cssx-max-widths val-name)))
|
||||
(if (nil? val) nil (str "max-width:" val)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Rounded: rounded, rounded-lg, rounded-full, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(= head "rounded")
|
||||
(cond
|
||||
(empty? rest) "border-radius:0.25rem"
|
||||
(= (first rest) "none") "border-radius:0"
|
||||
(= (first rest) "sm") "border-radius:0.125rem"
|
||||
(= (first rest) "md") "border-radius:0.375rem"
|
||||
(= (first rest) "lg") "border-radius:0.5rem"
|
||||
(= (first rest) "xl") "border-radius:0.75rem"
|
||||
(= (first rest) "2xl") "border-radius:1rem"
|
||||
(= (first rest) "3xl") "border-radius:1.5rem"
|
||||
(= (first rest) "full") "border-radius:9999px"
|
||||
true nil)
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Opacity: opacity-{n} (0-100)
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "opacity")
|
||||
(= (len rest) 1))
|
||||
(let ((n (parse-int (first rest) nil)))
|
||||
(empty? rest)
|
||||
"border-radius:0.25rem"
|
||||
(= (first rest) "none")
|
||||
"border-radius:0"
|
||||
(= (first rest) "sm")
|
||||
"border-radius:0.125rem"
|
||||
(= (first rest) "md")
|
||||
"border-radius:0.375rem"
|
||||
(= (first rest) "lg")
|
||||
"border-radius:0.5rem"
|
||||
(= (first rest) "xl")
|
||||
"border-radius:0.75rem"
|
||||
(= (first rest) "2xl")
|
||||
"border-radius:1rem"
|
||||
(= (first rest) "3xl")
|
||||
"border-radius:1.5rem"
|
||||
(= (first rest) "full")
|
||||
"border-radius:9999px"
|
||||
true
|
||||
nil)
|
||||
(and (= head "opacity") (= (len rest) 1))
|
||||
(let
|
||||
((n (parse-int (first rest) nil)))
|
||||
(if (nil? n) nil (str "opacity:" (/ n 100))))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Width/height: w-{n}, h-{n}, w-full, h-full, h-screen
|
||||
;; ---------------------------------------------------------
|
||||
(and (or (= head "w") (= head "h"))
|
||||
(= (len rest) 1))
|
||||
(let ((prop (if (= head "w") "width" "height"))
|
||||
(val (first rest)))
|
||||
(and (or (= head "w") (= head "h")) (= (len rest) 1))
|
||||
(let
|
||||
((prop (if (= head "w") "width" "height"))
|
||||
(val (first rest)))
|
||||
(cond
|
||||
(= val "full") (str prop ":100%")
|
||||
(= val "screen") (str prop (if (= head "w") ":100vw" ":100vh"))
|
||||
(= val "auto") (str prop ":auto")
|
||||
(= val "min") (str prop ":min-content")
|
||||
(= val "max") (str prop ":max-content")
|
||||
(= val "fit") (str prop ":fit-content")
|
||||
true (let ((n (parse-int val nil)))
|
||||
(if (nil? n) nil
|
||||
(str prop ":" (* n 0.25) "rem")))))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Gap: gap-{n}
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "gap")
|
||||
(= (len rest) 1))
|
||||
(let ((v (cssx-spacing-value (first rest))))
|
||||
(= val "full")
|
||||
(str prop ":100%")
|
||||
(= val "screen")
|
||||
(str prop (if (= head "w") ":100vw" ":100vh"))
|
||||
(= val "auto")
|
||||
(str prop ":auto")
|
||||
(= val "min")
|
||||
(str prop ":min-content")
|
||||
(= val "max")
|
||||
(str prop ":max-content")
|
||||
(= val "fit")
|
||||
(str prop ":fit-content")
|
||||
true
|
||||
(let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str prop ":" (* n 0.25) "rem")))))
|
||||
(and (= head "gap") (= (len rest) 1))
|
||||
(let
|
||||
((v (cssx-spacing-value (first rest))))
|
||||
(if (nil? v) nil (str "gap:" v)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text decoration: underline, no-underline, line-through
|
||||
;; ---------------------------------------------------------
|
||||
(and (= (len parts) 1)
|
||||
(or (= head "underline") (= head "overline") (= head "line-through")))
|
||||
(and
|
||||
(= (len parts) 1)
|
||||
(or
|
||||
(= head "underline")
|
||||
(= head "overline")
|
||||
(= head "line-through")))
|
||||
(str "text-decoration-line:" head)
|
||||
|
||||
(and (= (len parts) 2) (= head "no") (= (first rest) "underline"))
|
||||
(and
|
||||
(= (len parts) 2)
|
||||
(= head "no")
|
||||
(= (first rest) "underline"))
|
||||
"text-decoration-line:none"
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Cursor: cursor-pointer, cursor-default, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "cursor") (= (len rest) 1))
|
||||
(str "cursor:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Overflow: overflow-hidden, overflow-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "overflow") (= (len rest) 1))
|
||||
(str "overflow:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Transition: transition, transition-colors, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "transition") (empty? rest))
|
||||
"transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
|
||||
|
||||
(and (= head "transition") (= (first rest) "colors"))
|
||||
"transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
|
||||
true
|
||||
nil))))))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Fallback: unrecognised → nil (skip)
|
||||
;; ---------------------------------------------------------
|
||||
true nil))))))
|
||||
|
||||
;; =========================================================================
|
||||
;; Token processor — full token with optional state/bp prefixes
|
||||
;; =========================================================================
|
||||
|
||||
;; Process one token string → {:cls "sx-..." :rule ".sx-...{...}"} or nil
|
||||
;; Token format: [bp:]?[state:]?utility
|
||||
;; Examples: "bg-yellow-199", "hover:bg-rose-500", "md:hover:text-xl"
|
||||
(define cssx-process-token
|
||||
(fn (token)
|
||||
(let ((colon-parts (split token ":"))
|
||||
(n (len colon-parts)))
|
||||
;; Extract state, bp, and base utility from colon-separated parts
|
||||
(let ((bp nil) (state nil) (base nil))
|
||||
;; 1 part: just utility
|
||||
;; 2 parts: modifier:utility (could be bp or state)
|
||||
;; 3 parts: bp:state:utility
|
||||
(define
|
||||
cssx-process-token
|
||||
(fn
|
||||
(token)
|
||||
(let
|
||||
((colon-parts (split token ":")) (n (len colon-parts)))
|
||||
(let
|
||||
((bp nil) (state nil) (base nil))
|
||||
(cond
|
||||
(= n 1)
|
||||
(do (set! base (first colon-parts)))
|
||||
(= n 2)
|
||||
(let ((prefix (first colon-parts)))
|
||||
(let
|
||||
((prefix (first colon-parts)))
|
||||
(set! base (last colon-parts))
|
||||
(if (not (nil? (get cssx-breakpoints prefix)))
|
||||
(if
|
||||
(not (nil? (get cssx-breakpoints prefix)))
|
||||
(set! bp prefix)
|
||||
(set! state prefix)))
|
||||
(>= n 3)
|
||||
@@ -409,98 +262,39 @@
|
||||
(set! bp (first colon-parts))
|
||||
(set! state (nth colon-parts 1))
|
||||
(set! base (last colon-parts))))
|
||||
(let
|
||||
((css (cssx-resolve base)))
|
||||
(if
|
||||
(nil? css)
|
||||
nil
|
||||
(let
|
||||
((cls (str "sx-" (join "-" (split token ":"))))
|
||||
(pseudo
|
||||
(if
|
||||
(nil? state)
|
||||
""
|
||||
(or (get cssx-states state) (str ":" state))))
|
||||
(decl (str "." cls pseudo "{" css "}")))
|
||||
(if
|
||||
(nil? bp)
|
||||
{:rule decl :cls cls}
|
||||
(let
|
||||
((min-w (or (get cssx-breakpoints bp) bp)))
|
||||
{:rule (str "@media(min-width:" min-w "){" decl "}") :cls cls})))))))))
|
||||
|
||||
;; Resolve the base utility to CSS declarations
|
||||
(let ((css (cssx-resolve base)))
|
||||
(if (nil? css) nil
|
||||
(let ((cls (str "sx-" (join "-" (split token ":"))))
|
||||
(pseudo (if (nil? state) ""
|
||||
(or (get cssx-states state) (str ":" state))))
|
||||
(decl (str "." cls pseudo "{" css "}")))
|
||||
(if (nil? bp)
|
||||
{"cls" cls "rule" decl}
|
||||
(let ((min-w (or (get cssx-breakpoints bp) bp)))
|
||||
{"cls" cls
|
||||
"rule" (str "@media(min-width:" min-w "){" decl "}")})))))))))
|
||||
|
||||
;; =========================================================================
|
||||
;; tw — Tailwind-style inline style string from utility tokens
|
||||
;;
|
||||
;; Same token format as ~cssx/tw, returns a CSS declaration string for :style.
|
||||
;; Cannot do hover/responsive (use ~cssx/tw for those).
|
||||
;;
|
||||
;; Usage:
|
||||
;; (div :style (tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; "content")
|
||||
;; =========================================================================
|
||||
|
||||
(define tw
|
||||
(fn (tokens-str)
|
||||
(let ((tokens (split (or tokens-str "") " "))
|
||||
(parts (list)))
|
||||
(for-each (fn (tok)
|
||||
(when (not (= tok ""))
|
||||
(let ((css (cssx-resolve tok)))
|
||||
(when (not (nil? css))
|
||||
(append! parts (str css ";"))))))
|
||||
(define
|
||||
tw
|
||||
(fn
|
||||
(tokens-str)
|
||||
(let
|
||||
((tokens (split (or tokens-str "") " ")) (parts (list)))
|
||||
(for-each
|
||||
(fn
|
||||
(tok)
|
||||
(when
|
||||
(not (= tok ""))
|
||||
(let
|
||||
((css (cssx-resolve tok)))
|
||||
(when (not (nil? css)) (append! parts (str css ";"))))))
|
||||
tokens)
|
||||
(join "" parts))))
|
||||
|
||||
;; =========================================================================
|
||||
;; ~cssx/tw — spread component that injects JIT classes onto parent element
|
||||
;;
|
||||
;; Usage — as a child of any element:
|
||||
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; (h1 "styled content"))
|
||||
;;
|
||||
;; (button (~cssx/tw "hover:bg-rose-500 focus:border-blue-400")
|
||||
;; "interactive")
|
||||
;;
|
||||
;; Returns a spread value that merges :class and :data-tw onto the parent
|
||||
;; element. Collects CSS rules into the "cssx" bucket for a single global
|
||||
;; <style> flush. No wrapper element, no per-element <style> tags.
|
||||
;;
|
||||
;; Reusable as variables:
|
||||
;; (define important (~cssx/tw "font-bold text-4xl"))
|
||||
;; (div important "the queen is dead")
|
||||
;;
|
||||
;; Multiple spreads merge naturally:
|
||||
;; (div (~cssx/tw "bg-red-500") (~cssx/tw "p-4") "content")
|
||||
;; =========================================================================
|
||||
|
||||
(defcomp ~cssx/tw (&key tokens)
|
||||
(let ((token-list (filter (fn (t) (not (= t "")))
|
||||
(split (or tokens "") " ")))
|
||||
(results (map cssx-process-token token-list))
|
||||
(valid (filter (fn (r) (not (nil? r))) results))
|
||||
(classes (map (fn (r) (get r "cls")) valid))
|
||||
(rules (map (fn (r) (get r "rule")) valid))
|
||||
(_ (for-each (fn (rule) (collect! "cssx" rule)) rules)))
|
||||
;; Return spread: injects class + data-tw onto parent element
|
||||
(if (empty? classes)
|
||||
nil
|
||||
(make-spread {"class" (join " " classes)
|
||||
"data-tw" (or tokens "")}))))
|
||||
|
||||
|
||||
;; =========================================================================
|
||||
;; ~cssx/flush — emit collected CSS rules as a single <style> tag
|
||||
;;
|
||||
;; Place once in the page (typically in the layout, before </body>).
|
||||
;; Emits all accumulated CSSX rules and clears the bucket.
|
||||
;;
|
||||
;; Usage:
|
||||
;; (~cssx/flush)
|
||||
;; =========================================================================
|
||||
|
||||
(defcomp ~cssx/flush () :affinity :client
|
||||
(let ((rules (collected "cssx"))
|
||||
(head-style (dom-query "#sx-css")))
|
||||
;; On client: append rules to <style id="sx-css"> in <head>.
|
||||
;; On server: head-style is nil (no DOM). Don't clear the bucket —
|
||||
;; the shell's <head> template reads collected("cssx") and emits them.
|
||||
(when head-style
|
||||
(clear-collected! "cssx")
|
||||
(when (not (empty? rules))
|
||||
(dom-set-prop head-style "textContent"
|
||||
(str (dom-get-prop head-style "textContent") (join "" rules)))))))
|
||||
|
||||
Reference in New Issue
Block a user