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:
@@ -437,17 +437,9 @@
|
||||
(let
|
||||
((f (trampoline (eval-expr (first args) env)))
|
||||
(coll (trampoline (eval-expr (nth args 1) env))))
|
||||
(map
|
||||
(fn
|
||||
(item)
|
||||
(if
|
||||
(lambda? f)
|
||||
(let
|
||||
((local (env-merge (lambda-closure f) env)))
|
||||
(env-bind! local (first (lambda-params f)) item)
|
||||
(aser (lambda-body f) local))
|
||||
(cek-call f (list item))))
|
||||
coll))
|
||||
(let
|
||||
((results (map (fn (item) (if (lambda? f) (let ((local (env-extend (lambda-closure f)))) (env-bind! local (first (lambda-params f)) item) (aser (lambda-body f) local)) (cek-call f (list item)))) coll)))
|
||||
(aser-fragment results env)))
|
||||
(= name "map-indexed")
|
||||
(let
|
||||
((f (trampoline (eval-expr (first args) env)))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -430,17 +430,6 @@
|
||||
(host-callback thunk))
|
||||
(thunk))))
|
||||
|
||||
(define
|
||||
observe-intersection
|
||||
(fn
|
||||
(el callback once? delay)
|
||||
(let
|
||||
((cb (host-callback (fn (entries) (for-each (fn (entry) (when (host-get entry "isIntersecting") (if delay (set-timeout (fn () (callback entry)) delay) (callback entry)) (when once? (host-call observer "unobserve" el)))) (host-call entries "forEach" (host-callback (fn (e) e))))))))
|
||||
(let
|
||||
((observer (host-new "IntersectionObserver" (host-callback (fn (entries) (let ((arr-len (host-get entries "length"))) (let loop ((i 0)) (when (< i arr-len) (let ((entry (host-call entries "item" i))) (when (and entry (host-get entry "isIntersecting")) (if delay (set-timeout (fn () (callback entry)) delay) (callback entry)) (when once? (host-call observer "unobserve" el)))) (loop (+ i 1))))))))))
|
||||
(host-call observer "observe" el)
|
||||
observer))))
|
||||
|
||||
(define
|
||||
event-source-connect
|
||||
(fn
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,17 +1,23 @@
|
||||
;; Create raw signal dict with value, subs, deps fields
|
||||
(define
|
||||
make-signal
|
||||
(fn
|
||||
(value)
|
||||
(dict "__signal" true "value" value "subscribers" (list) "deps" (list))))
|
||||
|
||||
;; Type predicate for signals
|
||||
(define signal? (fn (x) (and (dict? x) (has-key? x "__signal"))))
|
||||
|
||||
;; Read current value from signal
|
||||
(define signal-value (fn (s) (get s "value")))
|
||||
|
||||
;; Write value to signal (no notification)
|
||||
(define signal-set-value! (fn (s v) (dict-set! s "value" v)))
|
||||
|
||||
;; List of subscriber functions
|
||||
(define signal-subscribers (fn (s) (get s "subscribers")))
|
||||
|
||||
;; Add a subscriber function
|
||||
(define
|
||||
signal-add-sub!
|
||||
(fn
|
||||
@@ -20,6 +26,7 @@
|
||||
(not (contains? (get s "subscribers") f))
|
||||
(dict-set! s "subscribers" (append (get s "subscribers") (list f))))))
|
||||
|
||||
;; Remove a subscriber function
|
||||
(define
|
||||
signal-remove-sub!
|
||||
(fn
|
||||
@@ -29,15 +36,19 @@
|
||||
"subscribers"
|
||||
(filter (fn (sub) (not (identical? sub f))) (get s "subscribers")))))
|
||||
|
||||
;; List of upstream signal dependencies
|
||||
(define signal-deps (fn (s) (get s "deps")))
|
||||
|
||||
;; Set upstream dependencies
|
||||
(define signal-set-deps! (fn (s deps) (dict-set! s "deps" deps)))
|
||||
|
||||
;; Create a reactive signal (user-facing constructor)
|
||||
(define
|
||||
signal
|
||||
:effects ()
|
||||
(fn ((initial-value :as any)) (make-signal initial-value)))
|
||||
|
||||
;; Dereference a signal, returning its current value
|
||||
(define
|
||||
deref
|
||||
:effects ()
|
||||
@@ -58,6 +69,7 @@
|
||||
(signal-add-sub! s notify-fn))))
|
||||
(signal-value s)))))
|
||||
|
||||
;; Set signal to new value and notify subscribers
|
||||
(define
|
||||
reset!
|
||||
:effects (mutation)
|
||||
@@ -72,6 +84,7 @@
|
||||
(signal-set-value! s value)
|
||||
(notify-subscribers s))))))
|
||||
|
||||
;; Apply function to current value and reset
|
||||
(define
|
||||
swap!
|
||||
:effects (mutation)
|
||||
@@ -87,6 +100,7 @@
|
||||
(signal-set-value! s new-val)
|
||||
(notify-subscribers s))))))
|
||||
|
||||
;; Create a derived signal that auto-updates from dependencies
|
||||
(define
|
||||
computed
|
||||
:effects (mutation)
|
||||
@@ -100,6 +114,7 @@
|
||||
(register-in-scope (fn () (dispose-computed s)))
|
||||
s))))
|
||||
|
||||
;; Create a side-effect that runs when dependencies change
|
||||
(define
|
||||
effect
|
||||
:effects (mutation)
|
||||
@@ -115,10 +130,13 @@
|
||||
(register-in-scope dispose-fn)
|
||||
dispose-fn)))))
|
||||
|
||||
;; Nesting counter for batched updates
|
||||
(define *batch-depth* 0)
|
||||
|
||||
;; Queued notifications during batch
|
||||
(define *batch-queue* (list))
|
||||
|
||||
;; Batch multiple signal updates, notify once at end
|
||||
(define
|
||||
batch
|
||||
:effects (mutation)
|
||||
@@ -148,6 +166,7 @@
|
||||
queue)
|
||||
(for-each (fn ((sub :as lambda)) (sub)) pending))))))
|
||||
|
||||
;; Notify all subscribers of a signal change
|
||||
(define
|
||||
notify-subscribers
|
||||
:effects (mutation)
|
||||
@@ -158,6 +177,7 @@
|
||||
(when (not (contains? *batch-queue* s)) (append! *batch-queue* s))
|
||||
(flush-subscribers s))))
|
||||
|
||||
;; Process queued subscriber notifications
|
||||
(define
|
||||
flush-subscribers
|
||||
:effects (mutation)
|
||||
@@ -165,6 +185,7 @@
|
||||
((s :as dict))
|
||||
(for-each (fn (sub) (cek-call sub nil)) (signal-subscribers s))))
|
||||
|
||||
;; Tear down a computed signal, remove from deps
|
||||
(define
|
||||
dispose-computed
|
||||
:effects (mutation)
|
||||
@@ -177,6 +198,7 @@
|
||||
(signal-deps s))
|
||||
(signal-set-deps! s (list)))))
|
||||
|
||||
;; Evaluate body in an island disposal scope
|
||||
(define
|
||||
with-island-scope
|
||||
:effects (mutation)
|
||||
@@ -185,6 +207,7 @@
|
||||
(scope-push! "sx-island-scope" scope-fn)
|
||||
(let ((result (body-fn))) (scope-pop! "sx-island-scope") result)))
|
||||
|
||||
;; Register a disposable in the current island scope
|
||||
(define
|
||||
register-in-scope
|
||||
:effects (mutation)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,41 +1,62 @@
|
||||
;; Assert condition is truthy, error with message
|
||||
(define assert (fn (condition msg) (when (not condition) (error (or msg "Assertion failed")))))
|
||||
|
||||
;; Assert two values are equal
|
||||
(define assert= (fn (actual expected msg) (when (not (= actual expected)) (error (or msg (str "Expected " expected ", got " actual))))))
|
||||
|
||||
;; Dict of mock IO operations for testing
|
||||
(define default-platform {:current-user (fn () nil) :csrf-token (fn () "test-csrf-token") :app-url (fn (service &rest path) "/mock-app-url") :frag (fn (service comp &rest args) "") :sleep (fn (ms) nil) :local-storage-set (fn (key val) nil) :set-cookie (fn (name val &rest opts) nil) :url-for (fn (endpoint &rest args) "/mock-url") :create-element (fn (tag) nil) :request-path (fn () "/") :config (fn (key) nil) :set-attr (fn (el name val) nil) :set-text (fn (el text) nil) :remove-child (fn (parent child) nil) :fetch (fn (url &rest opts) {:status 200 :body "" :ok true}) :query (fn (service name &rest args) (list)) :add-class (fn (el cls) nil) :get-element (fn (id) nil) :now (fn () 0) :abort (fn (code) nil) :action (fn (service name &rest args) {:ok true}) :remove-class (fn (el cls) nil) :append-child (fn (parent child) nil) :request-arg (fn (name) nil) :emit-dom (fn (op &rest args) nil) :local-storage-get (fn (key) nil) :get-cookie (fn (name) nil)})
|
||||
|
||||
;; Create a test session with mock IO platform
|
||||
(define make-harness :effects () (fn (&key platform) (let ((merged (if (nil? platform) default-platform (merge default-platform platform)))) {:log (list) :platform merged :state {:cookies {} :storage {} :dom nil}})))
|
||||
|
||||
;; Clear IO log and state for a new test
|
||||
(define harness-reset! :effects () (fn (session) (dict-set! session "log" (list)) (dict-set! session "state" {:cookies {} :storage {} :dom nil}) session))
|
||||
|
||||
;; Append an IO call record to session log
|
||||
(define harness-log :effects () (fn (session &key op) (let ((log (get session "log"))) (if (nil? op) log (filter (fn (entry) (= (get entry "op") op)) log)))))
|
||||
|
||||
;; Read state value from session store
|
||||
(define harness-get :effects () (fn (session key) (get (get session "state") key)))
|
||||
|
||||
;; Write state value to session store
|
||||
(define harness-set! :effects () (fn (session key value) (dict-set! (get session "state") key value) nil))
|
||||
|
||||
;; Wrap a mock fn to record calls in the IO log
|
||||
(define make-interceptor :effects () (fn (session op-name mock-fn) (fn (&rest args) (let ((result (if (empty? args) (mock-fn) (if (= 1 (len args)) (mock-fn (first args)) (if (= 2 (len args)) (mock-fn (first args) (nth args 1)) (if (= 3 (len args)) (mock-fn (first args) (nth args 1) (nth args 2)) (apply mock-fn args)))))) (log (get session "log"))) (append! log {:args args :result result :op op-name}) result))))
|
||||
|
||||
;; Bind all interceptors into the eval environment
|
||||
(define install-interceptors :effects () (fn (session env) (for-each (fn (key) (let ((mock-fn (get (get session "platform") key)) (interceptor (make-interceptor session key mock-fn))) (env-bind! env key interceptor))) (keys (get session "platform"))) env))
|
||||
|
||||
;; Query IO log: all calls, or filtered by op name
|
||||
(define io-calls :effects () (fn (session op-name) (filter (fn (entry) (= (get entry "op") op-name)) (get session "log"))))
|
||||
|
||||
;; Count IO calls, optionally filtered by op name
|
||||
(define io-call-count :effects () (fn (session op-name) (len (io-calls session op-name))))
|
||||
|
||||
;; Get the nth IO call record
|
||||
(define io-call-nth :effects () (fn (session op-name n) (let ((calls (io-calls session op-name))) (if (< n (len calls)) (nth calls n) nil))))
|
||||
|
||||
;; Get args from the nth call to an operation
|
||||
(define io-call-args :effects () (fn (session op-name n) (let ((call (io-call-nth session op-name n))) (if (nil? call) nil (get call "args")))))
|
||||
|
||||
;; Get return value from the nth call to an operation
|
||||
(define io-call-result :effects () (fn (session op-name n) (let ((call (io-call-nth session op-name n))) (if (nil? call) nil (get call "result")))))
|
||||
|
||||
;; Assert an IO operation was called at least once
|
||||
(define assert-io-called :effects () (fn (session op-name) (assert (> (io-call-count session op-name) 0) (str "Expected IO operation " op-name " to be called but it was not"))))
|
||||
|
||||
;; Assert an IO operation was never called
|
||||
(define assert-no-io :effects () (fn (session op-name) (assert (= (io-call-count session op-name) 0) (str "Expected IO operation " op-name " not to be called but it was called " (io-call-count session op-name) " time(s)"))))
|
||||
|
||||
;; Assert exact call count for an operation
|
||||
(define assert-io-count :effects () (fn (session op-name expected) (let ((actual (io-call-count session op-name))) (assert (= actual expected) (str "Expected " op-name " to be called " expected " time(s) but was called " actual " time(s)")))))
|
||||
|
||||
;; Assert args of the nth call match expected
|
||||
(define assert-io-args :effects () (fn (session op-name n expected-args) (let ((actual (io-call-args session op-name n))) (assert (equal? actual expected-args) (str "Expected call " n " to " op-name " with args " (str expected-args) " but got " (str actual))))))
|
||||
|
||||
;; Assert result of the nth call matches expected
|
||||
(define assert-io-result :effects () (fn (session op-name n expected) (let ((actual (io-call-result session op-name n))) (assert (equal? actual expected) (str "Expected call " n " to " op-name " to return " (str expected) " but got " (str actual))))))
|
||||
|
||||
;; Assert a state key has the expected value
|
||||
(define assert-state :effects () (fn (session key expected) (let ((actual (harness-get session key))) (assert (equal? actual expected) (str "Expected state " key " to be " (str expected) " but got " (str actual))))))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
;; Registry of all valid HTML tag names
|
||||
(define
|
||||
HTML_TAGS
|
||||
(list
|
||||
@@ -141,6 +142,7 @@
|
||||
"dialog"
|
||||
"menu"))
|
||||
|
||||
;; Self-closing tags (br, img, hr, etc.)
|
||||
(define
|
||||
VOID_ELEMENTS
|
||||
(list
|
||||
@@ -159,6 +161,7 @@
|
||||
"track"
|
||||
"wbr"))
|
||||
|
||||
;; Attrs that are true/false (checked, disabled, etc.)
|
||||
(define
|
||||
BOOLEAN_ATTRS
|
||||
(list
|
||||
@@ -186,8 +189,10 @@
|
||||
"reversed"
|
||||
"selected"))
|
||||
|
||||
;; Extensible list of forms treated as definitions
|
||||
(define *definition-form-extensions* (list))
|
||||
|
||||
;; Check if a symbol names a definition form
|
||||
(define
|
||||
definition-form?
|
||||
:effects ()
|
||||
@@ -203,6 +208,7 @@
|
||||
(= name "defeffect")
|
||||
(contains? *definition-form-extensions* name))))
|
||||
|
||||
;; Parse keyword attrs and children from element arg list
|
||||
(define
|
||||
parse-element-args
|
||||
:effects (render)
|
||||
@@ -233,6 +239,7 @@
|
||||
args)
|
||||
(list attrs children))))
|
||||
|
||||
;; Render attr dict to HTML attribute string
|
||||
(define
|
||||
render-attrs
|
||||
:effects ()
|
||||
@@ -255,6 +262,7 @@
|
||||
:else (str " " key "=\"" (escape-attr (str val)) "\""))))
|
||||
(keys attrs)))))
|
||||
|
||||
;; Evaluate cond expression (dispatches to scheme/clojure style)
|
||||
(define
|
||||
eval-cond
|
||||
:effects ()
|
||||
@@ -265,6 +273,7 @@
|
||||
(eval-cond-scheme clauses env)
|
||||
(eval-cond-clojure clauses env))))
|
||||
|
||||
;; Scheme-style cond: ((test body) ...)
|
||||
(define
|
||||
eval-cond-scheme
|
||||
:effects ()
|
||||
@@ -285,6 +294,7 @@
|
||||
body
|
||||
(eval-cond-scheme (rest clauses) env)))))))
|
||||
|
||||
;; Clojure-style cond: (test body test body ...)
|
||||
(define
|
||||
eval-cond-clojure
|
||||
:effects ()
|
||||
@@ -303,6 +313,7 @@
|
||||
body
|
||||
(eval-cond-clojure (slice clauses 2) env)))))))
|
||||
|
||||
;; Evaluate let binding pairs, extend env
|
||||
(define
|
||||
process-bindings
|
||||
:effects (mutation)
|
||||
@@ -324,6 +335,7 @@
|
||||
bindings)
|
||||
local)))
|
||||
|
||||
;; Check if an expression should be rendered vs evaluated
|
||||
(define
|
||||
is-render-expr?
|
||||
:effects ()
|
||||
@@ -350,6 +362,7 @@
|
||||
(> (len expr) 1)
|
||||
(= (type-of (nth expr 1)) "keyword")))))))))
|
||||
|
||||
;; Merge spread child attrs into parent element attrs
|
||||
(define
|
||||
merge-spread-attrs
|
||||
:effects (mutation)
|
||||
@@ -385,6 +398,7 @@
|
||||
(dict-set! target key val)))))
|
||||
(keys spread-dict))))
|
||||
|
||||
;; Escape special chars for HTML text content
|
||||
(define
|
||||
escape-html
|
||||
(fn
|
||||
@@ -397,4 +411,5 @@
|
||||
(set! r (replace r "\"" """))
|
||||
r)))
|
||||
|
||||
;; Escape special chars for HTML attribute values
|
||||
(define escape-attr (fn (s) (escape-html s)))
|
||||
|
||||
File diff suppressed because one or more lines are too long
414
shared/static/wasm/sx/tw-layout.sx
Normal file
414
shared/static/wasm/sx/tw-layout.sx
Normal file
@@ -0,0 +1,414 @@
|
||||
(define tw-spacing-props {:ml "margin-left:{v}" :mr "margin-right:{v}" :mt "margin-top:{v}" :mb "margin-bottom:{v}" :pl "padding-left:{v}" :gap-y "row-gap:{v}" :m "margin:{v}" :gap-x "column-gap:{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}" :gap "gap:{v}" :py "padding-top:{v};padding-bottom:{v}" :pt "padding-top:{v}" :mx "margin-left:{v};margin-right:{v}"})
|
||||
|
||||
(define tw-displays {:flex "flex" :table "table" :grid "grid" :inline-block "inline-block" :table-row "table-row" :inline "inline" :hidden "none" :block "block" :contents "contents" :inline-flex "inline-flex" :inline-grid "inline-grid" :table-cell "table-cell"})
|
||||
|
||||
(define tw-max-widths {:xs "20rem" :3xl "48rem" :7xl "80rem" :sm "24rem" :xl "36rem" :full "100%" :md "28rem" :6xl "72rem" :prose "65ch" :max "max-content" :5xl "64rem" :min "min-content" :lg "32rem" :2xl "42rem" :4xl "56rem" :none "none" :screen "100vw" :fit "fit-content"})
|
||||
|
||||
(define tw-min-widths {:full "100%" :0 "0px" :max "max-content" :min "min-content" :fit "fit-content"})
|
||||
|
||||
(define
|
||||
tw-resolve-layout
|
||||
(fn
|
||||
(token)
|
||||
(let
|
||||
((parts (split token "-"))
|
||||
(head (first parts))
|
||||
(rest (slice parts 1)))
|
||||
(cond
|
||||
(and (= (len parts) 1) (not (nil? (get tw-displays head))))
|
||||
(str "display:" (get tw-displays head))
|
||||
(and (= (len parts) 2) (not (nil? (get tw-displays token))))
|
||||
(str "display:" (get tw-displays token))
|
||||
(and (get tw-spacing-props head) (= (len rest) 1))
|
||||
(let
|
||||
((tmpl (get tw-spacing-props head))
|
||||
(v (tw-spacing-value (first rest))))
|
||||
(if (nil? v) nil (tw-template tmpl v)))
|
||||
(and
|
||||
(= (len rest) 2)
|
||||
(get tw-spacing-props (str head "-" (first rest))))
|
||||
(let
|
||||
((tmpl (get tw-spacing-props (str head "-" (first rest))))
|
||||
(v (tw-spacing-value (last rest))))
|
||||
(if (nil? v) nil (tw-template tmpl v)))
|
||||
(and
|
||||
(= head "space")
|
||||
(= (len rest) 2)
|
||||
(or (= (first rest) "x") (= (first rest) "y")))
|
||||
(let
|
||||
((v (tw-spacing-value (last rest))) (dir (first rest)))
|
||||
(if (nil? v) nil (if (= dir "x") {:suffix ">*+*" :css (str "margin-left:" v)} {:suffix ">*+*" :css (str "margin-top:" v)})))
|
||||
(and (= head "flex") (empty? rest))
|
||||
"display:flex"
|
||||
(and (= head "flex") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"row"
|
||||
"flex-direction:row"
|
||||
"col"
|
||||
"flex-direction:column"
|
||||
"wrap"
|
||||
"flex-wrap:wrap"
|
||||
"nowrap"
|
||||
"flex-wrap:nowrap"
|
||||
"1"
|
||||
"flex:1 1 0%"
|
||||
"auto"
|
||||
"flex:1 1 auto"
|
||||
"initial"
|
||||
"flex:0 1 auto"
|
||||
"none"
|
||||
"flex:none"
|
||||
:else nil)
|
||||
(and (= head "flex") (= (len rest) 2))
|
||||
(case
|
||||
(join "-" rest)
|
||||
"row-reverse"
|
||||
"flex-direction:row-reverse"
|
||||
"col-reverse"
|
||||
"flex-direction:column-reverse"
|
||||
"wrap-reverse"
|
||||
"flex-wrap:wrap-reverse"
|
||||
:else nil)
|
||||
(= head "grow")
|
||||
(if
|
||||
(empty? rest)
|
||||
"flex-grow:1"
|
||||
(if (= (first rest) "0") "flex-grow:0" nil))
|
||||
(= head "shrink")
|
||||
(if
|
||||
(empty? rest)
|
||||
"flex-shrink:1"
|
||||
(if (= (first rest) "0") "flex-shrink:0" nil))
|
||||
(and (= head "basis") (= (len rest) 1))
|
||||
(let
|
||||
((val (first rest)))
|
||||
(cond
|
||||
(= val "auto")
|
||||
"flex-basis:auto"
|
||||
(= val "full")
|
||||
"flex-basis:100%"
|
||||
(= val "0")
|
||||
"flex-basis:0px"
|
||||
(contains? val "/")
|
||||
(let
|
||||
((frac (split val "/")))
|
||||
(if
|
||||
(= (len frac) 2)
|
||||
(let
|
||||
((num (parse-int (first frac) nil))
|
||||
(den (parse-int (nth frac 1) nil)))
|
||||
(if
|
||||
(or (nil? num) (nil? den))
|
||||
nil
|
||||
(str "flex-basis:" (* (/ num den) 100) "%")))
|
||||
nil))
|
||||
:else (let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str "flex-basis:" (* n 0.25) "rem")))))
|
||||
(and (= head "justify") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"start"
|
||||
"justify-content:flex-start"
|
||||
"end"
|
||||
"justify-content:flex-end"
|
||||
"center"
|
||||
"justify-content:center"
|
||||
"between"
|
||||
"justify-content:space-between"
|
||||
"around"
|
||||
"justify-content:space-around"
|
||||
"evenly"
|
||||
"justify-content:space-evenly"
|
||||
"stretch"
|
||||
"justify-content:stretch"
|
||||
:else nil)
|
||||
(and (= head "items") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"start"
|
||||
"align-items:flex-start"
|
||||
"end"
|
||||
"align-items:flex-end"
|
||||
"center"
|
||||
"align-items:center"
|
||||
"baseline"
|
||||
"align-items:baseline"
|
||||
"stretch"
|
||||
"align-items:stretch"
|
||||
:else nil)
|
||||
(and (= head "self") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"auto"
|
||||
"align-self:auto"
|
||||
"start"
|
||||
"align-self:flex-start"
|
||||
"end"
|
||||
"align-self:flex-end"
|
||||
"center"
|
||||
"align-self:center"
|
||||
"stretch"
|
||||
"align-self:stretch"
|
||||
"baseline"
|
||||
"align-self:baseline"
|
||||
:else nil)
|
||||
(and (= head "content") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"start"
|
||||
"align-content:flex-start"
|
||||
"end"
|
||||
"align-content:flex-end"
|
||||
"center"
|
||||
"align-content:center"
|
||||
"between"
|
||||
"align-content:space-between"
|
||||
"around"
|
||||
"align-content:space-around"
|
||||
"evenly"
|
||||
"align-content:space-evenly"
|
||||
"stretch"
|
||||
"align-content:stretch"
|
||||
:else nil)
|
||||
(and (= head "order") (= (len rest) 1))
|
||||
(let
|
||||
((val (first rest)))
|
||||
(cond
|
||||
(= val "first")
|
||||
"order:-9999"
|
||||
(= val "last")
|
||||
"order:9999"
|
||||
(= val "none")
|
||||
"order:0"
|
||||
:else (let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str "order:" n)))))
|
||||
(and (= head "grid") (empty? rest))
|
||||
"display:grid"
|
||||
(and (= head "grid") (>= (len rest) 2) (= (first rest) "cols"))
|
||||
(let
|
||||
((val (join "-" (slice rest 1))))
|
||||
(cond
|
||||
(= val "none")
|
||||
"grid-template-columns:none"
|
||||
(= val "subgrid")
|
||||
"grid-template-columns:subgrid"
|
||||
:else (let
|
||||
((n (parse-int val nil)))
|
||||
(if
|
||||
(nil? n)
|
||||
nil
|
||||
(str "grid-template-columns:repeat(" n ",minmax(0,1fr))")))))
|
||||
(and (= head "grid") (>= (len rest) 2) (= (first rest) "rows"))
|
||||
(let
|
||||
((val (join "-" (slice rest 1))))
|
||||
(cond
|
||||
(= val "none")
|
||||
"grid-template-rows:none"
|
||||
(= val "subgrid")
|
||||
"grid-template-rows:subgrid"
|
||||
:else (let
|
||||
((n (parse-int val nil)))
|
||||
(if
|
||||
(nil? n)
|
||||
nil
|
||||
(str "grid-template-rows:repeat(" n ",minmax(0,1fr))")))))
|
||||
(and (= head "grid") (>= (len rest) 2) (= (first rest) "flow"))
|
||||
(case
|
||||
(nth rest 1)
|
||||
"row"
|
||||
"grid-auto-flow:row"
|
||||
"col"
|
||||
"grid-auto-flow:column"
|
||||
"dense"
|
||||
"grid-auto-flow:dense"
|
||||
:else nil)
|
||||
(and (= head "col") (>= (len rest) 2))
|
||||
(let
|
||||
((sub (first rest)) (val (join "-" (slice rest 1))))
|
||||
(cond
|
||||
(and (= sub "span") (= val "full"))
|
||||
"grid-column:1 / -1"
|
||||
(= sub "span")
|
||||
(let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str "grid-column:span " n " / span " n)))
|
||||
(= sub "start")
|
||||
(str "grid-column-start:" val)
|
||||
(= sub "end")
|
||||
(str "grid-column-end:" val)
|
||||
:else nil))
|
||||
(and (= head "row") (>= (len rest) 2))
|
||||
(let
|
||||
((sub (first rest)) (val (join "-" (slice rest 1))))
|
||||
(cond
|
||||
(and (= sub "span") (= val "full"))
|
||||
"grid-row:1 / -1"
|
||||
(= sub "span")
|
||||
(let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str "grid-row:span " n " / span " n)))
|
||||
(= sub "start")
|
||||
(str "grid-row-start:" val)
|
||||
(= sub "end")
|
||||
(str "grid-row-end:" val)
|
||||
:else nil))
|
||||
(and (= head "auto") (>= (len rest) 2))
|
||||
(let
|
||||
((sub (first rest)) (val (join "-" (slice rest 1))))
|
||||
(cond
|
||||
(and (= sub "cols") (= val "auto"))
|
||||
"grid-auto-columns:auto"
|
||||
(and (= sub "cols") (= val "min"))
|
||||
"grid-auto-columns:min-content"
|
||||
(and (= sub "cols") (= val "max"))
|
||||
"grid-auto-columns:max-content"
|
||||
(and (= sub "cols") (= val "fr"))
|
||||
"grid-auto-columns:minmax(0,1fr)"
|
||||
(and (= sub "rows") (= val "auto"))
|
||||
"grid-auto-rows:auto"
|
||||
(and (= sub "rows") (= val "min"))
|
||||
"grid-auto-rows:min-content"
|
||||
(and (= sub "rows") (= val "max"))
|
||||
"grid-auto-rows:max-content"
|
||||
(and (= sub "rows") (= val "fr"))
|
||||
"grid-auto-rows:minmax(0,1fr)"
|
||||
:else nil))
|
||||
(and
|
||||
(= (len parts) 1)
|
||||
(or
|
||||
(= head "relative")
|
||||
(= head "absolute")
|
||||
(= head "fixed")
|
||||
(= head "sticky")
|
||||
(= head "static")))
|
||||
(str "position:" head)
|
||||
(and
|
||||
(or
|
||||
(= head "top")
|
||||
(= head "right")
|
||||
(= head "bottom")
|
||||
(= head "left"))
|
||||
(= (len rest) 1))
|
||||
(let
|
||||
((v (tw-spacing-value (first rest))))
|
||||
(if (nil? v) nil (str head ":" v)))
|
||||
(and (= head "inset") (= (len rest) 1))
|
||||
(let
|
||||
((v (tw-spacing-value (first rest))))
|
||||
(if (nil? v) nil (str "inset:" v)))
|
||||
(and (= head "inset") (= (len rest) 2))
|
||||
(let
|
||||
((dir (first rest)) (v (tw-spacing-value (nth rest 1))))
|
||||
(if
|
||||
(nil? v)
|
||||
nil
|
||||
(case
|
||||
dir
|
||||
"x"
|
||||
(str "left:" v ";right:" v)
|
||||
"y"
|
||||
(str "top:" v ";bottom:" v)
|
||||
:else nil)))
|
||||
(and (= head "z") (= (len rest) 1))
|
||||
(if
|
||||
(= (first rest) "auto")
|
||||
"z-index:auto"
|
||||
(let
|
||||
((n (parse-int (first rest) nil)))
|
||||
(if (nil? n) nil (str "z-index:" n))))
|
||||
(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")
|
||||
(contains? val "/")
|
||||
(let
|
||||
((frac (split val "/")))
|
||||
(if
|
||||
(= (len frac) 2)
|
||||
(let
|
||||
((num (parse-int (first frac) nil))
|
||||
(den (parse-int (nth frac 1) nil)))
|
||||
(if
|
||||
(or (nil? num) (nil? den))
|
||||
nil
|
||||
(str prop ":" (* (/ num den) 100) "%")))
|
||||
nil))
|
||||
:else (let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str prop ":" (* n 0.25) "rem")))))
|
||||
(and (= head "max") (>= (len rest) 2) (= (first rest) "w"))
|
||||
(let
|
||||
((val-name (join "-" (slice rest 1)))
|
||||
(val (get tw-max-widths val-name)))
|
||||
(if (nil? val) nil (str "max-width:" val)))
|
||||
(and (= head "max") (>= (len rest) 2) (= (first rest) "h"))
|
||||
(let
|
||||
((val (first (slice rest 1))))
|
||||
(cond
|
||||
(= val "full")
|
||||
"max-height:100%"
|
||||
(= val "screen")
|
||||
"max-height:100vh"
|
||||
(= val "none")
|
||||
"max-height:none"
|
||||
:else (let
|
||||
((n (parse-int val nil)))
|
||||
(if (nil? n) nil (str "max-height:" (* n 0.25) "rem")))))
|
||||
(and (= head "min") (>= (len rest) 2) (= (first rest) "w"))
|
||||
(let
|
||||
((val-name (join "-" (slice rest 1)))
|
||||
(val (get tw-min-widths val-name)))
|
||||
(if (nil? val) nil (str "min-width:" val)))
|
||||
(and (= head "min") (>= (len rest) 2) (= (first rest) "h"))
|
||||
(let
|
||||
((val (first (slice rest 1))))
|
||||
(cond
|
||||
(= val "0")
|
||||
"min-height:0px"
|
||||
(= val "full")
|
||||
"min-height:100%"
|
||||
(= val "screen")
|
||||
"min-height:100vh"
|
||||
:else nil))
|
||||
(and (= head "overflow") (= (len rest) 1))
|
||||
(str "overflow:" (first rest))
|
||||
(and (= head "overflow") (= (len rest) 2))
|
||||
(str "overflow-" (first rest) ":" (nth rest 1))
|
||||
(and (= head "aspect") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"auto"
|
||||
"aspect-ratio:auto"
|
||||
"square"
|
||||
"aspect-ratio:1 / 1"
|
||||
"video"
|
||||
"aspect-ratio:16 / 9"
|
||||
:else nil)
|
||||
(and (= head "object") (= (len rest) 1))
|
||||
(str "object-fit:" (first rest))
|
||||
(and (= (len parts) 1) (= head "visible"))
|
||||
"visibility:visible"
|
||||
(and (= (len parts) 1) (= head "invisible"))
|
||||
"visibility:hidden"
|
||||
(and (= (len parts) 1) (= head "collapse"))
|
||||
"visibility:collapse"
|
||||
(and (= (len parts) 1) (= head "container"))
|
||||
"width:100%;max-width:100%"
|
||||
(and (= (len parts) 1) (= head "isolate"))
|
||||
"isolation:isolate"
|
||||
:else nil))))
|
||||
3
shared/static/wasm/sx/tw-layout.sxbc
Normal file
3
shared/static/wasm/sx/tw-layout.sxbc
Normal file
File diff suppressed because one or more lines are too long
213
shared/static/wasm/sx/tw-type.sx
Normal file
213
shared/static/wasm/sx/tw-type.sx
Normal file
@@ -0,0 +1,213 @@
|
||||
(define tw-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"})
|
||||
|
||||
(define tw-weights {:light "300" :semibold "600" :bold "700" :extrabold "800" :black "900" :extralight "200" :thin "100" :medium "500" :normal "400"})
|
||||
|
||||
(define tw-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"})
|
||||
|
||||
(define tw-alignments {:center true :end true :left true :right true :start true :justify true})
|
||||
|
||||
(define tw-leading {:tight "1.25" :9 "2.25rem" :loose "2" :relaxed "1.625" :3 "0.75rem" :8 "2rem" :5 "1.25rem" :4 "1rem" :6 "1.5rem" :snug "1.375" :none "1" :normal "1.5" :7 "1.75rem" :10 "2.5rem"})
|
||||
|
||||
(define tw-tracking {:wide "0.025em" :tight "-0.025em" :tighter "-0.05em" :wider "0.05em" :widest "0.1em" :normal "0em"})
|
||||
|
||||
(define
|
||||
tw-resolve-type
|
||||
(fn
|
||||
(token)
|
||||
(let
|
||||
((parts (split token "-"))
|
||||
(head (first parts))
|
||||
(rest (slice parts 1)))
|
||||
(cond
|
||||
(and
|
||||
(= head "text")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get tw-sizes (first rest)))))
|
||||
(get tw-sizes (first rest))
|
||||
(and
|
||||
(= head "text")
|
||||
(= (len rest) 1)
|
||||
(get tw-alignments (first rest)))
|
||||
(str "text-align:" (first rest))
|
||||
(and
|
||||
(= (len parts) 1)
|
||||
(or
|
||||
(= head "uppercase")
|
||||
(= head "lowercase")
|
||||
(= head "capitalize")))
|
||||
(str "text-transform:" head)
|
||||
(and (= (len parts) 2) (= head "normal") (= (first rest) "case"))
|
||||
"text-transform:none"
|
||||
(and
|
||||
(= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get tw-weights (first rest)))))
|
||||
(str "font-weight:" (get tw-weights (first rest)))
|
||||
(and
|
||||
(= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get tw-families (first rest)))))
|
||||
(str "font-family:" (get tw-families (first rest)))
|
||||
(and (= (len parts) 1) (= head "italic"))
|
||||
"font-style:italic"
|
||||
(and (= (len parts) 2) (= head "not") (= (first rest) "italic"))
|
||||
"font-style:normal"
|
||||
(and (= head "leading") (= (len rest) 1))
|
||||
(let
|
||||
((val (get tw-leading (first rest))))
|
||||
(if (nil? val) nil (str "line-height:" val)))
|
||||
(and (= head "tracking") (= (len rest) 1))
|
||||
(let
|
||||
((val (get tw-tracking (first rest))))
|
||||
(if (nil? val) nil (str "letter-spacing:" val)))
|
||||
(and (= head "whitespace") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"normal"
|
||||
"white-space:normal"
|
||||
"nowrap"
|
||||
"white-space:nowrap"
|
||||
"pre"
|
||||
"white-space:pre"
|
||||
"pre-line"
|
||||
"white-space:pre-line"
|
||||
"pre-wrap"
|
||||
"white-space:pre-wrap"
|
||||
"break-spaces"
|
||||
"white-space:break-spaces"
|
||||
:else nil)
|
||||
(and (= head "whitespace") (= (len rest) 2))
|
||||
(let
|
||||
((val (join "-" rest)))
|
||||
(case
|
||||
val
|
||||
"pre-line"
|
||||
"white-space:pre-line"
|
||||
"pre-wrap"
|
||||
"white-space:pre-wrap"
|
||||
"break-spaces"
|
||||
"white-space:break-spaces"
|
||||
:else nil))
|
||||
(and (= head "break") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"normal"
|
||||
"overflow-wrap:normal;word-break:normal"
|
||||
"words"
|
||||
"overflow-wrap:break-word"
|
||||
"all"
|
||||
"word-break:break-all"
|
||||
"keep"
|
||||
"word-break:keep-all"
|
||||
:else nil)
|
||||
(and (= (len parts) 1) (= head "truncate"))
|
||||
"overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
|
||||
(and (= head "line") (= (len rest) 2) (= (first rest) "clamp"))
|
||||
(let
|
||||
((val (nth rest 1)))
|
||||
(if
|
||||
(= val "none")
|
||||
"overflow:visible;display:block;-webkit-line-clamp:unset"
|
||||
(let
|
||||
((n (parse-int val nil)))
|
||||
(if
|
||||
(nil? n)
|
||||
nil
|
||||
(str
|
||||
"overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:"
|
||||
n)))))
|
||||
(and (= head "indent") (= (len rest) 1))
|
||||
(let
|
||||
((v (tw-spacing-value (first rest))))
|
||||
(if (nil? v) nil (str "text-indent:" v)))
|
||||
(and (= head "align") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"baseline"
|
||||
"vertical-align:baseline"
|
||||
"top"
|
||||
"vertical-align:top"
|
||||
"middle"
|
||||
"vertical-align:middle"
|
||||
"bottom"
|
||||
"vertical-align:bottom"
|
||||
"text-top"
|
||||
"vertical-align:text-top"
|
||||
"text-bottom"
|
||||
"vertical-align:text-bottom"
|
||||
"sub"
|
||||
"vertical-align:sub"
|
||||
"super"
|
||||
"vertical-align:super"
|
||||
:else nil)
|
||||
(and (= head "align") (= (len rest) 2))
|
||||
(let
|
||||
((val (join "-" rest)))
|
||||
(case
|
||||
val
|
||||
"text-top"
|
||||
"vertical-align:text-top"
|
||||
"text-bottom"
|
||||
"vertical-align:text-bottom"
|
||||
:else nil))
|
||||
(and (= head "list") (= (len rest) 1))
|
||||
(case
|
||||
(first rest)
|
||||
"none"
|
||||
"list-style-type:none"
|
||||
"disc"
|
||||
"list-style-type:disc"
|
||||
"decimal"
|
||||
"list-style-type:decimal"
|
||||
"inside"
|
||||
"list-style-position:inside"
|
||||
"outside"
|
||||
"list-style-position:outside"
|
||||
:else nil)
|
||||
(and
|
||||
(= head "text")
|
||||
(= (len rest) 1)
|
||||
(or
|
||||
(= (first rest) "wrap")
|
||||
(= (first rest) "nowrap")
|
||||
(= (first rest) "balance")
|
||||
(= (first rest) "pretty")))
|
||||
(str "text-wrap:" (first rest))
|
||||
(and (= head "hyphens") (= (len rest) 1))
|
||||
(str "hyphens:" (first rest))
|
||||
(and (= head "content") (= (len rest) 1) (= (first rest) "none"))
|
||||
"content:none"
|
||||
(and (= (len parts) 1) (= head "antialiased"))
|
||||
"-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale"
|
||||
(and
|
||||
(= (len parts) 2)
|
||||
(= head "subpixel")
|
||||
(= (first rest) "antialiased"))
|
||||
"-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto"
|
||||
(and (= (len parts) 2) (= (first rest) "nums"))
|
||||
(case
|
||||
head
|
||||
"tabular"
|
||||
"font-variant-numeric:tabular-nums"
|
||||
"proportional"
|
||||
"font-variant-numeric:proportional-nums"
|
||||
"lining"
|
||||
"font-variant-numeric:lining-nums"
|
||||
"oldstyle"
|
||||
"font-variant-numeric:oldstyle-nums"
|
||||
:else nil)
|
||||
(and (= (len parts) 2) (= (first rest) "fractions"))
|
||||
(case
|
||||
head
|
||||
"diagonal"
|
||||
"font-variant-numeric:diagonal-fractions"
|
||||
"stacked"
|
||||
"font-variant-numeric:stacked-fractions"
|
||||
:else nil)
|
||||
(and (= (len parts) 2) (= head "normal") (= (first rest) "nums"))
|
||||
"font-variant-numeric:normal"
|
||||
(and (= (len parts) 1) (= head "ordinal"))
|
||||
"font-variant-numeric:ordinal"
|
||||
(and (= (len parts) 2) (= head "slashed") (= (first rest) "zero"))
|
||||
"font-variant-numeric:slashed-zero"
|
||||
:else nil))))
|
||||
3
shared/static/wasm/sx/tw-type.sxbc
Normal file
3
shared/static/wasm/sx/tw-type.sxbc
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user