Rewrite CSSX: unified Tailwind-style utility token system
Replace the three-layer cssx system (macro + value functions + class components) with a single token resolver. Tokens like "bg-yellow-199", "hover:bg-rose-500", "md:text-xl" are parsed into CSS declarations. Two delivery mechanisms, same token format: - tw() function: returns inline style string for :style - ~cssx/tw macro: injects JIT class + <style> onto first child element The resolver handles: colours (21 names, any shade 0-950), spacing, typography, display, max-width, rounded, opacity, w/h, gap, text decoration, cursor, overflow, transitions. States (hover/focus/active) and responsive breakpoints (sm/md/lg/xl/2xl) for class-based usage. Next step: replace macro/function approach with spec-level primitives (defcontext/provide/context + spread) so ~cssx/tw becomes a proper component returning spread values, with rules collected via context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,68 +1,22 @@
|
||||
;; @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.
|
||||
;; Tailwind-style utility component. Write styling as utility tokens:
|
||||
;;
|
||||
;; Usage:
|
||||
;; (cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono"))
|
||||
;; (:bg (colour "stone" 50)))
|
||||
;; (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold"
|
||||
;; (div "content"))
|
||||
;;
|
||||
;; 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
|
||||
;; (~cssx/tw "hover:bg-rose-500 md:text-xl"
|
||||
;; (button "click me"))
|
||||
;;
|
||||
;; 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"))
|
||||
;; Each token becomes a deterministic class + JIT <style> rule.
|
||||
;; This is one instance of the CSSX component pattern — other styling
|
||||
;; components are possible with different vocabulary.
|
||||
|
||||
;; =========================================================================
|
||||
;; Layer 1: Generic mechanism — cssx macro + cssxgroup function
|
||||
;; Colour data — hue/saturation bases + shade-to-lightness curve
|
||||
;; =========================================================================
|
||||
|
||||
;; 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}
|
||||
@@ -84,7 +38,9 @@
|
||||
"slate" {"h" 215 "s" 16}
|
||||
"gray" {"h" 220 "s" 9}
|
||||
"zinc" {"h" 240 "s" 5}
|
||||
"neutral" {"h" 0 "s" 0}})
|
||||
"neutral" {"h" 0 "s" 0}
|
||||
"white" {"h" 0 "s" 0}
|
||||
"black" {"h" 0 "s" 0}})
|
||||
|
||||
(define lerp (fn (a b t) (+ a (* t (- b a)))))
|
||||
|
||||
@@ -114,29 +70,50 @@
|
||||
(l (shade-to-lightness shade)))
|
||||
(str "hsl(" h "," s "%," (round l) "%)"))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Font sizes — named size → font-size + line-height (Tailwind v3 scale)
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; =========================================================================
|
||||
;; Lookup tables — all the value vocabularies
|
||||
;; =========================================================================
|
||||
|
||||
;; Colour property shorthands → CSS property name
|
||||
(define cssx-colour-props
|
||||
{"bg" "background-color"
|
||||
"text" "color"
|
||||
"border" "border-color"})
|
||||
|
||||
;; 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}"})
|
||||
|
||||
;; 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;"})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Font weights — named weight → numeric value
|
||||
;; ---------------------------------------------------------------------------
|
||||
{"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"})
|
||||
|
||||
;; Named font weights
|
||||
(define cssx-weights
|
||||
{"thin" "100"
|
||||
"extralight" "200"
|
||||
@@ -148,67 +125,25 @@
|
||||
"extrabold" "800"
|
||||
"black" "900"})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Font families — named family → CSS font stack
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; 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"})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Standalone modifier functions — return CSS declaration strings
|
||||
;; Each returns a complete CSS declaration string. Use inside cssx groups
|
||||
;; or standalone on :style with str.
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Text alignment keywords
|
||||
(define cssx-alignments
|
||||
{"left" true "center" true "right" true "justify" true})
|
||||
|
||||
;; -- 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 ";")))
|
||||
;; 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"})
|
||||
|
||||
;; Named max-widths (Tailwind scale)
|
||||
(define cssx-max-widths
|
||||
@@ -216,4 +151,368 @@
|
||||
"lg" "32rem" "xl" "36rem" "2xl" "42rem"
|
||||
"3xl" "48rem" "4xl" "56rem" "5xl" "64rem"
|
||||
"6xl" "72rem" "7xl" "80rem"
|
||||
"full" "100%" "none" "none"})
|
||||
"full" "100%" "none" "none"
|
||||
"prose" "65ch" "screen" "100vw"})
|
||||
|
||||
;; Responsive breakpoints (mobile-first min-width)
|
||||
(define cssx-breakpoints
|
||||
{"sm" "640px"
|
||||
"md" "768px"
|
||||
"lg" "1024px"
|
||||
"xl" "1280px"
|
||||
"2xl" "1536px"})
|
||||
|
||||
;; 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)
|
||||
(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"))))))
|
||||
|
||||
;; 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) (length 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) (length 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)))
|
||||
(cond
|
||||
;; ---------------------------------------------------------
|
||||
;; Colour utilities: bg-{colour}-{shade}, text-{colour}-{shade}, border-{colour}-{shade}
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-colour-props head)
|
||||
(>= (length rest) 2)
|
||||
(not (nil? (parse-int (last rest) nil)))
|
||||
(not (nil? (get colour-bases (join "-" (slice rest 0 (- (length rest) 1)))))))
|
||||
(let ((css-prop (get cssx-colour-props head))
|
||||
(cname (join "-" (slice rest 0 (- (length 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")
|
||||
(= (length 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")
|
||||
(= (length rest) 1)
|
||||
(get cssx-alignments (first rest)))
|
||||
(str "text-align:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Font weight: font-bold, font-semibold, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (length 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")
|
||||
(= (length 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)
|
||||
(= (length 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 (= (length parts) 1)
|
||||
(not (nil? (get cssx-displays head))))
|
||||
(str "display:" (get cssx-displays head))
|
||||
|
||||
;; Inline-block, inline-flex (multi-word)
|
||||
(and (= (length 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")
|
||||
(>= (length 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")
|
||||
(= (length 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"))
|
||||
(= (length 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")
|
||||
(= (length rest) 1))
|
||||
(let ((v (cssx-spacing-value (first rest))))
|
||||
(if (nil? v) nil (str "gap:" v)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text decoration: underline, no-underline, line-through
|
||||
;; ---------------------------------------------------------
|
||||
(and (= (length parts) 1)
|
||||
(or (= head "underline") (= head "overline") (= head "line-through")))
|
||||
(str "text-decoration-line:" head)
|
||||
|
||||
(and (= (length parts) 2) (= head "no") (= (first rest) "underline"))
|
||||
"text-decoration-line:none"
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Cursor: cursor-pointer, cursor-default, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "cursor") (= (length rest) 1))
|
||||
(str "cursor:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Overflow: overflow-hidden, overflow-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "overflow") (= (length 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"
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; 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 (length 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
|
||||
(cond
|
||||
(= n 1)
|
||||
(do (set! base (first colon-parts)))
|
||||
(= n 2)
|
||||
(let ((prefix (first colon-parts)))
|
||||
(set! base (last colon-parts))
|
||||
(if (not (nil? (get cssx-breakpoints prefix)))
|
||||
(set! bp prefix)
|
||||
(set! state prefix)))
|
||||
(>= n 3)
|
||||
(do
|
||||
(set! bp (first colon-parts))
|
||||
(set! state (nth colon-parts 1))
|
||||
(set! base (last colon-parts))))
|
||||
|
||||
;; 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 ";"))))))
|
||||
tokens)
|
||||
(join "" parts))))
|
||||
|
||||
;; =========================================================================
|
||||
;; ~cssx/tw — macro that injects JIT classes onto the first child element
|
||||
;;
|
||||
;; Usage:
|
||||
;; (~cssx/tw "bg-yellow-199"
|
||||
;; (p "sunny"))
|
||||
;;
|
||||
;; (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold rounded-lg"
|
||||
;; (div (h1 "styled content")))
|
||||
;;
|
||||
;; (~cssx/tw "hover:bg-rose-500 focus:border-blue-400"
|
||||
;; (button "interactive"))
|
||||
;;
|
||||
;; (~cssx/tw "md:text-xl lg:p-8"
|
||||
;; (section "responsive"))
|
||||
;;
|
||||
;; Parses tokens at macro-expansion time, injects :class onto the first
|
||||
;; child element (merging with any existing :class), and prepends a
|
||||
;; <style> tag with the JIT CSS rules. No wrapper element.
|
||||
;; =========================================================================
|
||||
|
||||
;; Merge :class into an element's arg list.
|
||||
;; If element already has :class, prepend our classes to its value.
|
||||
;; If not, inject :class after the tag name.
|
||||
(define cssx-inject-class
|
||||
(fn (element cls-str)
|
||||
(let ((tag (first element))
|
||||
(args (slice element 1)))
|
||||
(cons tag (cssx-merge-class-args args cls-str false)))))
|
||||
|
||||
;; Walk arg list: find :class keyword, merge value. If not found, inject at end.
|
||||
(define cssx-merge-class-args
|
||||
(fn (args cls-str found)
|
||||
(if (empty? args)
|
||||
;; End of args — if no :class was found, inject one
|
||||
(if found (list) (list :class cls-str))
|
||||
(let ((head (first args))
|
||||
(tail (slice args 1)))
|
||||
(if (and (not found)
|
||||
(= (type-of head) "keyword")
|
||||
(= (keyword-name head) "class"))
|
||||
;; Found :class — merge with next arg (the value)
|
||||
(if (empty? tail)
|
||||
;; :class with no value — replace with ours
|
||||
(append (list :class cls-str) (list))
|
||||
;; :class with value — prepend our classes
|
||||
(append (list :class (str cls-str " " (first tail)))
|
||||
(cssx-merge-class-args (slice tail 1) cls-str true)))
|
||||
;; Not :class — keep and continue
|
||||
(cons head (cssx-merge-class-args tail cls-str found)))))))
|
||||
|
||||
(defmacro ~cssx/tw (tokens &rest children)
|
||||
(let ((token-list (filter (fn (t) (not (= t ""))) (split tokens " ")))
|
||||
(classes (list))
|
||||
(rules (list)))
|
||||
;; Process each token
|
||||
(for-each (fn (tok)
|
||||
(let ((r (cssx-process-token tok)))
|
||||
(when (not (nil? r))
|
||||
(append! classes (get r "cls"))
|
||||
(append! rules (get r "rule")))))
|
||||
token-list)
|
||||
(let ((cls-str (join " " classes))
|
||||
(rules-str (join "" rules))
|
||||
(first-child (first children))
|
||||
(rest-children (slice children 1)))
|
||||
(if (empty? classes)
|
||||
;; No resolved tokens — pass through unchanged
|
||||
(if (= (length children) 1) first-child `(<> ,@children))
|
||||
;; Inject class onto first child element
|
||||
(if (and (list? first-child) (not (empty? first-child)))
|
||||
;; First child is an element — inject :class, prepend <style>
|
||||
(let ((injected (cssx-inject-class first-child cls-str)))
|
||||
(if (empty? rest-children)
|
||||
`(<> (style ,rules-str) ,injected)
|
||||
`(<> (style ,rules-str) ,injected ,@rest-children)))
|
||||
;; First child isn't an element — wrap everything in div
|
||||
`(<> (style ,rules-str) (div :class ,cls-str ,@children)))))))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Nav components — logo header, sibling arrows, children links
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; CSSX replaces Tailwind text-*/bg-*/font-* classes — computed via cssx.sx
|
||||
;; Styling via cssx-style utility tokens (cssx.sx) — same format as ~cssx/tw
|
||||
|
||||
;; Logo + tagline + copyright — always shown at top of page area.
|
||||
;; The header itself is an island so the "reactive" word can cycle colours
|
||||
@@ -21,23 +21,21 @@
|
||||
(shade (signal 500))
|
||||
(current-family (computed (fn ()
|
||||
(nth families (mod (deref idx) (len families)))))))
|
||||
(div :style (str (display "block") (max-w (get cssx-max-widths "3xl"))
|
||||
(mx-auto) (px 4) (pt 8) (pb 4) (align "center"))
|
||||
(div :style (tw "block max-w-3xl mx-auto px-4 pt-8 pb-4 text-center")
|
||||
;; Logo — only this navigates home
|
||||
(a :href "/sx/"
|
||||
:sx-get "/sx/" :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:style (str (display "block") (decoration "none"))
|
||||
:style (tw "block no-underline")
|
||||
(lake :id "logo"
|
||||
(span :style (str (display "block") (mb 2)
|
||||
(cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono"))))
|
||||
(span :style (tw "block mb-2 text-violet-699 text-4xl font-bold font-mono")
|
||||
"(<sx>)")))
|
||||
;; Tagline — clicking "reactive" cycles colour.
|
||||
(p :style (str (mb 1) (cssx (:text (colour "stone" 500) (size "lg"))))
|
||||
(p :style (tw "mb-1 text-stone-500 text-lg")
|
||||
"The framework-free "
|
||||
(span
|
||||
:style (str (cssx (:text (colour (deref current-family) (deref shade))
|
||||
(weight "bold")))
|
||||
:style (str "color:" (colour (deref current-family) (deref shade)) ";"
|
||||
(tw "font-bold")
|
||||
"cursor:pointer;transition:color 0.3s,font-weight 0.3s;")
|
||||
:on-click (fn (e)
|
||||
(batch (fn ()
|
||||
@@ -47,11 +45,10 @@
|
||||
" hypermedium")
|
||||
;; Lake: server morphs copyright on navigation without disturbing signals.
|
||||
(lake :id "copyright"
|
||||
(p :style (cssx (:text (colour "stone" 400) (size "xs")))
|
||||
(p :style (tw "text-stone-400 text-xs")
|
||||
"© Giles Bradshaw 2026"
|
||||
(when path
|
||||
(span :style (str (cssx (:text (colour "stone" 300) (size "xs")))
|
||||
"margin-left:0.5em;")
|
||||
(span :style (str (tw "text-stone-300 text-xs") "margin-left:0.5em;")
|
||||
(str "· " path))))))))
|
||||
|
||||
|
||||
@@ -80,7 +77,7 @@
|
||||
:sx-select "#main-panel" :sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
:class "text-right"
|
||||
:style (cssx (:text (colour "stone" 500) (size "sm")))
|
||||
:style (tw "text-stone-500 text-sm")
|
||||
(str "← " (get prev-node "label")))
|
||||
(a :href (get node "href")
|
||||
:sx-get (get node "href") :sx-target "#main-panel"
|
||||
@@ -88,15 +85,15 @@
|
||||
:sx-push-url "true"
|
||||
:class "text-center px-4"
|
||||
:style (if is-leaf
|
||||
(cssx (:text (colour "violet" 700) (size "2xl") (weight "bold")))
|
||||
(cssx (:text (colour "violet" 700) (size "lg") (weight "semibold"))))
|
||||
(tw "text-violet-700 text-2xl font-bold")
|
||||
(tw "text-violet-700 text-lg font-semibold"))
|
||||
(get node "label"))
|
||||
(a :href (get next-node "href")
|
||||
:sx-get (get next-node "href") :sx-target "#main-panel"
|
||||
:sx-select "#main-panel" :sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
:class "text-left"
|
||||
:style (cssx (:text (colour "stone" 500) (size "sm")))
|
||||
:style (tw "text-stone-500 text-sm")
|
||||
(str (get next-node "label") " →")))))))
|
||||
|
||||
;; Children links — shown as clearly clickable buttons.
|
||||
@@ -109,8 +106,7 @@
|
||||
:sx-select "#main-panel" :sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
:class "px-3 py-1.5 rounded border transition-colors"
|
||||
:style (cssx (:text (colour "violet" 700) (size "sm"))
|
||||
(:border (colour "violet" 200)))
|
||||
:style (tw "text-violet-700 text-sm border-violet-200")
|
||||
(get item "label")))
|
||||
items))))
|
||||
|
||||
|
||||
@@ -2,19 +2,18 @@
|
||||
|
||||
(defcomp ~not-found/content (&key (path :as string?))
|
||||
(div :class "max-w-3xl mx-auto px-4 py-12 text-center"
|
||||
(h1 :style (cssx (:text (colour "stone" 800) (size "3xl") (weight "bold")))
|
||||
(h1 :style (tw "text-stone-800 text-3xl font-bold")
|
||||
"404")
|
||||
(p :class "mt-4"
|
||||
:style (cssx (:text (colour "stone" 500) (size "lg")))
|
||||
:style (tw "text-stone-500 text-lg")
|
||||
"Page not found")
|
||||
(when path
|
||||
(p :class "mt-2"
|
||||
:style (cssx (:text (colour "stone" 400) (size "sm") (family "mono")))
|
||||
:style (tw "text-stone-400 text-sm font-mono")
|
||||
path))
|
||||
(a :href "/sx/"
|
||||
:sx-get "/sx/" :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class "inline-block mt-6 px-4 py-2 rounded border transition-colors"
|
||||
:style (cssx (:text (colour "violet" 700) (size "sm"))
|
||||
(:border (colour "violet" 200)))
|
||||
:style (tw "text-violet-700 text-sm border-violet-200")
|
||||
"Back to home")))
|
||||
|
||||
Reference in New Issue
Block a user