Files
rose-ash/shared/sx/templates/cssx.sx
giles d5e416e478 Reactive island preservation across server-driven morphs
Islands survive hypermedia swaps: morph-node skips hydrated
data-sx-island elements when the same island exists in new content.
dispose-islands-in skips hydrated islands to prevent premature cleanup.

- @client directive: .sx files marked ;; @client send define forms to browser
- CSSX client-side: cssxgroup renamed (no hyphen) to avoid isRenderExpr
  matching it as a custom element — was producing [object HTMLElement]
- Island wrappers: div→span to avoid block-in-inline HTML parse breakage
- ~sx-header is now a defisland with inline reactive colour cycling
- bootstrap_js.py defaults output to shared/static/scripts/sx-browser.js
- Deleted stale sx-ref.js (sx-browser.js is the canonical browser build)
- Hegelian Synthesis essay: dialectic of hypertext and reactivity
- component-source helper handles Island types for docs pretty-printing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 14:10:35 +00:00

220 lines
8.4 KiB
Plaintext

;; @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"})