Files
rose-ash/shared/sx/templates/tw.sx
giles 609be68c9c ~tw: complete Tailwind implementation in pure SX, split into 3 files
- tw.sx: core engine (HSL color computation, token processor, responsive/
  state prefixes) + visual styles (colors, borders, shadows, ring, opacity,
  transitions, animations, transforms, cursor, decoration)
- tw-layout.sx: spatial arrangement (flex, grid, position, spacing,
  display, sizing, overflow, z-index, aspect-ratio, visibility)
- tw-type.sx: typography (font size/weight/family, line-height, tracking,
  text alignment/transform/wrap, whitespace, word-break, truncate, lists,
  hyphens, font-variant-numeric, antialiasing)

22 color names × infinite shades via HSL computation (not lookup tables).
Full responsive (sm/md/lg/xl/2xl) and state (hover/focus/active/disabled/
first/last/odd/even/visited/checked/before/after) prefix support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:42:32 +00:00

445 lines
15 KiB
Plaintext

(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} :fuchsia {:s 84 :h 292} :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)
(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))
:else 13)))
(define
colour
(fn
(name shade)
(let
((base (get colour-bases name)))
(if
(nil? base)
name
(if
(= name "white")
"#ffffff"
(if
(= name "black")
"#000000"
(let
((h (get base "h"))
(s (get base "s"))
(l (shade-to-lightness shade)))
(str "hsl(" h "," s "%," (round l) "%)"))))))))
(define tw-colour-props {:ring "--tw-ring-color" :outline "outline-color" :bg "background-color" :accent "accent-color" :border "border-color" :stroke "stroke" :text "color" :fill "fill"})
(define tw-breakpoints {:sm "640px" :xl "1280px" :md "768px" :lg "1024px" :2xl "1536px"})
(define tw-states {:focus ":focus" :before "::before" :first ":first-child" :disabled ":disabled" :required ":required" :even ":nth-child(even)" :hover ":hover" :focus-visible ":focus-visible" :last ":last-child" :visited ":visited" :odd ":nth-child(odd)" :active ":active" :focus-within ":focus-within" :checked ":checked" :placeholder "::placeholder" :after "::after"})
(define
tw-spacing-value
(fn
(v)
(cond
(= v "auto")
"auto"
(= v "px")
"1px"
(= v "0")
"0px"
(= v "0.5")
"0.125rem"
(= v "1.5")
"0.375rem"
(= v "2.5")
"0.625rem"
(= v "3.5")
"0.875rem"
:else (let
((n (parse-int v nil)))
(if (nil? n) nil (str (* n 0.25) "rem"))))))
(define
tw-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))))))))))
(define tw-shadow-sizes {:sm "0 1px 2px 0 rgb(0 0 0 / 0.05)" :xl "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" :md "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" :inner "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)" :lg "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" :2xl "0 25px 50px -12px rgb(0 0 0 / 0.25)" :none "0 0 #0000" : "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"})
(define tw-rounded-sizes {:3xl "1.5rem" :sm "0.125rem" :xl "0.75rem" :full "9999px" :md "0.375rem" :lg "0.5rem" :2xl "1rem" :none "0" : "0.25rem"})
(define tw-border-widths {:0 "0px" :2 "2px" :8 "8px" :4 "4px" : "1px"})
(define
tw-resolve-style
(fn
(token)
(let
((parts (split token "-"))
(head (first parts))
(rest (slice parts 1)))
(cond
(and
(get tw-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 tw-colour-props head))
(cname (join "-" (slice rest 0 (- (len rest) 1))))
(shade (parse-int (last rest) 0)))
(str css-prop ":" (colour cname shade)))
(and
(get tw-colour-props head)
(= (len rest) 1)
(or
(= (first rest) "white")
(= (first rest) "black")
(= (first rest) "transparent")
(= (first rest) "current")
(= (first rest) "inherit")))
(let
((css-prop (get tw-colour-props head))
(val
(case
(first rest)
"white"
"#ffffff"
"black"
"#000000"
"transparent"
"transparent"
"current"
"currentColor"
"inherit"
"inherit")))
(str css-prop ":" val))
(= head "rounded")
(cond
(empty? rest)
(str "border-radius:" (get tw-rounded-sizes ""))
(and
(= (len rest) 1)
(not (nil? (get tw-rounded-sizes (first rest)))))
(str "border-radius:" (get tw-rounded-sizes (first rest)))
(and
(>= (len rest) 1)
(or
(= (first rest) "t")
(= (first rest) "b")
(= (first rest) "l")
(= (first rest) "r")))
(let
((size (if (>= (len rest) 2) (get tw-rounded-sizes (nth rest 1)) (get tw-rounded-sizes "")))
(dir (first rest)))
(if
(nil? size)
nil
(case
dir
"t"
(str
"border-top-left-radius:"
size
";border-top-right-radius:"
size)
"b"
(str
"border-bottom-left-radius:"
size
";border-bottom-right-radius:"
size)
"l"
(str
"border-top-left-radius:"
size
";border-bottom-left-radius:"
size)
"r"
(str
"border-top-right-radius:"
size
";border-bottom-right-radius:"
size)
:else nil)))
:else nil)
(= head "border")
(cond
(empty? rest)
"border-width:1px"
(and
(= (len rest) 1)
(not (nil? (get tw-border-widths (first rest)))))
(str "border-width:" (get tw-border-widths (first rest)))
(and
(= (len rest) 1)
(or
(= (first rest) "t")
(= (first rest) "b")
(= (first rest) "l")
(= (first rest) "r")
(= (first rest) "x")
(= (first rest) "y")))
(let
((side (first rest)))
(case
side
"t"
"border-top-width:1px"
"b"
"border-bottom-width:1px"
"l"
"border-left-width:1px"
"r"
"border-right-width:1px"
"x"
"border-left-width:1px;border-right-width:1px"
"y"
"border-top-width:1px;border-bottom-width:1px"
:else nil))
(and
(= (len rest) 2)
(not (nil? (get tw-border-widths (nth rest 1)))))
(let
((side (first rest)) (w (get tw-border-widths (nth rest 1))))
(case
side
"t"
(str "border-top-width:" w)
"b"
(str "border-bottom-width:" w)
"l"
(str "border-left-width:" w)
"r"
(str "border-right-width:" w)
:else nil))
:else nil)
(= head "shadow")
(let
((size-key (if (empty? rest) "" (join "-" rest))))
(let
((val (get tw-shadow-sizes size-key)))
(if (nil? val) nil (str "box-shadow:" val))))
(and (= head "opacity") (= (len rest) 1))
(let
((n (parse-int (first rest) nil)))
(if (nil? n) nil (str "opacity:" (/ n 100))))
(and (= head "ring") (or (empty? rest) (= (len rest) 1)))
(let
((w (if (empty? rest) "3px" (let ((n (parse-int (first rest) nil))) (if (nil? n) nil (str n "px"))))))
(if
(nil? w)
nil
(str
"box-shadow:0 0 0 "
w
" var(--tw-ring-color, rgb(59 130 246 / 0.5))")))
(= head "outline")
(cond
(and (= (len rest) 1) (= (first rest) "none"))
"outline:2px solid transparent;outline-offset:2px"
(empty? rest)
"outline-style:solid"
:else nil)
(= head "transition")
(cond
(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"
(= (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"
(= (first rest) "all")
"transition-property:all;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
(= (first rest) "none")
"transition-property:none"
(= (first rest) "opacity")
"transition-property:opacity;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
(= (first rest) "shadow")
"transition-property:box-shadow;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
(= (first rest) "transform")
"transition-property:transform;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
:else nil)
(and (= head "duration") (= (len rest) 1))
(str "transition-duration:" (first rest) "ms")
(= head "ease")
(let
((val (join "-" rest)))
(case
val
"linear"
"transition-timing-function:linear"
"in"
"transition-timing-function:cubic-bezier(0.4,0,1,1)"
"out"
"transition-timing-function:cubic-bezier(0,0,0.2,1)"
"in-out"
"transition-timing-function:cubic-bezier(0.4,0,0.2,1)"
:else nil))
(and (= head "cursor") (= (len rest) 1))
(str "cursor:" (first rest))
(and (= head "cursor") (= (len rest) 2))
(str "cursor:" (join "-" rest))
(and
(= head "pointer")
(= (len rest) 2)
(= (first rest) "events"))
(str "pointer-events:" (nth rest 1))
(and (= head "select") (= (len rest) 1))
(str "user-select:" (first rest))
(and (= head "appearance") (= (len rest) 1))
(str "appearance:" (first rest))
(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"))
"text-decoration-line:none"
(and (= head "scale") (= (len rest) 1))
(let
((n (parse-int (first rest) nil)))
(if (nil? n) nil (str "transform:scale(" (/ n 100) ")")))
(and (= head "rotate") (= (len rest) 1))
(str "transform:rotate(" (first rest) "deg)")
(= head "animate")
(cond
(= (first rest) "spin")
"animation:spin 1s linear infinite"
(= (first rest) "ping")
"animation:ping 1s cubic-bezier(0,0,0.2,1) infinite"
(= (first rest) "pulse")
"animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite"
(= (first rest) "bounce")
"animation:bounce 1s infinite"
(= (first rest) "none")
"animation:none"
:else nil)
:else nil))))
(define
tw-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)
(do
(if
(get tw-breakpoints (first colon-parts))
(do
(set! bp (first colon-parts))
(set! base (nth colon-parts 1)))
(do
(set! state (first colon-parts))
(set! base (nth colon-parts 1)))))
(= n 3)
(do
(set! bp (first colon-parts))
(set! state (nth colon-parts 1))
(set! base (nth colon-parts 2)))
:else (do (set! base token)))
(let
((negative (and (>= (len base) 2) (= (substring base 0 1) "-")))
(actual-base (if negative (substring base 1 (len base)) base)))
(let
((css (or (tw-resolve-style actual-base) (tw-resolve-layout actual-base) (tw-resolve-type actual-base))))
(if
(nil? css)
nil
(let
((final-css (if negative (str css) css))
(cls
(str
"sx-"
(replace token ":" "-")
(replace token "." "d")))
(selector
(str "." cls (if state (get tw-states state) "")))
(rule
(if
bp
(str
"@media(min-width:"
(get tw-breakpoints bp)
"){"
selector
"{"
final-css
"}}")
(str selector "{" final-css "}"))))
{:rule rule :cls cls}))))))))
(defcomp
~tw
(&key tokens)
(let
((token-list (filter (fn (t) (not (= t ""))) (split (or tokens "") " ")))
(results (map tw-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)))
(if (empty? classes) nil (make-spread {:class (join " " classes) :data-tw (or tokens "")}))))
(defcomp
~tw/flush
()
:affinity :client
(let
((rules (collected "cssx")) (head-style (dom-query "#sx-css")))
(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)))))))