From 609be68c9c2c070064a06c65e85a2ec8feb21d57 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 21:42:32 +0000 Subject: [PATCH] ~tw: complete Tailwind implementation in pure SX, split into 3 files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- shared/sx/templates/tw-layout.sx | 410 ++++++++++++++++++++++++++++ shared/sx/templates/tw-type.sx | 213 +++++++++++++++ shared/sx/templates/tw.sx | 444 +++++++++++++++++++++++++++++++ 3 files changed, 1067 insertions(+) create mode 100644 shared/sx/templates/tw-layout.sx create mode 100644 shared/sx/templates/tw-type.sx create mode 100644 shared/sx/templates/tw.sx diff --git a/shared/sx/templates/tw-layout.sx b/shared/sx/templates/tw-layout.sx new file mode 100644 index 00000000..4011d119 --- /dev/null +++ b/shared/sx/templates/tw-layout.sx @@ -0,0 +1,410 @@ +(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 + (= head "space") + (= (len rest) 2) + (or (= (first rest) "x") (= (first rest) "y"))) + (let + ((v (tw-spacing-value (nth rest 1))) (dir (first rest))) + (if + (nil? v) + nil + (if (= dir "x") (str "column-gap:" v) (str "row-gap:" 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)))) diff --git a/shared/sx/templates/tw-type.sx b/shared/sx/templates/tw-type.sx new file mode 100644 index 00000000..06244997 --- /dev/null +++ b/shared/sx/templates/tw-type.sx @@ -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)))) diff --git a/shared/sx/templates/tw.sx b/shared/sx/templates/tw.sx new file mode 100644 index 00000000..73f67709 --- /dev/null +++ b/shared/sx/templates/tw.sx @@ -0,0 +1,444 @@ +(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)))))))