;; @client — send all define forms to browser for client-side use. ;; CSSX — computed CSS from s-expressions. ;; ;; Generic mechanism: cssx is a macro that groups CSS property declarations. ;; The vocabulary (property mappings, value functions) is pluggable — the ;; Tailwind-inspired defaults below are just one possible style system. ;; ;; Usage: ;; (cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono")) ;; (:bg (colour "stone" 50))) ;; ;; Each group is (:keyword value ...modifiers): ;; - keyword maps to a CSS property via cssx-properties dict ;; - value is the CSS value for that property ;; - modifiers are extra CSS declaration strings, concatenated in ;; ;; Single group: ;; (cssx (:text (colour "violet" 699))) ;; ;; Modifiers without a colour: ;; (cssx (:text nil (size "4xl") (weight "bold"))) ;; ;; Unknown keywords pass through as raw CSS property names: ;; (cssx (:outline (colour "red" 500))) → "outline:hsl(0,72%,53%);" ;; ;; Standalone modifiers work outside cssx too: ;; :style (size "4xl") ;; :style (str (weight "bold") (family "mono")) ;; ========================================================================= ;; Layer 1: Generic mechanism — cssx macro + cssxgroup function ;; ========================================================================= ;; Property keyword → CSS property name. Extend this dict for new mappings. (define cssx-properties {"text" "color" "bg" "background-color" "border" "border-color"}) ;; Evaluate one property group: (:text value modifier1 modifier2 ...) ;; If value is nil, only modifiers are emitted (no property declaration). ;; NOTE: name must NOT contain hyphens — the evaluator's isRenderExpr check ;; treats (hyphenated-name :keyword ...) as a custom HTML element. (define cssxgroup (fn (prop value b c d e) (let ((css-prop (or (get cssx-properties prop) prop))) (str (if (nil? value) "" (str css-prop ":" value ";")) (or b "") (or c "") (or d "") (or e ""))))) ;; cssx macro — takes one or more property groups, expands to (str ...). ;; (cssx (:text val ...) (:bg val ...)) ;; → (str (cssxgroup :text val ...) (cssxgroup :bg val ...)) (defmacro cssx (&rest groups) `(str ,@(map (fn (g) (cons 'cssxgroup g)) groups))) ;; ========================================================================= ;; Layer 2: Value vocabulary — colour, size, weight, family ;; These are independent functions. Use inside cssx groups or standalone. ;; Replace or extend with any style system. ;; ========================================================================= ;; --------------------------------------------------------------------------- ;; Colour — compute CSS colour value from name + shade ;; --------------------------------------------------------------------------- (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}}) (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)) true 13))) (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))) (str "hsl(" h "," s "%," (round l) "%)")))))) ;; --------------------------------------------------------------------------- ;; Font sizes — named size → font-size + line-height (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;"}) ;; --------------------------------------------------------------------------- ;; Font weights — named weight → numeric value ;; --------------------------------------------------------------------------- (define cssx-weights {"thin" "100" "extralight" "200" "light" "300" "normal" "400" "medium" "500" "semibold" "600" "bold" "700" "extrabold" "800" "black" "900"}) ;; --------------------------------------------------------------------------- ;; Font families — named family → CSS font stack ;; --------------------------------------------------------------------------- (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"}) ;; --------------------------------------------------------------------------- ;; Standalone modifier functions — return CSS declaration strings ;; Each returns a complete CSS declaration string. Use inside cssx groups ;; or standalone on :style with str. ;; --------------------------------------------------------------------------- ;; -- Typography -- (define size (fn (s) (or (get cssx-sizes s) (str "font-size:" s ";")))) (define weight (fn (w) (let ((v (get cssx-weights w))) (str "font-weight:" (or v w) ";")))) (define family (fn (f) (let ((v (get cssx-families f))) (str "font-family:" (or v f) ";")))) (define align (fn (a) (str "text-align:" a ";"))) (define decoration (fn (d) (str "text-decoration:" d ";"))) ;; -- Spacing (Tailwind scale: 1 unit = 0.25rem) -- (define spacing (fn (n) (str (* n 0.25) "rem"))) (define p (fn (n) (str "padding:" (spacing n) ";"))) (define px (fn (n) (str "padding-left:" (spacing n) ";padding-right:" (spacing n) ";"))) (define py (fn (n) (str "padding-top:" (spacing n) ";padding-bottom:" (spacing n) ";"))) (define pt (fn (n) (str "padding-top:" (spacing n) ";"))) (define pb (fn (n) (str "padding-bottom:" (spacing n) ";"))) (define pl (fn (n) (str "padding-left:" (spacing n) ";"))) (define pr (fn (n) (str "padding-right:" (spacing n) ";"))) (define m (fn (n) (str "margin:" (spacing n) ";"))) (define mx (fn (n) (str "margin-left:" (spacing n) ";margin-right:" (spacing n) ";"))) (define my (fn (n) (str "margin-top:" (spacing n) ";margin-bottom:" (spacing n) ";"))) (define mt (fn (n) (str "margin-top:" (spacing n) ";"))) (define mb (fn (n) (str "margin-bottom:" (spacing n) ";"))) (define ml (fn (n) (str "margin-left:" (spacing n) ";"))) (define mr (fn (n) (str "margin-right:" (spacing n) ";"))) (define mx-auto (fn () "margin-left:auto;margin-right:auto;")) ;; -- Display & layout -- (define display (fn (d) (str "display:" d ";"))) (define max-w (fn (w) (str "max-width:" w ";"))) ;; 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"})