HS parser: fix number+comparison keyword collision, eval-hs uses hs-compile
Parser: skip unit suffix when next ident is a comparison keyword (starts, ends, contains, matches, is, does, in, precedes, follows). Fixes "123 starts with '12'" returning "123starts" instead of true. eval-hs: use hs-compile directly instead of hs-to-sx-from-source with "return " prefix, which was causing the parser to consume the comparison as a string suffix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -693,7 +693,12 @@
|
||||
nil
|
||||
(do
|
||||
(when
|
||||
(and (number? left) (= (tp-type) "ident"))
|
||||
(and (number? left) (= (tp-type) "ident")
|
||||
(not (or (= (tp-val) "starts") (= (tp-val) "ends")
|
||||
(= (tp-val) "contains") (= (tp-val) "matches")
|
||||
(= (tp-val) "is") (= (tp-val) "does")
|
||||
(= (tp-val) "in") (= (tp-val) "precedes")
|
||||
(= (tp-val) "follows"))))
|
||||
(let
|
||||
((unit (tp-val)))
|
||||
(do
|
||||
|
||||
@@ -693,7 +693,12 @@
|
||||
nil
|
||||
(do
|
||||
(when
|
||||
(and (number? left) (= (tp-type) "ident"))
|
||||
(and (number? left) (= (tp-type) "ident")
|
||||
(not (or (= (tp-val) "starts") (= (tp-val) "ends")
|
||||
(= (tp-val) "contains") (= (tp-val) "matches")
|
||||
(= (tp-val) "is") (= (tp-val) "does")
|
||||
(= (tp-val) "in") (= (tp-val) "precedes")
|
||||
(= (tp-val) "follows"))))
|
||||
(let
|
||||
((unit (tp-val)))
|
||||
(do
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
;; Compiles the expression, wraps in a thunk, evaluates, returns result.
|
||||
(define eval-hs
|
||||
(fn (src)
|
||||
(let ((sx (hs-to-sx-from-source (str "return " src))))
|
||||
(let ((sx (hs-to-sx (hs-compile src))))
|
||||
(let ((handler (eval-expr-cek
|
||||
(list (quote fn) (list (quote me)) (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))
|
||||
(handler nil)))))
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
;; Bundle analyzer — live demonstration of dependency analysis + IO detection.
|
||||
;; Shows per-page component bundles vs total, visualizing payload savings.
|
||||
;; Drill down into each bundle to see component tree; expand to see SX source.
|
||||
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500
|
||||
|
||||
(defcomp ~analyzer/bundle-analyzer-content (&key (pages :as list) (total-components :as number) (total-macros :as number)
|
||||
(pure-count :as number) (io-count :as number))
|
||||
(~docs/page :title "Page Bundle Analyzer"
|
||||
|
||||
(p (~tw :tokens "text-stone-600 mb-6")
|
||||
"Live analysis of component dependency graphs and IO classification across all pages. "
|
||||
"Each bar shows how many of the "
|
||||
(strong (str total-components))
|
||||
" total components a page actually needs, computed by the "
|
||||
(a :href "/sx/(language.(spec.deps))" (~tw :tokens "text-violet-700 underline") "deps.sx")
|
||||
" transitive closure algorithm. "
|
||||
"Click a page to see its component tree; expand a component to see its SX source.")
|
||||
|
||||
(div (~tw :tokens "mb-8 grid grid-cols-4 gap-4")
|
||||
(~analyzer/stat :label "Total Components" :value (str total-components)
|
||||
:cls "text-violet-600")
|
||||
(~analyzer/stat :label "Total Macros" :value (str total-macros)
|
||||
:cls "text-stone-600")
|
||||
(~analyzer/stat :label "Pure Components" :value (str pure-count)
|
||||
:cls "text-blue-600")
|
||||
(~analyzer/stat :label "IO-Dependent" :value (str io-count)
|
||||
:cls "text-amber-600"))
|
||||
|
||||
(~docs/section :title "Per-Page Bundles" :id "bundles"
|
||||
(div (~tw :tokens "space-y-3")
|
||||
(map (fn (page)
|
||||
(~analyzer/row
|
||||
:name (get page "name")
|
||||
:path (get page "path")
|
||||
:needed (get page "needed")
|
||||
:direct (get page "direct")
|
||||
:total total-components
|
||||
:pct (get page "pct")
|
||||
:savings (get page "savings")
|
||||
:io-refs (get page "io-refs")
|
||||
:pure-in-page (get page "pure-in-page")
|
||||
:io-in-page (get page "io-in-page")
|
||||
:components (get page "components")))
|
||||
pages)))
|
||||
|
||||
(~docs/section :title "How It Works" :id "how"
|
||||
(ol (~tw :tokens "list-decimal pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Scan: ") "Regex finds all " (code "(~plans/content-addressed-components/name") " patterns in the page's content expression.")
|
||||
(li (strong "Resolve: ") "Each referenced component's body AST is walked to find transitive " (code "~") " references.")
|
||||
(li (strong "Closure: ") "The full set is the union of direct + transitive deps, following chains through the component graph.")
|
||||
(li (strong "Bundle: ") "Only these component definitions are serialized into the page payload. Everything else is omitted.")
|
||||
(li (strong "IO detect: ") "Each component body is scanned for references to IO primitives (frag, query, service, etc.). Components with zero transitive IO refs are pure — safe for client rendering."))
|
||||
(p (~tw :tokens "mt-4 text-stone-600")
|
||||
"The analysis handles circular references (via seen-set), "
|
||||
"walks all branches of control flow (if/when/cond/case), "
|
||||
"and includes macro definitions shared across components."))))
|
||||
|
||||
(defcomp ~analyzer/stat (&key (label :as string) (value :as string) (cls :as string))
|
||||
(div (~tw :tokens "rounded-lg border border-stone-200 p-4 text-center")
|
||||
(div :class (str "text-3xl font-bold " cls) value)
|
||||
(div (~tw :tokens "text-sm text-stone-500 mt-1") label)))
|
||||
|
||||
(defcomp ~analyzer/row (&key (name :as string) (path :as string) (needed :as number) (direct :as number) (total :as number) (pct :as number) (savings :as number)
|
||||
(io-refs :as list) (pure-in-page :as number) (io-in-page :as number) (components :as list))
|
||||
(details (~tw :tokens "rounded border border-stone-200")
|
||||
(summary (~tw :tokens "p-4 cursor-pointer hover:bg-stone-50 transition-colors")
|
||||
(div (~tw :tokens "flex items-center justify-between mb-2")
|
||||
(div
|
||||
(span (~tw :tokens "font-mono font-semibold text-stone-800") name)
|
||||
(span (~tw :tokens "text-stone-400 text-sm ml-2") path))
|
||||
(div (~tw :tokens "flex items-center gap-2")
|
||||
(span (~tw :tokens "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800")
|
||||
(str pure-in-page " pure"))
|
||||
(span (~tw :tokens "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800")
|
||||
(str io-in-page " IO"))
|
||||
(div (~tw :tokens "text-right")
|
||||
(span (~tw :tokens "font-mono text-sm")
|
||||
(span (~tw :tokens "text-violet-700 font-bold") (str needed))
|
||||
(span (~tw :tokens "text-stone-400") (str " / " total)))
|
||||
(span (~tw :tokens "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800")
|
||||
(str savings "% saved")))))
|
||||
(div (~tw :tokens "w-full bg-stone-200 rounded-full h-2.5")
|
||||
(div (~tw :tokens "bg-violet-600 h-2.5 rounded-full transition-all")
|
||||
:style (str "width: " pct "%"))))
|
||||
|
||||
;; Component tree (shown when expanded)
|
||||
(div (~tw :tokens "border-t border-stone-200 p-4 bg-stone-50")
|
||||
(div (~tw :tokens "text-xs font-medium text-stone-500 uppercase tracking-wide mb-3")
|
||||
(str needed " components in bundle"))
|
||||
(div (~tw :tokens "space-y-1")
|
||||
(map (fn (comp)
|
||||
(~analyzer/component
|
||||
:comp-name (get comp "name")
|
||||
:is-pure (get comp "is-pure")
|
||||
:io-refs (get comp "io-refs")
|
||||
:deps (get comp "deps")
|
||||
:source (get comp "source")))
|
||||
components)))))
|
||||
|
||||
(defcomp ~analyzer/component (&key (comp-name :as string) (is-pure :as boolean) (io-refs :as list) (deps :as list) (source :as string))
|
||||
(details :class (str "rounded border "
|
||||
(if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50"))
|
||||
(summary (~tw :tokens "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity")
|
||||
(div (~tw :tokens "flex items-center justify-between")
|
||||
(div (~tw :tokens "flex items-center gap-2")
|
||||
(span :class (str "inline-block w-2 h-2 rounded-full "
|
||||
(if is-pure "bg-blue-500" "bg-amber-500")))
|
||||
(span (~tw :tokens "font-mono text-sm font-medium text-stone-800") comp-name))
|
||||
(div (~tw :tokens "flex items-center gap-2")
|
||||
(when (not (empty? io-refs))
|
||||
(span (~tw :tokens "text-xs text-amber-700")
|
||||
(str "IO: " (join ", " io-refs))))
|
||||
(when (not (empty? deps))
|
||||
(span (~tw :tokens "text-xs text-stone-500")
|
||||
(str (len deps) " deps"))))))
|
||||
|
||||
;; SX source (shown when component expanded)
|
||||
(div (~tw :tokens "not-prose border-t border-stone-200 p-3 bg-stone-100 rounded-b")
|
||||
(pre (~tw :tokens "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto")
|
||||
(code (highlight source "lisp"))))))
|
||||
@@ -3,4 +3,6 @@
|
||||
(dict-set!
|
||||
__app-config
|
||||
"handler-prefixes"
|
||||
(append (get __app-config "handler-prefixes") (list "handler:hs-")))
|
||||
(append
|
||||
(get __app-config "handler-prefixes")
|
||||
(list "handler:hs-" "handler:gql-")))
|
||||
|
||||
48
sx/sx/applications/cssx/async/index.sx
Normal file
48
sx/sx/applications/cssx/async/index.sx
Normal file
@@ -0,0 +1,48 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Async CSS
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "Async CSS"
|
||||
|
||||
(~docs/section :title "The Pattern" :id "pattern"
|
||||
(p "A CSSX component that needs CSS it doesn't have yet can "
|
||||
(strong "fetch and cache it before rendering") ". This is just "
|
||||
(code "~shared:pages/suspense") " combined with a style component — no new infrastructure:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~shared:pages/suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))"
|
||||
"lisp")
|
||||
(p "The consumer never knows:")
|
||||
(highlight
|
||||
"(~cssx/styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "Use Cases" :id "use-cases"
|
||||
(~docs/subsection :title "Federated Components"
|
||||
(p "A " (code "~cssx/btn") " from another site arrives via IPFS with a CID pointing "
|
||||
"to its CSS. The component fetches and caches it before rendering. "
|
||||
"No coordination needed between sites.")
|
||||
(highlight
|
||||
"(defcomp ~cssx/federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~shared:pages/suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/subsection :title "Heavy UI Libraries"
|
||||
(p "Code editors, chart libraries, rich text editors — their CSS only loads "
|
||||
"when the component actually appears on screen:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/code-editor (&key language value on-change)\n (~cssx/styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/subsection :title "Lazy Themes"
|
||||
(p "Theme CSS loads on first use, then is instant on subsequent visits:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~cssx/styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/section :title "How It Composes" :id "composition"
|
||||
(p "Async CSS composes with everything already in SX:")
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700")
|
||||
(li (code "~shared:pages/suspense") " handles the async gap with fallback content")
|
||||
(li "localStorage handles caching across sessions")
|
||||
(li (code "<style id=\"sx-css\">") " is the injection target (same as CSS class delivery)")
|
||||
(li "Component content-hashing tracks what the client has")
|
||||
(li "No new types, no new delivery protocol, no new spec code")))))
|
||||
60
sx/sx/applications/cssx/comparison/index.sx
Normal file
60
sx/sx/applications/cssx/comparison/index.sx
Normal file
@@ -0,0 +1,60 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Comparison with CSS Technologies
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "Comparisons"
|
||||
|
||||
(~docs/section :title "styled-components / Emotion" :id "styled-components"
|
||||
(p (a :href "https://styled-components.com" (~tw :tokens "text-violet-600 hover:underline") "styled-components")
|
||||
" pioneered the idea that styling belongs in components. But it generates CSS "
|
||||
"at runtime, injects " (code "<style>") " tags, and produces opaque hashed class "
|
||||
"names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. "
|
||||
"It also carries significant runtime cost — parsing CSS template literals, hashing, "
|
||||
"deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
||||
(p "CSSX components share the core insight (" (em "styling is a component concern")
|
||||
") but without the runtime machinery. When a component applies Tailwind classes, "
|
||||
"there's zero CSS generation overhead. When it does emit " (code "<style>")
|
||||
" blocks, it's explicit — not hidden behind a tagged template literal. "
|
||||
"And the DOM is always readable."))
|
||||
|
||||
(~docs/section :title "CSS Modules" :id "css-modules"
|
||||
(p (a :href "https://github.com/css-modules/css-modules" (~tw :tokens "text-violet-600 hover:underline") "CSS Modules")
|
||||
" scope class names to avoid collisions by rewriting them at build time: "
|
||||
(code ".button") " becomes " (code ".button_abc123")
|
||||
". This solves the global namespace problem but creates the same opacity issue — "
|
||||
"hashed names in the DOM that you can't grep for or reason about.")
|
||||
(p "CSSX components don't need scoping because component boundaries already provide "
|
||||
"isolation. A " (code "~cssx/btn") " owns its markup. There's nothing to collide with."))
|
||||
|
||||
(~docs/section :title "Tailwind CSS" :id "tailwind"
|
||||
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the "
|
||||
"semantic layer on top. Raw Tailwind in markup — "
|
||||
(code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"")
|
||||
" — is powerful but verbose and duplicated across call sites.")
|
||||
(p "A CSSX component wraps that string once: " (code "(~cssx/btn :variant \"primary\" \"Submit\")")
|
||||
". The Tailwind classes are still there, readable in DevTools, but consumers don't "
|
||||
"repeat them. This is the same pattern Tailwind's own docs recommend ("
|
||||
(em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
||||
|
||||
(~docs/section :title "Vanilla Extract" :id "vanilla-extract"
|
||||
(p (a :href "https://vanilla-extract.style" (~tw :tokens "text-violet-600 hover:underline") "Vanilla Extract")
|
||||
" is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static "
|
||||
"CSS at build time, and referenced by generated class names. It avoids the runtime "
|
||||
"cost of styled-components but still requires a build step, a bundler plugin, and "
|
||||
"TypeScript. The generated class names are again opaque.")
|
||||
(p "CSSX components need no build step for styling — they're evaluated at render time "
|
||||
"like any other component. And since the component chooses its own strategy, it can "
|
||||
"reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — "
|
||||
"same API either way."))
|
||||
|
||||
(~docs/section :title "Design Tokens / Style Dictionary" :id "design-tokens"
|
||||
(p "The " (a :href "https://amzn.github.io/style-dictionary/" (~tw :tokens "text-violet-600 hover:underline") "Style Dictionary")
|
||||
" pattern — a JSON/YAML file mapping token names to values, compiled to "
|
||||
"platform-specific output — is essentially what the old CSSX was. It's the "
|
||||
"industry standard for design systems.")
|
||||
(p "The problem is that it's a parallel system: separate file format, separate build "
|
||||
"pipeline, separate caching, separate tooling. CSSX components eliminate all of "
|
||||
"that by expressing tokens as component parameters: "
|
||||
(code "(~cssx/theme :primary \"#7c3aed\")") " instead of "
|
||||
(code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}")
|
||||
". Same result, no parallel infrastructure."))))
|
||||
64
sx/sx/applications/cssx/delivery/index.sx
Normal file
64
sx/sx/applications/cssx/delivery/index.sx
Normal file
@@ -0,0 +1,64 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSS Delivery
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "CSS Delivery"
|
||||
|
||||
(~docs/section :title "Multiple Strategies" :id "strategies"
|
||||
(p "A CSSX component chooses its own styling strategy — and each strategy has its "
|
||||
"own delivery path:")
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700")
|
||||
(li (strong "Tailwind classes: ") "Delivered via the on-demand protocol below. "
|
||||
"The server ships only the rules the page actually uses.")
|
||||
(li (strong "Inline styles: ") "No delivery needed — " (code ":style") " attributes "
|
||||
"are part of the markup.")
|
||||
(li (strong "Emitted " (code "<style>") " blocks: ") "Components can emit CSS rules "
|
||||
"directly. They arrive as part of the rendered HTML — keyframes, custom properties, "
|
||||
"scoped rules, anything.")
|
||||
(li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files "
|
||||
"or lazy-load them via " (code "~shared:pages/suspense") " (see " (a :href "/sx/(applications.(cssx.async))" "Async CSS") ").")
|
||||
(li (strong "Custom properties: ") "A " (code "~cssx/theme") " component sets "
|
||||
(code "--color-primary") " etc. via a " (code "<style>") " block. Everything "
|
||||
"using " (code "var()") " repaints automatically."))
|
||||
(p "The protocol below handles the Tailwind utility class case — the most common "
|
||||
"strategy — but it's not the only game in town."))
|
||||
|
||||
(~docs/section :title "On-Demand Tailwind Delivery" :id "on-demand"
|
||||
(p "When components use Tailwind utility classes, SX ships " (em "only the CSS rules "
|
||||
"actually used on the page") ", computed at render time. No build step, no purging, "
|
||||
"no unused CSS.")
|
||||
(p "The server pre-parses the full Tailwind CSS file into an in-memory registry at "
|
||||
"startup. When a response is rendered, SX scans all " (code ":class") " values in "
|
||||
"the output, looks up only those classes, and embeds the matching rules."))
|
||||
|
||||
(~docs/section :title "The Protocol" :id "protocol"
|
||||
(p "First page load gets the full set of used rules. Subsequent navigations send a "
|
||||
"hash of what the client already has, and the server ships only the delta:")
|
||||
(highlight
|
||||
"# First page load:\nGET / HTTP/1.1\n\nHTTP/1.1 200 OK\nContent-Type: text/html\n# Full CSS in <style id=\"sx-css\"> + hash in <meta name=\"sx-css-classes\">\n\n# Subsequent navigation:\nGET /about HTTP/1.1\nSX-Css: a1b2c3d4\n\nHTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: e5f6g7h8\nSX-Css-Add: bg-blue-500,text-white,rounded-lg\n# Only new rules in <style data-sx-css>"
|
||||
"bash")
|
||||
(p "The client merges new rules into the existing " (code "<style id=\"sx-css\">")
|
||||
" block and updates its hash. No flicker, no duplicate rules."))
|
||||
|
||||
(~docs/section :title "Component-Aware Scanning" :id "scanning"
|
||||
(p "CSS scanning happens at two levels:")
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700")
|
||||
(li (strong "Registration time: ") "When a component is defined via " (code "defcomp")
|
||||
", its body is scanned for class strings. The component records which CSS classes "
|
||||
"it uses.")
|
||||
(li (strong "Render time: ") "The rendered output is scanned for any classes the "
|
||||
"static scan missed — dynamically constructed class strings, conditional classes, etc."))
|
||||
(p "This means the CSS registry knows roughly what a page needs before rendering, "
|
||||
"and catches any stragglers after."))
|
||||
|
||||
(~docs/section :title "Trade-offs" :id "tradeoffs"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Full Tailwind in memory: ") "The parsed CSS registry is ~4MB. This is "
|
||||
"a one-time startup cost per app instance.")
|
||||
(li (strong "Regex scanning: ") "Class detection uses regex, so dynamically "
|
||||
"constructed class names (e.g. " (code "(str \"bg-\" color \"-500\")") ") can be "
|
||||
"missed. Use complete class strings in " (code "case") "/" (code "cond") " branches.")
|
||||
(li (strong "No @apply: ") "Tailwind's " (code "@apply") " is a build-time feature. "
|
||||
"On-demand delivery works with utility classes directly.")
|
||||
(li (strong "Tailwind-shaped: ") "The registry parser understands Tailwind's naming "
|
||||
"conventions. Non-Tailwind CSS works via " (code "<style>") " blocks in components.")))))
|
||||
70
sx/sx/applications/cssx/index.sx
Normal file
70
sx/sx/applications/cssx/index.sx
Normal file
@@ -0,0 +1,70 @@
|
||||
;; CSSX — Styling as Components
|
||||
;; Documentation for the CSSX approach: no parallel style infrastructure,
|
||||
;; just defcomp components that decide how to style their children.
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Overview
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "CSSX Components"
|
||||
|
||||
(~docs/section :title "The Idea" :id "idea"
|
||||
(p (strong "Styling is just components.") " A CSSX component is a regular "
|
||||
(code "defcomp") " that decides how to style its children. It might apply "
|
||||
"Tailwind classes, or hand-written CSS classes, or inline styles, or generate "
|
||||
"rules at runtime. The implementation is the component's private business. "
|
||||
"The consumer just calls " (code "(~cssx/btn :variant \"primary\" \"Submit\")") " and doesn't care.")
|
||||
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, "
|
||||
"dependency scanning, server/client rendering, composition. No parallel infrastructure."))
|
||||
|
||||
(~docs/section :title "Why Not a Style Dictionary?" :id "why"
|
||||
(p "SX previously had a parallel CSS system: a style dictionary (JSON blob of "
|
||||
"atom-to-declaration mappings), a " (code "StyleValue") " type threaded through "
|
||||
"the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1")
|
||||
"), runtime CSS injection, and a separate caching pipeline (cookies, localStorage).")
|
||||
(p "This was ~3,000 lines of code across the spec, bootstrappers, and host implementations. "
|
||||
"It was never adopted. The codebase voted with its feet: " (code ":class") " strings "
|
||||
"with " (code "defcomp") " already covered every real use case.")
|
||||
(p "The result of that system: elements in the DOM got opaque class names like "
|
||||
(code "class=\"sx-a3f2b1\"") ". DevTools became useless. You couldn't inspect an "
|
||||
"element and understand its styling. " (strong "That was a deal breaker.")))
|
||||
|
||||
(~docs/section :title "Key Advantages" :id "advantages"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed "
|
||||
"hashes. DevTools works.")
|
||||
(li (strong "Data-driven styling: ") "Components receive data and decide styling. "
|
||||
(code "(~cssx/metric :value 150)") " renders red because " (code "value > 100")
|
||||
" — logic lives in the component, not a CSS preprocessor.")
|
||||
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style "
|
||||
"dictionary JSON, no injection pipeline. Components ARE the styling abstraction.")
|
||||
(li (strong "One cache: ") "Component hash/localStorage handles everything. No "
|
||||
"separate style dict caching.")
|
||||
(li (strong "Composable: ") (code "(~card :elevated true (~cssx/metric :value v))")
|
||||
" — styling composes like any other component.")
|
||||
(li (strong "Strategy-agnostic: ") "A component can apply Tailwind classes, emit "
|
||||
(code "<style>") " blocks, use inline styles, generate CSS custom properties, or "
|
||||
"any combination. Swap strategies without touching call sites.")))
|
||||
|
||||
(~docs/section :title "What Changed" :id "changes"
|
||||
(~docs/subsection :title "Removed (~3,000 lines)"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-600 text-sm")
|
||||
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
|
||||
(li (code "cssx.sx") " spec module (resolve-style, resolve-atom, split-variant, hash, injection)")
|
||||
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", localStorage)")
|
||||
(li (code "style_dict.py") " (782 lines) and " (code "style_resolver.py") " (254 lines)")
|
||||
(li (code "css") " and " (code "merge-styles") " primitives")
|
||||
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "make-style-value") ", " (code "inject-style-value"))
|
||||
(li (code "defkeyframes") " special form")
|
||||
(li "Style dict cookies and localStorage keys")))
|
||||
|
||||
(~docs/subsection :title "Kept"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-600 text-sm")
|
||||
(li (code "defstyle") " — simplified to bind any value (string, function, etc.)")
|
||||
(li (code "tw.css") " — the compiled Tailwind stylesheet, delivered via CSS class tracking")
|
||||
(li (code ":class") " attribute — just takes strings, no special-casing")
|
||||
(li "CSS class delivery (" (code "SX-Css") " headers, " (code "<style id=\"sx-css\">") ")")
|
||||
(li "All component infrastructure (defcomp, caching, bundling, deps)")))
|
||||
|
||||
(~docs/subsection :title "Added"
|
||||
(p "Nothing. CSSX components are just " (code "defcomp") ". The only new thing is "
|
||||
"a convention: components whose primary purpose is styling.")))))
|
||||
63
sx/sx/applications/cssx/live/index.sx
Normal file
63
sx/sx/applications/cssx/live/index.sx
Normal file
@@ -0,0 +1,63 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live Styles
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "Live Styles"
|
||||
|
||||
(~docs/section :title "Styles That Respond to Events" :id "concept"
|
||||
(p "Combine " (code "~live") " (SSE) or " (code "~ws") " (WebSocket) with style "
|
||||
"components, and you get styles that change in real-time in response to server "
|
||||
"events. No new infrastructure — just components receiving data through a "
|
||||
"persistent transport."))
|
||||
|
||||
(~docs/section :title "SSE: Live Theme Updates" :id "sse-theme"
|
||||
(p "A " (code "~live") " component declares a persistent connection to an SSE "
|
||||
"endpoint. When the server pushes a new event, " (code "resolveSuspense")
|
||||
" replaces the content:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/brand\"\n (~shared:pages/suspense :id \"theme\"\n (~cssx/theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
|
||||
"lisp")
|
||||
(p "Server pushes a new theme:")
|
||||
(highlight
|
||||
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~cssx/theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
|
||||
"text")
|
||||
(p "The " (code "~cssx/theme") " component emits CSS custom properties. Everything "
|
||||
"using " (code "var(--color-primary)") " repaints instantly:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "SSE: Live Dashboard Metrics" :id "sse-metrics"
|
||||
(p "Style changes driven by live data — the component decides the visual treatment:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\"\n (~cssx/metric :value 0 :label \"CPU\" :threshold 80))\n (~shared:pages/suspense :id \"memory\"\n (~cssx/metric :value 0 :label \"Memory\" :threshold 90))\n (~shared:pages/suspense :id \"requests\"\n (~cssx/metric :value 0 :label \"RPS\" :threshold 1000)))"
|
||||
"lisp")
|
||||
(p "Server pushes updated values. " (code "~cssx/metric") " turns red when "
|
||||
(code "value > threshold") " — the styling logic lives in the component, "
|
||||
"not in CSS selectors or JavaScript event handlers."))
|
||||
|
||||
(~docs/section :title "WebSocket: Collaborative Design" :id "ws-design"
|
||||
(p "Bidirectional channel for real-time collaboration. A designer adjusts a color, "
|
||||
"all connected clients see the change:")
|
||||
(highlight
|
||||
"(~ws :src \"/ws/design-studio\"\n (~shared:pages/suspense :id \"canvas-theme\"\n (~cssx/theme :primary \"#7c3aed\")))"
|
||||
"lisp")
|
||||
(p "Client sends a color change:")
|
||||
(highlight
|
||||
";; Designer picks a new primary color\n(sx-send ws-conn '(theme-update :primary \"#dc2626\"))"
|
||||
"lisp")
|
||||
(p "Server broadcasts to all connected clients via " (code "sx-resolve") " — "
|
||||
"every client's " (code "~cssx/theme") " component re-renders with the new color."))
|
||||
|
||||
(~docs/section :title "Why This Works" :id "why"
|
||||
(p "Every one of these patterns is just a " (code "defcomp") " receiving data "
|
||||
"through a persistent transport. The styling strategy — CSS custom properties, "
|
||||
"class swaps, inline styles, " (code "<style>") " blocks — is the component's "
|
||||
"private business. The transport doesn't know or care.")
|
||||
(p "A parallel style system would have needed its own streaming, its own caching, "
|
||||
"its own delta protocol for each of these use cases — duplicating what components "
|
||||
"already do.")
|
||||
(p (~tw :tokens "mt-4 text-stone-500 italic")
|
||||
"Note: ~live and ~ws are planned (see Live Streaming). The patterns shown here "
|
||||
"will work as described once the streaming transport is implemented. The component "
|
||||
"and suspense infrastructure they depend on already exists."))))
|
||||
51
sx/sx/applications/cssx/patterns/index.sx
Normal file
51
sx/sx/applications/cssx/patterns/index.sx
Normal file
@@ -0,0 +1,51 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Patterns
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "Patterns"
|
||||
|
||||
(~docs/section :title "Class Mapping" :id "class-mapping"
|
||||
(p "The simplest pattern: a component that maps semantic keywords to class strings.")
|
||||
(highlight
|
||||
"(defcomp ~cssx/btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
|
||||
"lisp")
|
||||
(p "Consumers call " (code "(~cssx/btn :variant \"primary\" \"Submit\")") ". The Tailwind "
|
||||
"classes are readable in DevTools but never repeated across call sites."))
|
||||
|
||||
(~docs/section :title "Data-Driven Styling" :id "data-driven"
|
||||
(p "Styling that responds to data values — impossible with static CSS:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
|
||||
"lisp")
|
||||
(p "The component makes a " (em "decision") " about styling based on data. "
|
||||
"No CSS preprocessor or class name convention can express \"red when value > 100\"."))
|
||||
|
||||
(~docs/section :title "Style Functions" :id "style-functions"
|
||||
(p "Reusable style logic that returns class strings — no wrapping element needed:")
|
||||
(highlight
|
||||
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage:\n(div :class (card-classes :elevated true) ...)\n(article :class (card-classes :bordered true) ...)"
|
||||
"lisp")
|
||||
(p "Or with " (code "defstyle") " for named bindings:")
|
||||
(highlight
|
||||
"(defstyle card-base \"rounded-lg p-4 shadow-sm\")\n(defstyle card-elevated \"rounded-lg p-4 shadow-lg\")\n\n(div :class card-base ...)"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "Responsive Layouts" :id "responsive"
|
||||
(p "Components that encode responsive breakpoints:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "Emitting CSS Directly" :id "emitting-css"
|
||||
(p "Components are not limited to referencing existing classes. They can generate "
|
||||
"CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))"
|
||||
"lisp")
|
||||
(highlight
|
||||
"(defcomp ~cssx/theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
||||
"lisp")
|
||||
(p "The CSS strategy is the component's private implementation detail. Consumers call "
|
||||
(code "(~cssx/pulse :color \"red\" \"Loading...\")") " or "
|
||||
(code "(~cssx/theme :primary \"#2563eb\" ...)") " without knowing or caring whether the "
|
||||
"component uses classes, inline styles, generated rules, or all three."))))
|
||||
45
sx/sx/applications/cssx/philosophy/index.sx
Normal file
45
sx/sx/applications/cssx/philosophy/index.sx
Normal file
@@ -0,0 +1,45 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Philosophy
|
||||
;; ---------------------------------------------------------------------------
|
||||
(defcomp ()
|
||||
(~docs/page :title "Philosophy"
|
||||
|
||||
(~docs/section :title "The Collapse" :id "collapse"
|
||||
(p "The web has spent two decades building increasingly complex CSS tooling: "
|
||||
"preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style "
|
||||
"dictionaries. Each solves a real problem but adds a new system with its own "
|
||||
"caching, bundling, and mental model.")
|
||||
(p "CSSX components collapse all of this back to the simplest possible thing: "
|
||||
(strong "a function that takes data and returns markup with classes.")
|
||||
" That's what a component already is. There is no separate styling system because "
|
||||
"there doesn't need to be."))
|
||||
|
||||
(~docs/section :title "Proof by Deletion" :id "proof"
|
||||
(p "The strongest validation: we built the full parallel system — style dictionary, "
|
||||
"StyleValue type, content-addressed hashing, runtime injection, localStorage "
|
||||
"caching — and then deleted it because nobody used it. The codebase already had "
|
||||
"the answer: " (code "defcomp") " with " (code ":class") " strings.")
|
||||
(p "3,000 lines of infrastructure removed. Zero lines added. Every use case still works."))
|
||||
|
||||
(~docs/section :title "The Right Abstraction Level" :id "abstraction"
|
||||
(p "CSS-in-JS puts styling " (em "below") " components — you style elements, then compose "
|
||||
"them. Utility CSS puts styling " (em "beside") " components — classes in markup, logic "
|
||||
"elsewhere. Both create a seam between what something does and how it looks.")
|
||||
(p "CSSX components put styling " (em "inside") " components — at the same level as "
|
||||
"structure and behavior. A " (code "~cssx/metric") " component knows its own thresholds, "
|
||||
"its own color scheme, its own responsive behavior. Styling is just another "
|
||||
"decision the component makes, not a separate concern."))
|
||||
|
||||
(~docs/section :title "Relationship to Other Plans" :id "relationships"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any "
|
||||
"other component. A " (code "~cssx/btn") " from one site can be shared to another via "
|
||||
"IPFS. No " (code ":css-atoms") " manifest field needed — the component carries "
|
||||
"its own styling logic.")
|
||||
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. "
|
||||
"No style injection timing issues, no FOUC from late CSS loading.")
|
||||
(li (strong "Component Bundling: ") "deps.sx already handles transitive component deps. "
|
||||
"Style components are just more components in the bundle — no separate style bundling.")
|
||||
(li (strong "Live Streaming: ") "SSE and WebSocket transports push data to components. "
|
||||
"Style components react to that data like any other component — no separate style "
|
||||
"streaming protocol.")))))
|
||||
57
sx/sx/applications/graphql/_islands/parse-island.sx
Normal file
57
sx/sx/applications/graphql/_islands/parse-island.sx
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
;; Live GraphQL parse island — parses input and shows AST + serialized output
|
||||
(defisland
|
||||
()
|
||||
(let
|
||||
((input (signal "{\n user(id: 1) {\n name\n email\n }\n}"))
|
||||
(ast-output (signal ""))
|
||||
(ser-output (signal "")))
|
||||
(define
|
||||
do-parse!
|
||||
(fn
|
||||
(e)
|
||||
(let
|
||||
((src (deref input)))
|
||||
(when
|
||||
(not (empty? src))
|
||||
(let
|
||||
((doc (gql-parse src)))
|
||||
(do
|
||||
(set-signal! ast-output (sx-serialize doc))
|
||||
(set-signal! ser-output (gql-serialize doc))))))))
|
||||
(div
|
||||
(~tw :tokens "flex flex-col gap-3")
|
||||
(textarea
|
||||
:rows "5"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none")
|
||||
:placeholder "{ user(id: 1) { name email } }"
|
||||
:value (deref input)
|
||||
:on-input (fn (e) (set-signal! input (get-prop e "target.value"))))
|
||||
(button
|
||||
(~tw
|
||||
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
||||
:on-click do-parse!
|
||||
"Parse")
|
||||
(when
|
||||
(not (empty? (deref ast-output)))
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(div
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2")
|
||||
"AST")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto")
|
||||
(deref ast-output)))
|
||||
(div
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2")
|
||||
"Serialized")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-900 text-amber-400 p-4 rounded-lg text-sm overflow-x-auto")
|
||||
(deref ser-output))))))))
|
||||
274
sx/sx/applications/graphql/index.sx
Normal file
274
sx/sx/applications/graphql/index.sx
Normal file
@@ -0,0 +1,274 @@
|
||||
|
||||
;; GraphQL demo — parser, executor, and hyperscript integration
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"A pure SX implementation of the "
|
||||
(a
|
||||
:href "https://spec.graphql.org/"
|
||||
:target "_blank"
|
||||
(~tw :tokens "text-violet-600 underline")
|
||||
"GraphQL query language")
|
||||
". The parser, executor, and serializer are all s-expressions "
|
||||
"compiled to bytecode by the same kernel that runs the rest of the site.")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-6")
|
||||
"GraphQL operations map directly to the existing "
|
||||
(code "defquery")
|
||||
"/"
|
||||
(code "defaction")
|
||||
" system. Queries become IO suspension calls, mutations become actions, "
|
||||
"and field selection is projection over result dicts. The hyperscript "
|
||||
"integration adds "
|
||||
(code "fetch gql { ... }")
|
||||
" as a native command.")
|
||||
(~docs/section
|
||||
:title "Parser"
|
||||
:id "parser"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The tokenizer and recursive-descent parser live in "
|
||||
(code "lib/graphql.sx")
|
||||
". Feed it any GraphQL source and get an SX AST back.")
|
||||
(~docs/code :src "(gql-parse \"{ user(id: 1) { name email } }\")")
|
||||
(p (~tw :tokens "text-gray-500 text-sm mb-3") "Result:")
|
||||
(~docs/code
|
||||
:src (str
|
||||
"(gql-doc\n"
|
||||
" (gql-query nil () ()\n"
|
||||
" ((gql-field \"user\" ((\"id\" 1)) ()\n"
|
||||
" ((gql-field \"name\" () () ())\n"
|
||||
" (gql-field \"email\" () () ()))))))"))
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"Every GraphQL concept has an SX node type: "
|
||||
(code "gql-query")
|
||||
", "
|
||||
(code "gql-mutation")
|
||||
", "
|
||||
(code "gql-field")
|
||||
", "
|
||||
(code "gql-fragment")
|
||||
", "
|
||||
(code "gql-var")
|
||||
", "
|
||||
(code "gql-directive")
|
||||
". "
|
||||
"Accessors like "
|
||||
(code "gql-field-name")
|
||||
" and "
|
||||
(code "gql-op-selections")
|
||||
" make the AST easy to walk."))
|
||||
(~docs/section
|
||||
:title "Variables and Operations"
|
||||
:id "variables"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Named operations, variable definitions with types and defaults, "
|
||||
"and variable references all parse to structured AST nodes.")
|
||||
(~docs/code
|
||||
:src "query GetUser($id: ID!, $limit: Int = 10) {\n user(id: $id) {\n name\n posts(limit: $limit) { title }\n }\n}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 text-sm mt-3 mb-3")
|
||||
"Variable definitions become "
|
||||
(code "(gql-var-def name type default)")
|
||||
" nodes. "
|
||||
"References become "
|
||||
(code "(gql-var name)")
|
||||
" nodes that the executor substitutes "
|
||||
"from a provided bindings dict."))
|
||||
(~docs/section
|
||||
:title "Fragments"
|
||||
:id "fragments"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Fragment definitions, fragment spreads, and inline fragments:")
|
||||
(~docs/code
|
||||
:src "query {\n user {\n ...UserFields\n ... on Admin { role }\n }\n}\n\nfragment UserFields on User {\n name\n email\n}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"The executor collects fragment definitions from the document "
|
||||
"and resolves spreads during projection. Inline fragments apply "
|
||||
"unconditionally (type checking is available via "
|
||||
(code "spec/types.sx")
|
||||
")."))
|
||||
(~docs/section
|
||||
:title "Executor"
|
||||
:id "executor"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The executor in "
|
||||
(code "lib/graphql-exec.sx")
|
||||
" walks the parsed AST and dispatches each root field through a resolver. "
|
||||
"The default resolver uses "
|
||||
(code "perform io-gql-resolve")
|
||||
" to dispatch via IO suspension, exactly like "
|
||||
(code "defquery")
|
||||
".")
|
||||
(~docs/code
|
||||
:src "(let ((doc (gql-parse \"{ user(id: 1) { name email } }\"))\n (resolver (fn (field-name args op-type)\n (if (= field-name \"user\")\n {:name \"Alice\" :email \"alice@test.com\" :age 30}\n nil))))\n (gql-execute doc {} resolver))")
|
||||
(p (~tw :tokens "text-gray-500 text-sm mb-3") "Result:")
|
||||
(~docs/code
|
||||
:src "{:data {:user {:email \"alice@test.com\" :name \"Alice\"}}}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"Notice "
|
||||
(code ":age")
|
||||
" is absent from the result. "
|
||||
"The executor projects each resolver result down to only the fields "
|
||||
"the query requested. Nested selections, list results, and aliased "
|
||||
"fields all work."))
|
||||
(~docs/section
|
||||
:title "Aliases"
|
||||
:id "aliases"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Query the same field multiple times with different arguments:")
|
||||
(~docs/code
|
||||
:src "{\n me: user(id: 1) { name }\n them: user(id: 2) { name }\n}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"Each aliased field calls the resolver independently. "
|
||||
"Results are keyed by the alias name: "
|
||||
(code "{:data {:me {:name ...} :them {:name ...}}}")
|
||||
"."))
|
||||
(~docs/section
|
||||
:title "Serializer"
|
||||
:id "serializer"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The serializer converts an AST back to GraphQL source. "
|
||||
"Useful for logging, debugging, and wire format.")
|
||||
(~docs/code
|
||||
:src "(gql-serialize (gql-parse \"query GetUser($id: ID!) { user(id: $id) { name } }\"))")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 text-sm mt-3")
|
||||
"Round-trips cleanly. Parse, transform the AST, serialize back."))
|
||||
(~docs/section
|
||||
:title "Hyperscript: fetch gql"
|
||||
:id "hyperscript"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The "
|
||||
(code "fetch gql")
|
||||
" command embeds GraphQL queries directly in hyperscript. "
|
||||
"Write queries inline "
|
||||
(em "in the HTML attribute")
|
||||
", compiled to bytecode "
|
||||
"alongside the rest of the hyperscript.")
|
||||
(~docs/code
|
||||
:src "<!-- shorthand query -->\n<button _=\"on click fetch gql { user(id: 1) { name } } then put it.data.user.name into #result\">\n Load User\n</button>\n\n<!-- with explicit endpoint -->\n<button _=\"on click fetch gql { posts { title } } from '/api/graphql' then ...\">\n Load Posts\n</button>\n\n<!-- mutation -->\n<button _=\"on click fetch gql mutation { deletePost(id: 42) { success } } then ...\">\n Delete\n</button>")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3 mb-3")
|
||||
"The parser collects the GraphQL body between braces, preserving nested "
|
||||
"structure. At runtime, "
|
||||
(code "hs-fetch-gql")
|
||||
" sends a standard "
|
||||
(code "{ \"query\": \"...\", \"variables\": {} }")
|
||||
" JSON POST to the endpoint (defaults to "
|
||||
(code "/graphql")
|
||||
").")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600")
|
||||
"The compilation pipeline: hyperscript source "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"HS tokenizer "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"HS parser ("
|
||||
(code "fetch-gql")
|
||||
" AST) "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"HS compiler ("
|
||||
(code "hs-fetch-gql")
|
||||
" call) "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"bytecode. The GraphQL source string is a constant in the compiled output."))
|
||||
(~docs/section
|
||||
:title "GraphQL → SX Mapping"
|
||||
:id "mapping"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Every GraphQL concept has a direct SX equivalent:")
|
||||
(table
|
||||
(~tw :tokens "w-full text-sm border-collapse mb-4")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-200")
|
||||
(th
|
||||
(~tw :tokens "text-left py-2 pr-4 text-gray-500 font-medium")
|
||||
"GraphQL")
|
||||
(th
|
||||
(~tw :tokens "text-left py-2 pr-4 text-gray-500 font-medium")
|
||||
"SX")
|
||||
(th
|
||||
(~tw :tokens "text-left py-2 text-gray-500 font-medium")
|
||||
"Mechanism")))
|
||||
(tbody
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Query")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "defquery"))
|
||||
(td (~tw :tokens "py-2") "IO suspension"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Mutation")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "defaction"))
|
||||
(td (~tw :tokens "py-2") "IO suspension"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Subscription")
|
||||
(td (~tw :tokens "py-2 pr-4") "SSE + signals")
|
||||
(td (~tw :tokens "py-2") "Reactive islands"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Fragment")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "defcomp"))
|
||||
(td (~tw :tokens "py-2") "Component composition"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Schema")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "spec/types.sx"))
|
||||
(td (~tw :tokens "py-2") "Gradual type system"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Resolver")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "perform"))
|
||||
(td (~tw :tokens "py-2") "CEK IO suspension"))
|
||||
(tr
|
||||
(td (~tw :tokens "py-2 pr-4") "Field selection")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "gql-project"))
|
||||
(td (~tw :tokens "py-2") "Dict projection")))))
|
||||
(~docs/section
|
||||
:title "Live Parse"
|
||||
:id "live-parse"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Type a GraphQL query and see the SX AST. "
|
||||
"Parsed by the same "
|
||||
(code "lib/graphql.sx")
|
||||
" that compiles to bytecode.")
|
||||
(div
|
||||
(~tw :tokens "flex flex-col gap-3")
|
||||
(textarea
|
||||
:id "gql-input"
|
||||
:name "source"
|
||||
:rows "5"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none")
|
||||
:placeholder "{ user(id: 1) { name email } }"
|
||||
"{\n user(id: 1) {\n name\n email\n }\n}")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors cursor-pointer")
|
||||
:id "gql-parse-btn"
|
||||
"Parse")
|
||||
(div
|
||||
:id "gql-output"
|
||||
(~tw
|
||||
:tokens "min-h-24 p-3 bg-gray-50 rounded-lg border border-gray-200 font-mono text-sm text-gray-700 whitespace-pre-wrap")
|
||||
(span (~tw :tokens "text-gray-400") "AST will appear here")))
|
||||
(script
|
||||
"document.getElementById('gql-parse-btn').addEventListener('click', function() { var src = document.getElementById('gql-input').value; fetch('/sx/(applications.(graphql.(api.parse-demo)))?source=' + encodeURIComponent(src)).then(function(r) { return r.text(); }).then(function(html) { document.getElementById('gql-output').innerHTML = html; }); });"))))
|
||||
137
sx/sx/applications/htmx/_test/test-demo.sx
Normal file
137
sx/sx/applications/htmx/_test/test-demo.sx
Normal file
@@ -0,0 +1,137 @@
|
||||
;; Tests for the htmx demo page
|
||||
;; Each deftest declares its runner: :playwright (live browser) or :harness (mock IO)
|
||||
|
||||
;; ── Click to Load ──────────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
click-to-load
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(click "button[hx-get]")
|
||||
(wait 2000)
|
||||
(assert-text "#click-result" :contains "Content loaded!")
|
||||
(assert-text "#click-result" :contains "Fetched from the server via sx-get"))
|
||||
|
||||
(deftest
|
||||
click-to-load-network
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(click "button[hx-get]")
|
||||
(assert-network :method "GET" :url-contains "api.click"))
|
||||
|
||||
;; ── Active Search ──────────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
search-debounce
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(fill "input[hx-get]" "swap")
|
||||
(wait 1500)
|
||||
(assert-network
|
||||
:method "GET"
|
||||
:url-contains "api.search"
|
||||
:url-contains "q=swap")
|
||||
(assert-text "#search-results" :not-contains "Type to search"))
|
||||
|
||||
(deftest
|
||||
search-empty
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(fill "input[hx-get]" "xyznonexistent")
|
||||
(wait 1500)
|
||||
(assert-text "#search-results" :contains "No results"))
|
||||
|
||||
;; ── Tabs ───────────────────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
tab-switch
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(click "button[hx-get*=\"tab=features\"]")
|
||||
(wait 2000)
|
||||
(assert-text "#htmx-tab-content" :not-contains "Click a tab")
|
||||
(assert-network :method "GET" :url-contains "api.tab"))
|
||||
|
||||
(deftest
|
||||
tab-overview
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(click "button[hx-get*=\"tab=overview\"]")
|
||||
(wait 2000)
|
||||
(assert-text "#htmx-tab-content" :contains "htmx gives you access"))
|
||||
|
||||
;; ── Append Items ───────────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
append-item
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(assert-count "#item-list > *" 0)
|
||||
(click "button[hx-post*=\"api.append\"]")
|
||||
(wait 2000)
|
||||
(assert-count "#item-list > *" :gte 1)
|
||||
(assert-network :method "POST" :url-contains "api.append"))
|
||||
|
||||
(deftest
|
||||
append-multiple
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(click "button[hx-post*=\"api.append\"]")
|
||||
(wait 1500)
|
||||
(click "button[hx-post*=\"api.append\"]")
|
||||
(wait 1500)
|
||||
(assert-count "#item-list > *" :gte 2))
|
||||
|
||||
;; ── Delete with Confirm ────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
delete-item
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(assert-count "#delete-list .item" 3)
|
||||
(accept-dialog)
|
||||
(click "button[hx-delete]:first-of-type")
|
||||
(wait 2000)
|
||||
(assert-count "#delete-list .item" 2)
|
||||
(assert-network :method "DELETE" :url-contains "api.delete"))
|
||||
|
||||
;; ── Form Submission ────────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
form-submit
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(fill "form[hx-post] input[name=\"name\"]" "Alice")
|
||||
(fill "form[hx-post] input[name=\"email\"]" "alice@example.com")
|
||||
(click "form[hx-post] button[type=\"submit\"]")
|
||||
(wait 2000)
|
||||
(assert-text "#form-result" :contains "Alice")
|
||||
(assert-text "#form-result" :contains "alice@example.com")
|
||||
(assert-network :method "POST" :url-contains "api.form"))
|
||||
|
||||
;; ── Boot & Activation ──────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
htmx-boot-activates
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(assert-count "[hx-get],[hx-post],[hx-delete]" :gte 8)
|
||||
(eval "(type-of htmx-boot-subtree!)" :equals "function"))
|
||||
|
||||
(deftest
|
||||
no-console-errors
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(assert-no-errors)
|
||||
(assert-text "[data-sx-ready]" :exists true))
|
||||
|
||||
;; ── OOB Swap Filtering ────────────────────────────────────────
|
||||
|
||||
(deftest
|
||||
oob-not-visible
|
||||
:runner :playwright
|
||||
:url "/sx/(applications.(htmx))"
|
||||
(click "button[hx-get]")
|
||||
(wait 2000)
|
||||
(assert-text "#click-result" :not-contains "defcomp")
|
||||
(assert-text "#click-result" :not-contains "~examples"))
|
||||
313
sx/sx/applications/htmx/index.sx
Normal file
313
sx/sx/applications/htmx/index.sx
Normal file
@@ -0,0 +1,313 @@
|
||||
;; htmx demo — live interactive examples using hx-* attributes
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "htmx"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"Every "
|
||||
(code "hx-*")
|
||||
" attribute is syntactic sugar for a "
|
||||
(a
|
||||
:href "/sx/(applications.(hyperscript))"
|
||||
(~tw :tokens "text-violet-600 underline")
|
||||
"_hyperscript")
|
||||
" event handler. Same runtime, same bytecode, zero duplication.")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 mb-8")
|
||||
"These demos use real "
|
||||
(code "hx-*")
|
||||
" attributes processed by "
|
||||
(code "htmx-activate!")
|
||||
" — not "
|
||||
(code "_=\"...\"")
|
||||
" hyperscript syntax. "
|
||||
"Click buttons, type in inputs, and watch the network tab.")
|
||||
(~docs/section
|
||||
:title "Click to Load"
|
||||
:id "click-to-load"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The simplest htmx pattern: a button with "
|
||||
(code "hx-get")
|
||||
" that loads server content into a target element.")
|
||||
(~docs/code
|
||||
:src "<button hx-get=\"/sx/(applications.(htmx.(api.click)))\"\n hx-target=\"#click-result\"\n hx-swap=\"innerHTML\">\n Load Content\n</button>")
|
||||
(div
|
||||
(~tw :tokens "mt-4 flex flex-col gap-3")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
||||
:hx-get "/sx/(applications.(htmx.(api.click)))"
|
||||
:hx-target "#click-result"
|
||||
:hx-swap "innerHTML"
|
||||
"Load Content")
|
||||
(div
|
||||
:id "click-result"
|
||||
(~tw
|
||||
:tokens "min-h-16 border border-dashed border-gray-200 rounded-lg flex items-center justify-center")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Content will appear here"))))
|
||||
(~docs/section
|
||||
:title "Active Search"
|
||||
:id "active-search"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Search-as-you-type with "
|
||||
(code "hx-trigger=\"input changed delay:300ms\"")
|
||||
" — debounces input and only fires when the value actually changes.")
|
||||
(~docs/code
|
||||
:src "<input hx-get=\"/sx/(applications.(htmx.(api.search)))\"\n hx-trigger=\"input changed delay:300ms\"\n hx-target=\"#search-results\"\n name=\"q\"\n placeholder=\"Search patterns...\">")
|
||||
(div
|
||||
(~tw :tokens "mt-4 space-y-3")
|
||||
(input
|
||||
(~tw
|
||||
:tokens "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 focus:border-violet-400 outline-none")
|
||||
:type "text"
|
||||
:name "q"
|
||||
:placeholder "Search patterns… (try 'tab', 'modal', 'scroll')"
|
||||
:hx-get "/sx/(applications.(htmx.(api.search)))"
|
||||
:hx-trigger "input changed delay:300ms"
|
||||
:hx-target "#search-results")
|
||||
(div
|
||||
:id "search-results"
|
||||
(~tw :tokens "border border-gray-200 rounded-lg overflow-hidden")
|
||||
(p (~tw :tokens "text-gray-400 text-sm p-3") "Type to search…"))))
|
||||
(~docs/section
|
||||
:title "Tabs"
|
||||
:id "tabs"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Tab buttons with "
|
||||
(code "hx-get")
|
||||
" loading content from the server. "
|
||||
"Each click replaces the tab panel with server-rendered HTML.")
|
||||
(~docs/code
|
||||
:src "<button hx-get=\"/api/tab?tab=overview\"\n hx-target=\"#tab-content\"\n hx-swap=\"innerHTML\">\n Overview\n</button>")
|
||||
(div
|
||||
(~tw :tokens "mt-4 border border-gray-200 rounded-lg overflow-hidden")
|
||||
(div
|
||||
(~tw :tokens "flex border-b border-gray-200 bg-gray-50")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 text-sm font-medium text-violet-700 border-b-2 border-violet-600 bg-white")
|
||||
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=overview"
|
||||
:hx-target "#htmx-tab-content"
|
||||
:hx-swap "innerHTML"
|
||||
"Overview")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700")
|
||||
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=features"
|
||||
:hx-target "#htmx-tab-content"
|
||||
:hx-swap "innerHTML"
|
||||
"Features")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700")
|
||||
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=code"
|
||||
:hx-target "#htmx-tab-content"
|
||||
:hx-swap "innerHTML"
|
||||
"Code"))
|
||||
(div
|
||||
:id "htmx-tab-content"
|
||||
(~tw :tokens "p-4")
|
||||
(p
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Click a tab to load content"))))
|
||||
(~docs/section
|
||||
:title "Append Items"
|
||||
:id "append"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Using "
|
||||
(code "hx-swap=\"beforeend\"")
|
||||
" to append new items instead of replacing content. "
|
||||
"The htmx v4 alias "
|
||||
(code "append")
|
||||
" also works.")
|
||||
(~docs/code
|
||||
:src "<button hx-post=\"/api/append\"\n hx-target=\"#item-list\"\n hx-swap=\"beforeend\">\n Add Item\n</button>")
|
||||
(div
|
||||
(~tw :tokens "mt-4 space-y-3")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
||||
:hx-post "/sx/(applications.(htmx.(api.append)))"
|
||||
:hx-target "#item-list"
|
||||
:hx-swap "beforeend"
|
||||
"Add Item")
|
||||
(div
|
||||
:id "item-list"
|
||||
(~tw
|
||||
:tokens "min-h-16 border border-dashed border-gray-200 rounded-lg p-2")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Items will append here"))))
|
||||
(~docs/section
|
||||
:title "Delete with Confirm"
|
||||
:id "delete"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
(code "hx-confirm")
|
||||
" shows a native dialog before firing the request. "
|
||||
(code "hx-delete")
|
||||
" sends a DELETE method. The empty response removes the target.")
|
||||
(~docs/code
|
||||
:src "<button hx-delete=\"/api/delete\"\n hx-target=\"closest .item\"\n hx-swap=\"outerHTML\"\n hx-confirm=\"Are you sure?\">\n Delete\n</button>")
|
||||
(div
|
||||
:id "delete-demo"
|
||||
(~tw :tokens "mt-4 space-y-2")
|
||||
(div
|
||||
(~tw
|
||||
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
|
||||
(span "First item")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
|
||||
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
|
||||
:hx-target "closest .item"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-confirm "Delete this item?"
|
||||
"Delete"))
|
||||
(div
|
||||
(~tw
|
||||
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
|
||||
(span "Second item")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
|
||||
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
|
||||
:hx-target "closest .item"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-confirm "Delete this item?"
|
||||
"Delete"))
|
||||
(div
|
||||
(~tw
|
||||
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
|
||||
(span "Third item")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
|
||||
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
|
||||
:hx-target "closest .item"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-confirm "Delete this item?"
|
||||
"Delete"))))
|
||||
(~docs/section
|
||||
:title "Form Submission"
|
||||
:id "form"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"A form with "
|
||||
(code "hx-post")
|
||||
" sends form data via AJAX. "
|
||||
"The response replaces the target — no full page reload.")
|
||||
(~docs/code
|
||||
:src "<form hx-post=\"/api/form\"\n hx-target=\"#form-result\"\n hx-swap=\"innerHTML\">\n <input name=\"name\" />\n <input name=\"email\" type=\"email\" />\n <button type=\"submit\">Submit</button>\n</form>")
|
||||
(div
|
||||
(~tw :tokens "mt-4 space-y-3")
|
||||
(form
|
||||
(~tw :tokens "space-y-3")
|
||||
:hx-post "/sx/(applications.(htmx.(api.form)))"
|
||||
:hx-target "#form-result"
|
||||
:hx-swap "innerHTML"
|
||||
(div
|
||||
(~tw :tokens "flex gap-3")
|
||||
(input
|
||||
(~tw
|
||||
:tokens "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 outline-none")
|
||||
:type "text"
|
||||
:name "name"
|
||||
:placeholder "Name")
|
||||
(input
|
||||
(~tw
|
||||
:tokens "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 outline-none")
|
||||
:type "email"
|
||||
:name "email"
|
||||
:placeholder "Email"))
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
||||
:type "submit"
|
||||
"Submit"))
|
||||
(div
|
||||
:id "form-result"
|
||||
(~tw :tokens "min-h-12")
|
||||
(span
|
||||
(~tw :tokens "text-gray-400 text-sm")
|
||||
"Submit the form to see the response"))))
|
||||
(~docs/section
|
||||
:title "How It Works"
|
||||
:id "how-it-works"
|
||||
(~docs/note
|
||||
(p
|
||||
(strong "Zero separate runtime. ")
|
||||
"htmx attributes are translated to the same runtime calls as "
|
||||
(a
|
||||
:href "/sx/(applications.(hyperscript))"
|
||||
(~tw :tokens "text-violet-600 underline")
|
||||
"_hyperscript")
|
||||
" event handlers. "
|
||||
(code "hx-get")
|
||||
" becomes "
|
||||
(code "perform io-fetch")
|
||||
". "
|
||||
(code "hx-swap=\"innerHTML\"")
|
||||
" becomes "
|
||||
(code "dom-set-inner-html")
|
||||
". "
|
||||
"Same bytecode path."))
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"When the page loads, "
|
||||
(code "htmx-boot!")
|
||||
" scans the DOM for elements with "
|
||||
(code "hx-get")
|
||||
", "
|
||||
(code "hx-post")
|
||||
", etc. For each element, "
|
||||
(code "htmx-activate!")
|
||||
" reads the attributes, builds a handler function "
|
||||
"from the same primitives the hyperscript compiler emits, and registers "
|
||||
"it via "
|
||||
(code "hs-on")
|
||||
".")
|
||||
(ol
|
||||
(~tw :tokens "mt-3 space-y-1 text-gray-600 list-decimal list-inside")
|
||||
(li
|
||||
(code "htmx-boot!")
|
||||
" finds all "
|
||||
(code "[hx-get],[hx-post],…")
|
||||
" elements")
|
||||
(li
|
||||
(code "htmx-activate!")
|
||||
" reads "
|
||||
(code "hx-get")
|
||||
", "
|
||||
(code "hx-target")
|
||||
", "
|
||||
(code "hx-swap")
|
||||
", "
|
||||
(code "hx-trigger")
|
||||
" attributes")
|
||||
(li
|
||||
"Builds a handler: "
|
||||
(code
|
||||
"(fn (evt) (perform (io-fetch url method)) (hx-swap! target ...))"))
|
||||
(li
|
||||
"Registers via "
|
||||
(code "hs-on")
|
||||
" — same event system as hyperscript")
|
||||
(li "On trigger, handler fires through the same bytecode VM"))
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"The full implementation is ~400 lines in "
|
||||
(code "lib/hyperscript/htmx.sx")
|
||||
" with "
|
||||
(a
|
||||
:href "/sx/(applications.(hyperscript.htmx))"
|
||||
(~tw :tokens "text-violet-600 underline")
|
||||
"57 tests")
|
||||
" covering parsing, swap modes, triggers, and v4 features."))))
|
||||
32
sx/sx/applications/hyperscript/_islands/compile-result.sx
Normal file
32
sx/sx/applications/hyperscript/_islands/compile-result.sx
Normal file
@@ -0,0 +1,32 @@
|
||||
;; _hyperscript Playground
|
||||
;; Lives under Applications: /sx/(applications.(hyperscript))
|
||||
;; ── Compile result component (server-side rendered) ─────────────
|
||||
(defcomp
|
||||
(&key source)
|
||||
(if
|
||||
(or (nil? source) (empty? source))
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-400 italic")
|
||||
"Enter some hyperscript and click Compile")
|
||||
(let
|
||||
((compiled (hs-to-sx-from-source source)))
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(div
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2")
|
||||
"Compiled SX")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto")
|
||||
(sx-serialize compiled)))
|
||||
(div
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2")
|
||||
"Parse Tree")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-900 text-amber-400 p-4 rounded-lg text-sm overflow-x-auto")
|
||||
(sx-serialize (hs-compile source))))))))
|
||||
30
sx/sx/applications/hyperscript/_islands/example.sx
Normal file
30
sx/sx/applications/hyperscript/_islands/example.sx
Normal file
@@ -0,0 +1,30 @@
|
||||
;; ── Compile handler (POST endpoint) ─────────────────────────────
|
||||
(defcomp
|
||||
(&key source description)
|
||||
(div
|
||||
(~tw :tokens "border border-gray-200 rounded-lg p-4 mb-4")
|
||||
(when
|
||||
description
|
||||
(p (~tw :tokens "text-sm text-gray-600 mb-2") description))
|
||||
(div
|
||||
(~tw :tokens "flex gap-4 items-start mb-3")
|
||||
(div
|
||||
(~tw :tokens "flex-1")
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-400 mb-1")
|
||||
"Source")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-50 text-gray-900 p-3 rounded text-sm font-mono whitespace-pre-wrap break-words")
|
||||
(str "_=\"" source "\"")))
|
||||
(when
|
||||
(string-contains? source "on click")
|
||||
(div
|
||||
(~tw :tokens "flex-shrink-0 pt-5")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 border border-violet-300 rounded-lg text-sm font-medium text-violet-700 hover:bg-violet-50 transition-colors")
|
||||
:_ source
|
||||
"Try it"))))
|
||||
(~hyperscript/compile-result :source source)))
|
||||
36
sx/sx/applications/hyperscript/_islands/live-demo.sx
Normal file
36
sx/sx/applications/hyperscript/_islands/live-demo.sx
Normal file
@@ -0,0 +1,36 @@
|
||||
;; ── Playground island ───────────────────────────────────────────
|
||||
(defcomp
|
||||
()
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(style
|
||||
"\n .bg-violet-600 { background-color: rgb(124 58 237); }\n .text-white { color: white; }\n .animate-bounce { animation: bounce 1s infinite; }\n @keyframes bounce {\n 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1); }\n 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1); }\n }\n ")
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-600")
|
||||
"These buttons have "
|
||||
(code "_=\"...\"")
|
||||
" attributes — hyperscript compiled to SX and activated at boot.")
|
||||
(div
|
||||
(~tw :tokens "flex gap-3 items-center")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors")
|
||||
:_ "on click toggle .bg-violet-600 on me then toggle .text-white on me"
|
||||
"Toggle Color")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors")
|
||||
:_ "on click add .animate-bounce to me then wait 1s then remove .animate-bounce from me"
|
||||
"Bounce")
|
||||
(span
|
||||
(~tw :tokens "text-sm text-gray-500")
|
||||
:id "click-counter"
|
||||
"0 clicks"))
|
||||
(div
|
||||
(~tw :tokens "mt-2")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors")
|
||||
:_ "on click increment @data-count on me then set #click-counter's innerHTML to my @data-count"
|
||||
:data-count "0"
|
||||
"Count Clicks"))))
|
||||
28
sx/sx/applications/hyperscript/_islands/playground.sx
Normal file
28
sx/sx/applications/hyperscript/_islands/playground.sx
Normal file
@@ -0,0 +1,28 @@
|
||||
;; ── Pipeline example component ──────────────────────────────────
|
||||
(defcomp
|
||||
()
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(form
|
||||
:sx-post "/sx/(applications.(hyperscript.(api.compile)))"
|
||||
:sx-target "#hs-playground-result"
|
||||
:sx-swap "innerHTML"
|
||||
(div
|
||||
(label
|
||||
(~tw :tokens "block text-sm font-medium text-gray-700 mb-1")
|
||||
"Hyperscript source")
|
||||
(textarea
|
||||
:name "source"
|
||||
(~tw
|
||||
:tokens "w-full h-24 font-mono text-sm p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 focus:border-transparent")
|
||||
"on click add .active to me"))
|
||||
(div
|
||||
(~tw :tokens "mt-2")
|
||||
(button
|
||||
:type "submit"
|
||||
(~tw
|
||||
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 text-sm font-medium")
|
||||
"Compile")))
|
||||
(div
|
||||
:id "hs-playground-result"
|
||||
(~hyperscript/compile-result :source "on click add .active to me"))))
|
||||
32
sx/sx/applications/hyperscript/_islands/translation.sx
Normal file
32
sx/sx/applications/hyperscript/_islands/translation.sx
Normal file
@@ -0,0 +1,32 @@
|
||||
;; Side-by-side htmx ↔ hyperscript translation display
|
||||
(defcomp
|
||||
(&key htmx-html hs-source description)
|
||||
(div
|
||||
(~tw :tokens "border border-gray-200 rounded-lg overflow-hidden mb-6")
|
||||
(when
|
||||
description
|
||||
(div
|
||||
(~tw :tokens "px-4 py-2 bg-gray-50 border-b border-gray-200")
|
||||
(p (~tw :tokens "text-sm font-medium text-gray-700") description)))
|
||||
(div
|
||||
(~tw :tokens "grid grid-cols-2 divide-x divide-gray-200")
|
||||
(div
|
||||
(~tw :tokens "p-4")
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wide text-gray-400 mb-2")
|
||||
"htmx")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-stone-100 text-gray-900 p-3 rounded text-sm font-mono overflow-x-auto whitespace-pre")
|
||||
htmx-html))
|
||||
(div
|
||||
(~tw :tokens "p-4")
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wide text-violet-400 mb-2")
|
||||
"_hyperscript")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-stone-100 text-gray-900 p-3 rounded text-sm font-mono overflow-x-auto whitespace-pre")
|
||||
hs-source)))))
|
||||
330
sx/sx/applications/hyperscript/htmx/index.sx
Normal file
330
sx/sx/applications/hyperscript/htmx/index.sx
Normal file
@@ -0,0 +1,330 @@
|
||||
;; htmx 4.0 compatibility — demo page
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "htmx.sx"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"Every "
|
||||
(code "hx-")
|
||||
" attribute is syntactic sugar for a hyperscript event handler. "
|
||||
"The htmx compat layer translates attributes to the same runtime calls "
|
||||
"the "
|
||||
(code "_=\"...\"")
|
||||
" compiler emits. Same bytecode. Zero duplication.")
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-500 mb-8")
|
||||
"htmx 4.0 compatible: "
|
||||
(code "hx-action")
|
||||
"/"
|
||||
(code "hx-method")
|
||||
", "
|
||||
(code ":inherited")
|
||||
" modifiers, swap aliases, "
|
||||
(code "hx-status:XXX")
|
||||
", "
|
||||
(code "hx-sync")
|
||||
", SSE in core.")
|
||||
(~docs/section
|
||||
:title "The Translation"
|
||||
:id "translation"
|
||||
(p
|
||||
"An htmx element and its hyperscript equivalent compile to identical runtime calls:")
|
||||
(~hyperscript/translation
|
||||
:htmx-html "<button hx-get=\"/api/items\"\n hx-target=\"#list\"\n hx-swap=\"innerHTML\"\n hx-indicator=\"#spinner\"\n hx-confirm=\"Sure?\"\n hx-push-url=\"true\">"
|
||||
:hs-source "on click\n confirm 'Sure?'\n add .htmx-request to #spinner\n fetch /api/items\n put the result into #list\n push url '/api/items'\n remove .htmx-request from #spinner"
|
||||
:description "A button that fetches, swaps, confirms, indicates, and pushes history")
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-600 mt-4")
|
||||
"Both go through: "
|
||||
(code "hs-on")
|
||||
" → "
|
||||
(code "perform io-fetch")
|
||||
" → "
|
||||
(code "dom-set-inner-html")
|
||||
". The htmx path skips parsing/compilation and "
|
||||
"calls the runtime primitives directly."))
|
||||
(~docs/section
|
||||
:title "How It Works"
|
||||
:id "how-it-works"
|
||||
(~docs/note
|
||||
(p
|
||||
(strong "Architecture: ")
|
||||
"htmx-activate! scans "
|
||||
(code "hx-*")
|
||||
" attributes, "
|
||||
"builds a handler function from the same runtime primitives the hyperscript compiler emits "
|
||||
"(hs-on, dom-add-class, perform io-fetch, etc.), and registers it via "
|
||||
(code "hs-on")
|
||||
". "
|
||||
"No separate runtime. No duplication."))
|
||||
(p "The activation pipeline:")
|
||||
(ol
|
||||
(~tw :tokens "list-decimal pl-6 space-y-1 text-sm text-gray-700 mb-4")
|
||||
(li
|
||||
(code "hs-activate!(el)")
|
||||
" reads "
|
||||
(code "_=\"...\"")
|
||||
" → parse → compile → eval")
|
||||
(li
|
||||
(code "htmx-activate!(el)")
|
||||
" reads "
|
||||
(code "hx-*")
|
||||
" → build handler → "
|
||||
(code "hs-on"))
|
||||
(li "Both produce the same runtime calls")
|
||||
(li
|
||||
(code "hs-boot-subtree!")
|
||||
" runs both on every DOM subtree (page load + after swaps)"))
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
"(define htmx-activate!\n (fn (el)\n (when (not (dom-get-attr el \"hx-ignore\"))\n (let ((verb-info (hx-verb-info el)))\n (when (and verb-info (not (dom-get-data el \"hx-active\")))\n (dom-set-data el \"hx-active\" true)\n (let ((method (first verb-info))\n (url (nth verb-info 1)))\n (let ((trigger (hx-parse-trigger\n (dom-get-attr el \"hx-trigger\") el))\n (handler (hx-make-handler el method url)))\n (hx-register-trigger! el trigger handler))))))))"
|
||||
"lisp")))
|
||||
(~docs/section
|
||||
:title "What's Shared (Zero Duplication)"
|
||||
:id "shared"
|
||||
(~docs/table
|
||||
:headers ("Capability" "Runtime Function" "Used By")
|
||||
:rows (list
|
||||
(list "Event dispatch" "hs-on / dom-listen" "both")
|
||||
(list "HTTP fetch" "perform io-fetch" "both")
|
||||
(list
|
||||
"DOM swap"
|
||||
"dom-set-inner-html / dom-insert-adjacent-html"
|
||||
"both")
|
||||
(list "CSS transitions" "dom-add-class / dom-remove-class" "both")
|
||||
(list "History API" "perform io-push-state" "both")
|
||||
(list "Indicator toggle" "dom-add-class .htmx-request" "both")
|
||||
(list "Confirm dialog" "perform io-confirm" "both")
|
||||
(list "Wait/settle" "hs-wait" "both")
|
||||
(list "Subtree boot" "hs-boot-subtree!" "both"))))
|
||||
(~docs/section
|
||||
:title "Live Demos"
|
||||
:id "demos"
|
||||
(p
|
||||
"These demos use "
|
||||
(code "_=\"...\"")
|
||||
" hyperscript syntax. "
|
||||
"The equivalent "
|
||||
(code "hx-*")
|
||||
" attributes would produce identical behavior "
|
||||
"through the same runtime.")
|
||||
(~docs/subsection
|
||||
:title "Toggle (Class Manipulation)"
|
||||
(~hyperscript/translation
|
||||
:htmx-html "<button hx-get=\"\" hx-swap=\"none\"\n hx-trigger=\"click\">"
|
||||
:hs-source "on click toggle .bg-violet-600 on me\n toggle .text-white on me"
|
||||
:description "Toggle class on click")
|
||||
(div
|
||||
(~tw :tokens "flex gap-3 items-center mt-3 mb-6")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors duration-200")
|
||||
:_ "on click toggle .bg-violet-600 on me then toggle .text-white on me"
|
||||
"Click to Toggle")))
|
||||
(~docs/subsection
|
||||
:title "Counter (State + DOM Update)"
|
||||
(~hyperscript/translation
|
||||
:htmx-html "<button hx-get=\"/api/increment\"\n hx-target=\"#count\"\n hx-swap=\"innerHTML\">"
|
||||
:hs-source "on click\n get the innerHTML of #demo-count\n set x to it as Int\n set the innerHTML of #demo-count\n to (x + 1)"
|
||||
:description "Increment a counter")
|
||||
(div
|
||||
(~tw :tokens "flex gap-3 items-center mt-3 mb-6")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "px-4 py-2 border border-violet-300 rounded-lg text-sm bg-violet-50 hover:bg-violet-100")
|
||||
:_ "on click get the innerHTML of #demo-count then set x to it as Int then set the innerHTML of #demo-count to (x + 1)"
|
||||
"+1")
|
||||
(span (~tw :tokens "text-lg font-mono") :id "demo-count" "0")))
|
||||
(~docs/subsection
|
||||
:title "Show/Hide (Indicator Pattern)"
|
||||
(~hyperscript/translation
|
||||
:htmx-html "<button hx-get=\"/api/data\"\n hx-indicator=\"#demo-spinner\">"
|
||||
:hs-source "on click\n add .htmx-request to #demo-spinner\n wait 1s\n remove .htmx-request from #demo-spinner"
|
||||
:description "Show indicator during request")
|
||||
(style
|
||||
"\n #demo-spinner { opacity: 0; transition: opacity 0.2s; }\n #demo-spinner.htmx-request { opacity: 1; }\n ")
|
||||
(div
|
||||
(~tw :tokens "flex gap-3 items-center mt-3 mb-6")
|
||||
(button
|
||||
(~tw :tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm")
|
||||
:_ "on click add .htmx-request to #demo-spinner then wait 1s then remove .htmx-request from #demo-spinner"
|
||||
"Load Data")
|
||||
(span
|
||||
:id "demo-spinner"
|
||||
(~tw :tokens "text-sm text-violet-600")
|
||||
"Loading...")))
|
||||
(~docs/subsection
|
||||
:title "Swap Modes"
|
||||
(~hyperscript/translation
|
||||
:htmx-html "<div hx-get=\"/fragment\"\n hx-target=\"#demo-swap-target\"\n hx-swap=\"beforeend\">"
|
||||
:hs-source "on click\n fetch /fragment\n put the result at the end of\n #demo-swap-target"
|
||||
:description "Append content (hx-swap=\"beforeend\" = v4 alias \"append\")")
|
||||
(div
|
||||
(~tw :tokens "mt-3 mb-6")
|
||||
(div
|
||||
:id "demo-swap-target"
|
||||
(~tw
|
||||
:tokens "border border-dashed border-gray-300 rounded-lg p-3 min-h-12 text-sm text-gray-500")
|
||||
(div (~tw :tokens "text-gray-400") "Target area"))
|
||||
(button
|
||||
(~tw
|
||||
:tokens "mt-2 px-4 py-2 border border-gray-300 rounded-lg text-sm")
|
||||
:_ "on click put '<div class=\"py-1 text-violet-700\">Appended at ' + Date() + '</div>' at the end of #demo-swap-target"
|
||||
"Append Item"))))
|
||||
(~docs/section
|
||||
:title "htmx 4.0 Features"
|
||||
:id "v4"
|
||||
(~docs/subsection
|
||||
:title "Swap Aliases"
|
||||
(p "v4 adds short aliases that normalize to canonical names:")
|
||||
(~docs/table
|
||||
:headers ("v4 Alias" "Canonical" "Meaning")
|
||||
:rows (list
|
||||
(list
|
||||
(code "before")
|
||||
(code "beforebegin")
|
||||
"Insert before element")
|
||||
(list (code "after") (code "afterend") "Insert after element")
|
||||
(list
|
||||
(code "prepend")
|
||||
(code "afterbegin")
|
||||
"Insert as first child")
|
||||
(list (code "append") (code "beforeend") "Insert as last child")
|
||||
(list
|
||||
(code "innerMorph")
|
||||
(code "innerMorph")
|
||||
"Morph children (idiomorph)")
|
||||
(list
|
||||
(code "outerMorph")
|
||||
(code "outerMorph")
|
||||
"Morph entire element")
|
||||
(list
|
||||
(code "textContent")
|
||||
(code "textContent")
|
||||
"Set text, no HTML parsing"))))
|
||||
(~docs/subsection
|
||||
:title "hx-action / hx-method"
|
||||
(p "v4 alternative to " (code "hx-get") "/" (code "hx-post") ":")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; These are equivalent:\n(button :hx-post \"/api/submit\")\n(button :hx-action \"/api/submit\" :hx-method \"post\")"
|
||||
"lisp")))
|
||||
(~docs/subsection
|
||||
:title "Explicit Inheritance"
|
||||
(p
|
||||
"v4 requires "
|
||||
(code ":inherited")
|
||||
" modifier for attribute inheritance:")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; v4: parent target inherited by children\n(div :hx-target:inherited \"#results\"\n (button :hx-get \"/search\" \"Search\")\n (button :hx-get \"/filter\" \"Filter\"))"
|
||||
"lisp"))
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-500 mt-2")
|
||||
"Both buttons target "
|
||||
(code "#results")
|
||||
" via inherited attribute, "
|
||||
"not implicit inheritance."))
|
||||
(~docs/subsection
|
||||
:title "Status-Based Swap (hx-status:XXX)"
|
||||
(p "Override swap behavior per HTTP status code:")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
"(form :hx-post \"/api/submit\"\n :hx-target \"#result\"\n :hx-status:422 \"swap:innerHTML target:#errors\"\n :hx-status:5xx \"swap:none\")"
|
||||
"lisp"))
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-500 mt-2")
|
||||
"Specificity: exact ("
|
||||
(code "422")
|
||||
") > two-digit ("
|
||||
(code "50x")
|
||||
") > one-digit ("
|
||||
(code "5xx")
|
||||
")."))
|
||||
(~docs/subsection
|
||||
:title "Request Sync (hx-sync)"
|
||||
(p "Synchronize concurrent requests:")
|
||||
(~docs/table
|
||||
:headers ("Strategy" "Behavior")
|
||||
:rows (list
|
||||
(list (code "drop") "Skip if request already in flight (default)")
|
||||
(list (code "abort") "Cancel existing request, start new")
|
||||
(list (code "replace") "Cancel existing, replace with new")
|
||||
(list
|
||||
(code "queue last")
|
||||
"Queue latest request until current completes")))
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; Search: cancel stale requests\n(input :hx-get \"/search\"\n :hx-trigger \"keyup changed delay:500ms\"\n :hx-sync \"this:replace\")"
|
||||
"lisp")))
|
||||
(~docs/subsection
|
||||
:title "Server-Sent Events"
|
||||
(p "SSE is built into core in v4 (no extension needed):")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
"(div :hx-sse-connect \"/events\"\n :hx-sse-swap \"message:#notifications,alert:#alerts:outerHTML\")"
|
||||
"lisp"))
|
||||
(p
|
||||
(~tw :tokens "text-sm text-gray-500 mt-2")
|
||||
"Each event type maps to a target element and swap mode.")))
|
||||
(~docs/section
|
||||
:title "Swap Modifiers"
|
||||
:id "swap-modifiers"
|
||||
(p
|
||||
"The "
|
||||
(code "hx-swap")
|
||||
" value accepts space-separated modifiers after the mode:")
|
||||
(~docs/table
|
||||
:headers ("Modifier" "Example" "Effect")
|
||||
:rows (list
|
||||
(list (code "swap:") (code "swap:100ms") "Delay before swap")
|
||||
(list
|
||||
(code "settle:")
|
||||
(code "settle:200ms")
|
||||
"Delay between swap and settle")
|
||||
(list
|
||||
(code "scroll:")
|
||||
(code "scroll:top")
|
||||
"Auto-scroll after swap")
|
||||
(list
|
||||
(code "transition:")
|
||||
(code "transition:true")
|
||||
"Use View Transitions API")
|
||||
(list
|
||||
(code "strip:")
|
||||
(code "strip:true")
|
||||
"Remove outer wrapper before swap")
|
||||
(list
|
||||
(code "target:")
|
||||
(code "target:#alt")
|
||||
"Override target inline")
|
||||
(list
|
||||
(code "ignoreTitle:")
|
||||
(code "ignoreTitle:true")
|
||||
"Don't update page title")))
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
"(button :hx-get \"/page\"\n :hx-swap \"innerHTML swap:50ms settle:100ms scroll:top transition:true\")"
|
||||
"lisp")))
|
||||
(~docs/section
|
||||
:title "Implementation"
|
||||
:id "implementation"
|
||||
(p "The total implementation:")
|
||||
(ul
|
||||
(~tw :tokens "list-disc pl-6 space-y-1 text-sm text-gray-700")
|
||||
(li (code "lib/hyperscript/htmx.sx") " — ~400 lines, 55 functions")
|
||||
(li
|
||||
"2 lines changed in "
|
||||
(code "integration.sx")
|
||||
" to hook into "
|
||||
(code "hs-activate!"))
|
||||
(li "57 tests across 11 suites")
|
||||
(li "Runtime cost: zero — it " (em "is") " the hyperscript runtime"))
|
||||
(~docs/note
|
||||
(p
|
||||
(strong "The insight: ")
|
||||
"htmx and hyperscript are not two competing libraries. "
|
||||
"htmx is a declarative attribute syntax for a subset of what hyperscript can express. "
|
||||
"The 'htmx compat layer' is just a thin attribute scanner "
|
||||
"that builds the same handler functions the hyperscript compiler produces.")))))
|
||||
83
sx/sx/applications/hyperscript/index.sx
Normal file
83
sx/sx/applications/hyperscript/index.sx
Normal file
@@ -0,0 +1,83 @@
|
||||
;; ── Live demo: actual hyperscript running ───────────────────────
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "_hyperscript Playground"
|
||||
(p
|
||||
:class "text-lg text-gray-600 mb-6"
|
||||
"Compile "
|
||||
(code "_hyperscript")
|
||||
" to SX expressions. "
|
||||
"The same "
|
||||
(code "_=\"...\"")
|
||||
" syntax, compiled to cached bytecode instead of re-parsed every page load.")
|
||||
(~docs/section
|
||||
:title "Interactive Playground"
|
||||
:id "playground"
|
||||
(p
|
||||
"Edit the hyperscript source and click Compile to see the tokenized, parsed, and compiled SX output.")
|
||||
(~hyperscript/playground))
|
||||
(~docs/section
|
||||
:title "Live Demo"
|
||||
:id "live-demo"
|
||||
(p
|
||||
"These buttons have "
|
||||
(code "_=\"...\"")
|
||||
" attributes — hyperscript compiled to SX and activated at boot.")
|
||||
(~hyperscript/live-demo))
|
||||
(~docs/section
|
||||
:title "Pipeline"
|
||||
:id "pipeline"
|
||||
(p "Every hyperscript string passes through three stages:")
|
||||
(ol
|
||||
:class "list-decimal list-inside space-y-1 text-sm text-gray-700 mb-4"
|
||||
(li
|
||||
(strong "Tokenize")
|
||||
" — source string to typed token stream (keywords, classes, ids, operators, strings)")
|
||||
(li
|
||||
(strong "Parse")
|
||||
" — token stream to AST (commands, expressions, features)")
|
||||
(li
|
||||
(strong "Compile")
|
||||
" — AST to SX expressions targeting "
|
||||
(code "web/lib/dom.sx")
|
||||
" primitives"))
|
||||
(p
|
||||
"The compiled SX is wrapped in "
|
||||
(code "(fn (me) ...)")
|
||||
" and evaluated with "
|
||||
(code "me")
|
||||
" bound to the element. Hyperscript variables are visible to "
|
||||
(code "eval (sx-expr)")
|
||||
" escapes."))
|
||||
(~docs/section
|
||||
:title "Examples"
|
||||
:id "examples"
|
||||
(style
|
||||
"\n button.active { background-color: #7c3aed !important; color: white !important; border-color: #7c3aed !important; }\n button.active:hover { background-color: #6d28d9 !important; }\n button.light { background-color: #fef3c7 !important; color: #92400e !important; border-color: #f59e0b !important; }\n button.light:hover { background-color: #fde68a !important; }\n button.dark { background-color: #1e293b !important; color: #e2e8f0 !important; border-color: #475569 !important; }\n button.dark:hover { background-color: #334155 !important; }\n .animate-bounce { animation: bounce 1s; }\n @keyframes bounce {\n 0%, 100% { transform: translateY(0); }\n 50% { transform: translateY(-25%); }\n }")
|
||||
(~hyperscript/example
|
||||
:source "on click add .active to me"
|
||||
:description "Event handler: click adds a CSS class")
|
||||
(~hyperscript/example
|
||||
:source "on click toggle between .light and .dark on me"
|
||||
:description "Toggle between two states")
|
||||
(~hyperscript/example
|
||||
:source "on click set my innerHTML to eval (str \"Hello from \" (+ 2 3) \" worlds\")"
|
||||
:description "SX escape: evaluate SX expressions from hyperscript")
|
||||
(~hyperscript/example
|
||||
:source "on click put \"<b>Rendered!</b>\" into #target"
|
||||
:description "Target another element by CSS selector")
|
||||
(div
|
||||
:id "target"
|
||||
(~tw
|
||||
:tokens "border border-dashed border-gray-300 rounded-lg p-3 min-h-[2rem] text-sm text-gray-400")
|
||||
"← render target")
|
||||
(~hyperscript/example
|
||||
:source "def double(n) return n + n end"
|
||||
:description "Define reusable functions")
|
||||
(~hyperscript/example
|
||||
:source "on click repeat 3 times add .active to me then wait 300ms then remove .active from me then wait 300ms end"
|
||||
:description "Iteration: repeat with timed animation (continuation after loop pending)")
|
||||
(~hyperscript/example
|
||||
:source "behavior Draggable on mousedown add .dragging to me end on mouseup remove .dragging from me end end"
|
||||
:description "Reusable behaviors — install on any element"))))
|
||||
@@ -1,5 +1,4 @@
|
||||
(defcomp
|
||||
~applications/native-browser/content
|
||||
()
|
||||
(~docs/page
|
||||
:title "Native SX Browser"
|
||||
@@ -1,6 +1,5 @@
|
||||
;; Pretext island — effect as let binding
|
||||
(defisland
|
||||
~pretext-demo/live
|
||||
()
|
||||
(let
|
||||
((words (split "In the beginning was the Word, and the Word was with God, and the Word was God. The same was in the beginning with God. All things were made by him; and without him was not any thing made that was made. In him was life; and the life was the light of men." " "))
|
||||
49
sx/sx/applications/pretext/_islands/render-paragraph.sx
Normal file
49
sx/sx/applications/pretext/_islands/render-paragraph.sx
Normal file
@@ -0,0 +1,49 @@
|
||||
;; Pretext demo — DOM-free text layout
|
||||
;;
|
||||
;; Visual-first: shows typeset text, then explains how.
|
||||
;; Uses measure-text (perform) for glyph measurement.
|
||||
;; Compute positioned word data for one line.
|
||||
(defcomp
|
||||
(&key lines max-width line-height n-words label)
|
||||
(let
|
||||
((lh (or line-height 24)) (n-lines (len lines)))
|
||||
(div
|
||||
:class "relative rounded-lg border border-stone-200 bg-white overflow-hidden"
|
||||
(when
|
||||
label
|
||||
(div
|
||||
:class "px-4 pt-3 pb-1"
|
||||
(span
|
||||
:class "text-xs font-medium uppercase tracking-wide text-stone-400"
|
||||
label)))
|
||||
(div
|
||||
:style (str
|
||||
"position:relative;height:"
|
||||
(* n-lines lh)
|
||||
"px;padding:12px 16px;")
|
||||
(map
|
||||
(fn
|
||||
(line)
|
||||
(let
|
||||
((y (get line :y)))
|
||||
(map
|
||||
(fn
|
||||
(pw)
|
||||
(span
|
||||
:style (str
|
||||
"position:absolute;left:"
|
||||
(+ (get pw :x) 16)
|
||||
"px;top:"
|
||||
(+ y 12)
|
||||
"px;font-size:15px;line-height:"
|
||||
lh
|
||||
"px;white-space:nowrap;")
|
||||
(get pw :word)))
|
||||
(get line :words))))
|
||||
lines))
|
||||
(div
|
||||
:class "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between"
|
||||
(span
|
||||
:class "text-xs text-stone-400"
|
||||
(str n-lines " lines, " n-words " words"))
|
||||
(span :class "text-xs text-stone-400" (str "width: " max-width "px"))))))
|
||||
@@ -1,58 +1,5 @@
|
||||
;; Pretext demo — DOM-free text layout
|
||||
;;
|
||||
;; Visual-first: shows typeset text, then explains how.
|
||||
;; Uses measure-text (perform) for glyph measurement.
|
||||
|
||||
;; Compute positioned word data for one line.
|
||||
(defcomp
|
||||
~pretext-demo/render-paragraph
|
||||
(&key lines max-width line-height n-words label)
|
||||
(let
|
||||
((lh (or line-height 24)) (n-lines (len lines)))
|
||||
(div
|
||||
:class "relative rounded-lg border border-stone-200 bg-white overflow-hidden"
|
||||
(when
|
||||
label
|
||||
(div
|
||||
:class "px-4 pt-3 pb-1"
|
||||
(span
|
||||
:class "text-xs font-medium uppercase tracking-wide text-stone-400"
|
||||
label)))
|
||||
(div
|
||||
:style (str
|
||||
"position:relative;height:"
|
||||
(* n-lines lh)
|
||||
"px;padding:12px 16px;")
|
||||
(map
|
||||
(fn
|
||||
(line)
|
||||
(let
|
||||
((y (get line :y)))
|
||||
(map
|
||||
(fn
|
||||
(pw)
|
||||
(span
|
||||
:style (str
|
||||
"position:absolute;left:"
|
||||
(+ (get pw :x) 16)
|
||||
"px;top:"
|
||||
(+ y 12)
|
||||
"px;font-size:15px;line-height:"
|
||||
lh
|
||||
"px;white-space:nowrap;")
|
||||
(get pw :word)))
|
||||
(get line :words))))
|
||||
lines))
|
||||
(div
|
||||
:class "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between"
|
||||
(span
|
||||
:class "text-xs text-stone-400"
|
||||
(str n-lines " lines, " n-words " words"))
|
||||
(span :class "text-xs text-stone-400" (str "width: " max-width "px"))))))
|
||||
|
||||
;; Compute all positioned lines for a paragraph.
|
||||
(defcomp
|
||||
~pretext-demo/content
|
||||
()
|
||||
(let
|
||||
((sample-words (split "In the beginning was the Word, and the Word was with God, and the Word was God. The same was in the beginning with God. All things were made by him; and without him was not any thing made that was made. In him was life; and the life was the light of men." " "))
|
||||
@@ -261,4 +208,4 @@
|
||||
(code "canvas.measureText")
|
||||
" gives pixel-perfect metrics — the browser that measures is the browser that renders. "
|
||||
"Drag the sliders to re-layout in real time.")
|
||||
(div :data-sx-island "pretext-demo/live" "")))))))
|
||||
(div :data-sx-island "pretext-demo/live" "")))))))
|
||||
10
sx/sx/applications/protocol/activitypub/index.sx
Normal file
10
sx/sx/applications/protocol/activitypub/index.sx
Normal file
@@ -0,0 +1,10 @@
|
||||
(defcomp ()
|
||||
(~docs/page :title "ActivityPub"
|
||||
(~docs/note
|
||||
(p "Honest note: ActivityPub wire format is JSON-LD, not sx. This documents how AP integrates with the sx rendering layer."))
|
||||
(~docs/section :title "AP activities" :id "activities"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Rose Ash services communicate cross-domain writes via ActivityPub-shaped activities. Each service has a virtual actor. Activities are JSON-LD objects sent to /internal/inbox endpoints. RSA signatures authenticate the sender."))
|
||||
(~docs/section :title "Event bus" :id "bus"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The event bus dispatches activities to registered handlers. Handlers are async functions that process the activity and may trigger side effects. The bus runs as a background processor in each service."))))
|
||||
14
sx/sx/applications/protocol/fragments/index.sx
Normal file
14
sx/sx/applications/protocol/fragments/index.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
(defcomp ()
|
||||
(~docs/page :title "Cross-Service Fragments"
|
||||
(~docs/section :title "Fragment protocol" :id "protocol"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments that other services compose into their pages. Fragment endpoints return text/sx or text/html.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The frag resolver is an I/O primitive in the render tree:")
|
||||
(~docs/code :src (highlight "(frag \"blog\" \"link-card\" :slug \"hello\")" "lisp")))
|
||||
(~docs/section :title "SxExpr wrapping" :id "wrapping"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly in the render tree. When it returns text/html, it's wrapped in a ~rich-text component that inserts the HTML via raw!. This allows transparent composition across service boundaries."))
|
||||
(~docs/section :title "fetch_fragments()" :id "fetch"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The Python helper fetch_fragments() fetches multiple fragments in parallel via asyncio.gather(). Fragments are cached in Redis with short TTLs. Each fragment request is HMAC-signed for authentication."))))
|
||||
16
sx/sx/applications/protocol/future/index.sx
Normal file
16
sx/sx/applications/protocol/future/index.sx
Normal file
@@ -0,0 +1,16 @@
|
||||
(defcomp ()
|
||||
(~docs/page :title "Future Possibilities"
|
||||
(~docs/note
|
||||
(p "This page is speculative. Nothing here is implemented. It documents ideas that may or may not happen."))
|
||||
(~docs/section :title "Custom protocol schemes" :id "schemes"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx:// and sxs:// as custom URI schemes for content addressing or deep linking. An sx:// URI could resolve to an sx expression from a federated registry. This is technically feasible but practically unnecessary for a single-site deployment."))
|
||||
(~docs/section :title "Sx as AP serialization" :id "ap-sx"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"ActivityPub objects could be serialized as s-expressions instead of JSON-LD. S-expressions are more compact and easier to parse. The practical barrier: the entire AP ecosystem expects JSON-LD."))
|
||||
(~docs/section :title "Sx-native federation" :id "federation"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Federated services could exchange sx fragments directly — render a remote user's profile card by fetching its sx source from their server. This requires trust and standardization that doesn't exist yet."))
|
||||
(~docs/section :title "Realistic assessment" :id "realistic"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The most likely near-term improvement is sx:// deep linking for client-side component resolution. Everything else requires ecosystem adoption that one project can't drive alone."))))
|
||||
23
sx/sx/applications/protocol/index.sx
Normal file
23
sx/sx/applications/protocol/index.sx
Normal file
@@ -0,0 +1,23 @@
|
||||
;; Protocol documentation pages — fully self-contained
|
||||
(defcomp ()
|
||||
(~docs/page :title "Wire Format"
|
||||
(~docs/section :title "The text/sx content type" :id "content-type"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx responses use content type text/sx. The body is s-expression source code. The client parses and evaluates it, then renders the result into the DOM.")
|
||||
(~docs/code :src (highlight "HTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: a1b2c3d4\n\n(div :class \"p-4\"\n (~card :title \"Hello\"))" "bash")))
|
||||
(~docs/section :title "Request lifecycle" :id "lifecycle"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"1. User interacts with an element that has sx-get/sx-post/etc.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"2. sx.js fires sx:beforeRequest, then sends the HTTP request with SX-Request: true header.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"3. Server builds s-expression tree, scans CSS classes, prepends missing component definitions.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"4. Client receives text/sx response, parses it, evaluates it, renders to DOM.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"5. sx.js fires sx:afterSwap and sx:afterSettle.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"6. Any sx-swap-oob elements are swapped into their targets elsewhere in the DOM."))
|
||||
(~docs/section :title "Component definitions" :id "components"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"On full page loads, component definitions are in <script type=\"text/sx\" data-components>. On subsequent sx requests, missing definitions are prepended to the response body. The client caches definitions in localStorage keyed by a content hash."))))
|
||||
14
sx/sx/applications/protocol/internal-services/index.sx
Normal file
14
sx/sx/applications/protocol/internal-services/index.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
(defcomp ()
|
||||
(~docs/page :title "Internal Services"
|
||||
(~docs/note
|
||||
(p "Honest note: the internal service protocol is JSON, not sx. Sx is the composition layer on top. The protocols below are the plumbing underneath."))
|
||||
(~docs/section :title "HMAC-signed HTTP" :id "hmac"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Services communicate via HMAC-signed HTTP requests with short timeouts:")
|
||||
(ul (~tw :tokens "space-y-2 text-stone-600 font-mono text-sm")
|
||||
(li "GET /internal/data/{query} — read data (3s timeout)")
|
||||
(li "POST /internal/actions/{action} — execute write (5s timeout)")
|
||||
(li "POST /internal/inbox — ActivityPub-shaped event delivery")))
|
||||
(~docs/section :title "fetch_data / call_action" :id "fetch"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Python helpers fetch_data() and call_action() handle HMAC signing, serialization, and error handling. They resolve service URLs from environment variables (INTERNAL_URL_BLOG, etc) and fall back to public URLs in development."))))
|
||||
15
sx/sx/applications/protocol/resolver-io/index.sx
Normal file
15
sx/sx/applications/protocol/resolver-io/index.sx
Normal file
@@ -0,0 +1,15 @@
|
||||
(defcomp ()
|
||||
(~docs/page :title "Resolver I/O"
|
||||
(~docs/section :title "Async I/O primitives" :id "primitives"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The sx resolver identifies I/O nodes in the render tree, groups them, executes them in parallel via asyncio.gather(), and substitutes results back in.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"I/O primitives:")
|
||||
(ul (~tw :tokens "space-y-2 text-stone-600")
|
||||
(li (span (~tw :tokens "font-mono text-violet-700") "frag") " — fetch a cross-service fragment")
|
||||
(li (span (~tw :tokens "font-mono text-violet-700") "query") " — read data from another service")
|
||||
(li (span (~tw :tokens "font-mono text-violet-700") "action") " — execute a write on another service")
|
||||
(li (span (~tw :tokens "font-mono text-violet-700") "current-user") " — resolve the current authenticated user")))
|
||||
(~docs/section :title "Execution model" :id "execution"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The render tree is walked to find I/O nodes. All nodes at the same depth are gathered and executed in parallel. Results replace the I/O nodes in the tree. The walk continues until no more I/O nodes are found. This typically completes in 1-2 passes."))))
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Main documentation page
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~sx-urls/urls-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "SX URLs"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"S-expression URLs — where the address of a thing and the thing itself are the same kind of thing.")
|
||||
@@ -1,5 +1,4 @@
|
||||
(defcomp
|
||||
~applications/sxtp/content
|
||||
()
|
||||
(~docs/page
|
||||
:title "SXTP Protocol"
|
||||
421
sx/sx/cssx.sx
421
sx/sx/cssx.sx
@@ -1,421 +0,0 @@
|
||||
;; CSSX — Styling as Components
|
||||
;; Documentation for the CSSX approach: no parallel style infrastructure,
|
||||
;; just defcomp components that decide how to style their children.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Overview
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/overview-content ()
|
||||
(~docs/page :title "CSSX Components"
|
||||
|
||||
(~docs/section :title "The Idea" :id "idea"
|
||||
(p (strong "Styling is just components.") " A CSSX component is a regular "
|
||||
(code "defcomp") " that decides how to style its children. It might apply "
|
||||
"Tailwind classes, or hand-written CSS classes, or inline styles, or generate "
|
||||
"rules at runtime. The implementation is the component's private business. "
|
||||
"The consumer just calls " (code "(~cssx/btn :variant \"primary\" \"Submit\")") " and doesn't care.")
|
||||
(p "Because it's " (code "defcomp") ", you get everything for free: caching, bundling, "
|
||||
"dependency scanning, server/client rendering, composition. No parallel infrastructure."))
|
||||
|
||||
(~docs/section :title "Why Not a Style Dictionary?" :id "why"
|
||||
(p "SX previously had a parallel CSS system: a style dictionary (JSON blob of "
|
||||
"atom-to-declaration mappings), a " (code "StyleValue") " type threaded through "
|
||||
"the evaluator and renderer, content-addressed hash class names (" (code "sx-a3f2b1")
|
||||
"), runtime CSS injection, and a separate caching pipeline (cookies, localStorage).")
|
||||
(p "This was ~3,000 lines of code across the spec, bootstrappers, and host implementations. "
|
||||
"It was never adopted. The codebase voted with its feet: " (code ":class") " strings "
|
||||
"with " (code "defcomp") " already covered every real use case.")
|
||||
(p "The result of that system: elements in the DOM got opaque class names like "
|
||||
(code "class=\"sx-a3f2b1\"") ". DevTools became useless. You couldn't inspect an "
|
||||
"element and understand its styling. " (strong "That was a deal breaker.")))
|
||||
|
||||
(~docs/section :title "Key Advantages" :id "advantages"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Readable DOM: ") "Elements have real class names, not content-addressed "
|
||||
"hashes. DevTools works.")
|
||||
(li (strong "Data-driven styling: ") "Components receive data and decide styling. "
|
||||
(code "(~cssx/metric :value 150)") " renders red because " (code "value > 100")
|
||||
" — logic lives in the component, not a CSS preprocessor.")
|
||||
(li (strong "One system: ") "No separate " (code "StyleValue") " type, no style "
|
||||
"dictionary JSON, no injection pipeline. Components ARE the styling abstraction.")
|
||||
(li (strong "One cache: ") "Component hash/localStorage handles everything. No "
|
||||
"separate style dict caching.")
|
||||
(li (strong "Composable: ") (code "(~card :elevated true (~cssx/metric :value v))")
|
||||
" — styling composes like any other component.")
|
||||
(li (strong "Strategy-agnostic: ") "A component can apply Tailwind classes, emit "
|
||||
(code "<style>") " blocks, use inline styles, generate CSS custom properties, or "
|
||||
"any combination. Swap strategies without touching call sites.")))
|
||||
|
||||
(~docs/section :title "What Changed" :id "changes"
|
||||
(~docs/subsection :title "Removed (~3,000 lines)"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-600 text-sm")
|
||||
(li (code "StyleValue") " type and all plumbing (type checks in eval, render, serialize)")
|
||||
(li (code "cssx.sx") " spec module (resolve-style, resolve-atom, split-variant, hash, injection)")
|
||||
(li "Style dictionary JSON format, loading, caching (" (code "<script type=\"text/sx-styles\">") ", localStorage)")
|
||||
(li (code "style_dict.py") " (782 lines) and " (code "style_resolver.py") " (254 lines)")
|
||||
(li (code "css") " and " (code "merge-styles") " primitives")
|
||||
(li "Platform interface: " (code "fnv1a-hash") ", " (code "compile-regex") ", " (code "make-style-value") ", " (code "inject-style-value"))
|
||||
(li (code "defkeyframes") " special form")
|
||||
(li "Style dict cookies and localStorage keys")))
|
||||
|
||||
(~docs/subsection :title "Kept"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-600 text-sm")
|
||||
(li (code "defstyle") " — simplified to bind any value (string, function, etc.)")
|
||||
(li (code "tw.css") " — the compiled Tailwind stylesheet, delivered via CSS class tracking")
|
||||
(li (code ":class") " attribute — just takes strings, no special-casing")
|
||||
(li "CSS class delivery (" (code "SX-Css") " headers, " (code "<style id=\"sx-css\">") ")")
|
||||
(li "All component infrastructure (defcomp, caching, bundling, deps)")))
|
||||
|
||||
(~docs/subsection :title "Added"
|
||||
(p "Nothing. CSSX components are just " (code "defcomp") ". The only new thing is "
|
||||
"a convention: components whose primary purpose is styling.")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Patterns
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/patterns-content ()
|
||||
(~docs/page :title "Patterns"
|
||||
|
||||
(~docs/section :title "Class Mapping" :id "class-mapping"
|
||||
(p "The simplest pattern: a component that maps semantic keywords to class strings.")
|
||||
(highlight
|
||||
"(defcomp ~cssx/btn (&key variant disabled &rest children)\n (button\n :class (str \"px-4 py-2 rounded font-medium transition \"\n (case variant\n \"primary\" \"bg-blue-600 text-white hover:bg-blue-700\"\n \"danger\" \"bg-red-600 text-white hover:bg-red-700\"\n \"ghost\" \"bg-transparent hover:bg-stone-100\"\n \"bg-stone-200 hover:bg-stone-300\")\n (when disabled \" opacity-50 cursor-not-allowed\"))\n :disabled disabled\n children))"
|
||||
"lisp")
|
||||
(p "Consumers call " (code "(~cssx/btn :variant \"primary\" \"Submit\")") ". The Tailwind "
|
||||
"classes are readable in DevTools but never repeated across call sites."))
|
||||
|
||||
(~docs/section :title "Data-Driven Styling" :id "data-driven"
|
||||
(p "Styling that responds to data values — impossible with static CSS:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/metric (&key value label threshold)\n (let ((t (or threshold 10)))\n (div :class (str \"p-3 rounded font-bold \"\n (cond\n ((> value (* t 10)) \"bg-red-500 text-white\")\n ((> value t) \"bg-amber-200 text-amber-900\")\n (:else \"bg-green-100 text-green-800\")))\n (span :class \"text-sm\" label) \": \" (span (str value)))))"
|
||||
"lisp")
|
||||
(p "The component makes a " (em "decision") " about styling based on data. "
|
||||
"No CSS preprocessor or class name convention can express \"red when value > 100\"."))
|
||||
|
||||
(~docs/section :title "Style Functions" :id "style-functions"
|
||||
(p "Reusable style logic that returns class strings — no wrapping element needed:")
|
||||
(highlight
|
||||
"(define card-classes\n (fn (&key elevated bordered)\n (str \"rounded-lg p-4 \"\n (if elevated \"shadow-lg\" \"shadow-sm\")\n (when bordered \" border border-stone-200\"))))\n\n;; Usage:\n(div :class (card-classes :elevated true) ...)\n(article :class (card-classes :bordered true) ...)"
|
||||
"lisp")
|
||||
(p "Or with " (code "defstyle") " for named bindings:")
|
||||
(highlight
|
||||
"(defstyle card-base \"rounded-lg p-4 shadow-sm\")\n(defstyle card-elevated \"rounded-lg p-4 shadow-lg\")\n\n(div :class card-base ...)"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "Responsive Layouts" :id "responsive"
|
||||
(p "Components that encode responsive breakpoints:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/responsive-grid (&key cols &rest children)\n (div :class (str \"grid gap-4 \"\n (case (or cols 3)\n 1 \"grid-cols-1\"\n 2 \"grid-cols-1 md:grid-cols-2\"\n 3 \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n 4 \"grid-cols-2 md:grid-cols-3 lg:grid-cols-4\"))\n children))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "Emitting CSS Directly" :id "emitting-css"
|
||||
(p "Components are not limited to referencing existing classes. They can generate "
|
||||
"CSS — " (code "<style>") " tags, keyframes, custom properties — as part of their output:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/pulse (&key color duration &rest children)\n (<>\n (style (str \"@keyframes sx-pulse {\"\n \"0%,100% { opacity:1 } 50% { opacity:.5 } }\"))\n (div :style (str \"animation: sx-pulse \" (or duration \"2s\") \" infinite;\"\n \"color:\" (or color \"inherit\"))\n children)))"
|
||||
"lisp")
|
||||
(highlight
|
||||
"(defcomp ~cssx/theme (&key primary surface &rest children)\n (<>\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\"))\n children))"
|
||||
"lisp")
|
||||
(p "The CSS strategy is the component's private implementation detail. Consumers call "
|
||||
(code "(~cssx/pulse :color \"red\" \"Loading...\")") " or "
|
||||
(code "(~cssx/theme :primary \"#2563eb\" ...)") " without knowing or caring whether the "
|
||||
"component uses classes, inline styles, generated rules, or all three."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Async CSS
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/async-content ()
|
||||
(~docs/page :title "Async CSS"
|
||||
|
||||
(~docs/section :title "The Pattern" :id "pattern"
|
||||
(p "A CSSX component that needs CSS it doesn't have yet can "
|
||||
(strong "fetch and cache it before rendering") ". This is just "
|
||||
(code "~shared:pages/suspense") " combined with a style component — no new infrastructure:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~shared:pages/suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))"
|
||||
"lisp")
|
||||
(p "The consumer never knows:")
|
||||
(highlight
|
||||
"(~cssx/styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "Use Cases" :id "use-cases"
|
||||
(~docs/subsection :title "Federated Components"
|
||||
(p "A " (code "~cssx/btn") " from another site arrives via IPFS with a CID pointing "
|
||||
"to its CSS. The component fetches and caches it before rendering. "
|
||||
"No coordination needed between sites.")
|
||||
(highlight
|
||||
"(defcomp ~cssx/federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~shared:pages/suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/subsection :title "Heavy UI Libraries"
|
||||
(p "Code editors, chart libraries, rich text editors — their CSS only loads "
|
||||
"when the component actually appears on screen:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/code-editor (&key language value on-change)\n (~cssx/styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/subsection :title "Lazy Themes"
|
||||
(p "Theme CSS loads on first use, then is instant on subsequent visits:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~cssx/styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))"
|
||||
"lisp")))
|
||||
|
||||
(~docs/section :title "How It Composes" :id "composition"
|
||||
(p "Async CSS composes with everything already in SX:")
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700")
|
||||
(li (code "~shared:pages/suspense") " handles the async gap with fallback content")
|
||||
(li "localStorage handles caching across sessions")
|
||||
(li (code "<style id=\"sx-css\">") " is the injection target (same as CSS class delivery)")
|
||||
(li "Component content-hashing tracks what the client has")
|
||||
(li "No new types, no new delivery protocol, no new spec code")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Live Styles
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/live-content ()
|
||||
(~docs/page :title "Live Styles"
|
||||
|
||||
(~docs/section :title "Styles That Respond to Events" :id "concept"
|
||||
(p "Combine " (code "~live") " (SSE) or " (code "~ws") " (WebSocket) with style "
|
||||
"components, and you get styles that change in real-time in response to server "
|
||||
"events. No new infrastructure — just components receiving data through a "
|
||||
"persistent transport."))
|
||||
|
||||
(~docs/section :title "SSE: Live Theme Updates" :id "sse-theme"
|
||||
(p "A " (code "~live") " component declares a persistent connection to an SSE "
|
||||
"endpoint. When the server pushes a new event, " (code "resolveSuspense")
|
||||
" replaces the content:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/brand\"\n (~shared:pages/suspense :id \"theme\"\n (~cssx/theme :primary \"#7c3aed\" :surface \"#fafaf9\")))"
|
||||
"lisp")
|
||||
(p "Server pushes a new theme:")
|
||||
(highlight
|
||||
"event: sx-resolve\ndata: {\"id\": \"theme\", \"sx\": \"(~cssx/theme :primary \\\"#2563eb\\\" :surface \\\"#1e1e2e\\\")\"}"
|
||||
"text")
|
||||
(p "The " (code "~cssx/theme") " component emits CSS custom properties. Everything "
|
||||
"using " (code "var(--color-primary)") " repaints instantly:")
|
||||
(highlight
|
||||
"(defcomp ~cssx/theme (&key primary surface)\n (style (str \":root {\"\n \"--color-primary:\" (or primary \"#7c3aed\") \";\"\n \"--color-surface:\" (or surface \"#fafaf9\") \"}\")))"
|
||||
"lisp"))
|
||||
|
||||
(~docs/section :title "SSE: Live Dashboard Metrics" :id "sse-metrics"
|
||||
(p "Style changes driven by live data — the component decides the visual treatment:")
|
||||
(highlight
|
||||
"(~live :src \"/api/stream/dashboard\"\n (~shared:pages/suspense :id \"cpu\"\n (~cssx/metric :value 0 :label \"CPU\" :threshold 80))\n (~shared:pages/suspense :id \"memory\"\n (~cssx/metric :value 0 :label \"Memory\" :threshold 90))\n (~shared:pages/suspense :id \"requests\"\n (~cssx/metric :value 0 :label \"RPS\" :threshold 1000)))"
|
||||
"lisp")
|
||||
(p "Server pushes updated values. " (code "~cssx/metric") " turns red when "
|
||||
(code "value > threshold") " — the styling logic lives in the component, "
|
||||
"not in CSS selectors or JavaScript event handlers."))
|
||||
|
||||
(~docs/section :title "WebSocket: Collaborative Design" :id "ws-design"
|
||||
(p "Bidirectional channel for real-time collaboration. A designer adjusts a color, "
|
||||
"all connected clients see the change:")
|
||||
(highlight
|
||||
"(~ws :src \"/ws/design-studio\"\n (~shared:pages/suspense :id \"canvas-theme\"\n (~cssx/theme :primary \"#7c3aed\")))"
|
||||
"lisp")
|
||||
(p "Client sends a color change:")
|
||||
(highlight
|
||||
";; Designer picks a new primary color\n(sx-send ws-conn '(theme-update :primary \"#dc2626\"))"
|
||||
"lisp")
|
||||
(p "Server broadcasts to all connected clients via " (code "sx-resolve") " — "
|
||||
"every client's " (code "~cssx/theme") " component re-renders with the new color."))
|
||||
|
||||
(~docs/section :title "Why This Works" :id "why"
|
||||
(p "Every one of these patterns is just a " (code "defcomp") " receiving data "
|
||||
"through a persistent transport. The styling strategy — CSS custom properties, "
|
||||
"class swaps, inline styles, " (code "<style>") " blocks — is the component's "
|
||||
"private business. The transport doesn't know or care.")
|
||||
(p "A parallel style system would have needed its own streaming, its own caching, "
|
||||
"its own delta protocol for each of these use cases — duplicating what components "
|
||||
"already do.")
|
||||
(p (~tw :tokens "mt-4 text-stone-500 italic")
|
||||
"Note: ~live and ~ws are planned (see Live Streaming). The patterns shown here "
|
||||
"will work as described once the streaming transport is implemented. The component "
|
||||
"and suspense infrastructure they depend on already exists."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Comparison with CSS Technologies
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/comparison-content ()
|
||||
(~docs/page :title "Comparisons"
|
||||
|
||||
(~docs/section :title "styled-components / Emotion" :id "styled-components"
|
||||
(p (a :href "https://styled-components.com" (~tw :tokens "text-violet-600 hover:underline") "styled-components")
|
||||
" pioneered the idea that styling belongs in components. But it generates CSS "
|
||||
"at runtime, injects " (code "<style>") " tags, and produces opaque hashed class "
|
||||
"names (" (code "class=\"sc-bdfBwQ fNMpVx\"") "). Open DevTools and you see gibberish. "
|
||||
"It also carries significant runtime cost — parsing CSS template literals, hashing, "
|
||||
"deduplicating — and needs a separate SSR extraction step (" (code "ServerStyleSheet") ").")
|
||||
(p "CSSX components share the core insight (" (em "styling is a component concern")
|
||||
") but without the runtime machinery. When a component applies Tailwind classes, "
|
||||
"there's zero CSS generation overhead. When it does emit " (code "<style>")
|
||||
" blocks, it's explicit — not hidden behind a tagged template literal. "
|
||||
"And the DOM is always readable."))
|
||||
|
||||
(~docs/section :title "CSS Modules" :id "css-modules"
|
||||
(p (a :href "https://github.com/css-modules/css-modules" (~tw :tokens "text-violet-600 hover:underline") "CSS Modules")
|
||||
" scope class names to avoid collisions by rewriting them at build time: "
|
||||
(code ".button") " becomes " (code ".button_abc123")
|
||||
". This solves the global namespace problem but creates the same opacity issue — "
|
||||
"hashed names in the DOM that you can't grep for or reason about.")
|
||||
(p "CSSX components don't need scoping because component boundaries already provide "
|
||||
"isolation. A " (code "~cssx/btn") " owns its markup. There's nothing to collide with."))
|
||||
|
||||
(~docs/section :title "Tailwind CSS" :id "tailwind"
|
||||
(p "Tailwind is " (em "complementary") ", not competitive. CSSX components are the "
|
||||
"semantic layer on top. Raw Tailwind in markup — "
|
||||
(code ":class \"px-4 py-2 bg-blue-600 text-white font-medium rounded hover:bg-blue-700\"")
|
||||
" — is powerful but verbose and duplicated across call sites.")
|
||||
(p "A CSSX component wraps that string once: " (code "(~cssx/btn :variant \"primary\" \"Submit\")")
|
||||
". The Tailwind classes are still there, readable in DevTools, but consumers don't "
|
||||
"repeat them. This is the same pattern Tailwind's own docs recommend ("
|
||||
(em "\"extracting components\"") ") — CSSX components are just SX's native way of doing it."))
|
||||
|
||||
(~docs/section :title "Vanilla Extract" :id "vanilla-extract"
|
||||
(p (a :href "https://vanilla-extract.style" (~tw :tokens "text-violet-600 hover:underline") "Vanilla Extract")
|
||||
" is zero-runtime CSS-in-JS: styles are written in TypeScript, compiled to static "
|
||||
"CSS at build time, and referenced by generated class names. It avoids the runtime "
|
||||
"cost of styled-components but still requires a build step, a bundler plugin, and "
|
||||
"TypeScript. The generated class names are again opaque.")
|
||||
(p "CSSX components need no build step for styling — they're evaluated at render time "
|
||||
"like any other component. And since the component chooses its own strategy, it can "
|
||||
"reference pre-built classes (zero runtime) " (em "or") " generate CSS on the fly — "
|
||||
"same API either way."))
|
||||
|
||||
(~docs/section :title "Design Tokens / Style Dictionary" :id "design-tokens"
|
||||
(p "The " (a :href "https://amzn.github.io/style-dictionary/" (~tw :tokens "text-violet-600 hover:underline") "Style Dictionary")
|
||||
" pattern — a JSON/YAML file mapping token names to values, compiled to "
|
||||
"platform-specific output — is essentially what the old CSSX was. It's the "
|
||||
"industry standard for design systems.")
|
||||
(p "The problem is that it's a parallel system: separate file format, separate build "
|
||||
"pipeline, separate caching, separate tooling. CSSX components eliminate all of "
|
||||
"that by expressing tokens as component parameters: "
|
||||
(code "(~cssx/theme :primary \"#7c3aed\")") " instead of "
|
||||
(code "{\"color\": {\"primary\": {\"value\": \"#7c3aed\"}}}")
|
||||
". Same result, no parallel infrastructure."))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Philosophy
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/philosophy-content ()
|
||||
(~docs/page :title "Philosophy"
|
||||
|
||||
(~docs/section :title "The Collapse" :id "collapse"
|
||||
(p "The web has spent two decades building increasingly complex CSS tooling: "
|
||||
"preprocessors, CSS-in-JS, atomic CSS, utility frameworks, design tokens, style "
|
||||
"dictionaries. Each solves a real problem but adds a new system with its own "
|
||||
"caching, bundling, and mental model.")
|
||||
(p "CSSX components collapse all of this back to the simplest possible thing: "
|
||||
(strong "a function that takes data and returns markup with classes.")
|
||||
" That's what a component already is. There is no separate styling system because "
|
||||
"there doesn't need to be."))
|
||||
|
||||
(~docs/section :title "Proof by Deletion" :id "proof"
|
||||
(p "The strongest validation: we built the full parallel system — style dictionary, "
|
||||
"StyleValue type, content-addressed hashing, runtime injection, localStorage "
|
||||
"caching — and then deleted it because nobody used it. The codebase already had "
|
||||
"the answer: " (code "defcomp") " with " (code ":class") " strings.")
|
||||
(p "3,000 lines of infrastructure removed. Zero lines added. Every use case still works."))
|
||||
|
||||
(~docs/section :title "The Right Abstraction Level" :id "abstraction"
|
||||
(p "CSS-in-JS puts styling " (em "below") " components — you style elements, then compose "
|
||||
"them. Utility CSS puts styling " (em "beside") " components — classes in markup, logic "
|
||||
"elsewhere. Both create a seam between what something does and how it looks.")
|
||||
(p "CSSX components put styling " (em "inside") " components — at the same level as "
|
||||
"structure and behavior. A " (code "~cssx/metric") " component knows its own thresholds, "
|
||||
"its own color scheme, its own responsive behavior. Styling is just another "
|
||||
"decision the component makes, not a separate concern."))
|
||||
|
||||
(~docs/section :title "Relationship to Other Plans" :id "relationships"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Content-Addressed Components: ") "CSSX components get CIDs like any "
|
||||
"other component. A " (code "~cssx/btn") " from one site can be shared to another via "
|
||||
"IPFS. No " (code ":css-atoms") " manifest field needed — the component carries "
|
||||
"its own styling logic.")
|
||||
(li (strong "Isomorphic Rendering: ") "Components render the same on server and client. "
|
||||
"No style injection timing issues, no FOUC from late CSS loading.")
|
||||
(li (strong "Component Bundling: ") "deps.sx already handles transitive component deps. "
|
||||
"Style components are just more components in the bundle — no separate style bundling.")
|
||||
(li (strong "Live Streaming: ") "SSE and WebSocket transports push data to components. "
|
||||
"Style components react to that data like any other component — no separate style "
|
||||
"streaming protocol.")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CSS Delivery
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~cssx/delivery-content ()
|
||||
(~docs/page :title "CSS Delivery"
|
||||
|
||||
(~docs/section :title "Multiple Strategies" :id "strategies"
|
||||
(p "A CSSX component chooses its own styling strategy — and each strategy has its "
|
||||
"own delivery path:")
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700")
|
||||
(li (strong "Tailwind classes: ") "Delivered via the on-demand protocol below. "
|
||||
"The server ships only the rules the page actually uses.")
|
||||
(li (strong "Inline styles: ") "No delivery needed — " (code ":style") " attributes "
|
||||
"are part of the markup.")
|
||||
(li (strong "Emitted " (code "<style>") " blocks: ") "Components can emit CSS rules "
|
||||
"directly. They arrive as part of the rendered HTML — keyframes, custom properties, "
|
||||
"scoped rules, anything.")
|
||||
(li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files "
|
||||
"or lazy-load them via " (code "~shared:pages/suspense") " (see " (a :href "/sx/(applications.(cssx.async))" "Async CSS") ").")
|
||||
(li (strong "Custom properties: ") "A " (code "~cssx/theme") " component sets "
|
||||
(code "--color-primary") " etc. via a " (code "<style>") " block. Everything "
|
||||
"using " (code "var()") " repaints automatically."))
|
||||
(p "The protocol below handles the Tailwind utility class case — the most common "
|
||||
"strategy — but it's not the only game in town."))
|
||||
|
||||
(~docs/section :title "On-Demand Tailwind Delivery" :id "on-demand"
|
||||
(p "When components use Tailwind utility classes, SX ships " (em "only the CSS rules "
|
||||
"actually used on the page") ", computed at render time. No build step, no purging, "
|
||||
"no unused CSS.")
|
||||
(p "The server pre-parses the full Tailwind CSS file into an in-memory registry at "
|
||||
"startup. When a response is rendered, SX scans all " (code ":class") " values in "
|
||||
"the output, looks up only those classes, and embeds the matching rules."))
|
||||
|
||||
(~docs/section :title "The Protocol" :id "protocol"
|
||||
(p "First page load gets the full set of used rules. Subsequent navigations send a "
|
||||
"hash of what the client already has, and the server ships only the delta:")
|
||||
(highlight
|
||||
"# First page load:\nGET / HTTP/1.1\n\nHTTP/1.1 200 OK\nContent-Type: text/html\n# Full CSS in <style id=\"sx-css\"> + hash in <meta name=\"sx-css-classes\">\n\n# Subsequent navigation:\nGET /about HTTP/1.1\nSX-Css: a1b2c3d4\n\nHTTP/1.1 200 OK\nContent-Type: text/sx\nSX-Css-Hash: e5f6g7h8\nSX-Css-Add: bg-blue-500,text-white,rounded-lg\n# Only new rules in <style data-sx-css>"
|
||||
"bash")
|
||||
(p "The client merges new rules into the existing " (code "<style id=\"sx-css\">")
|
||||
" block and updates its hash. No flicker, no duplicate rules."))
|
||||
|
||||
(~docs/section :title "Component-Aware Scanning" :id "scanning"
|
||||
(p "CSS scanning happens at two levels:")
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700")
|
||||
(li (strong "Registration time: ") "When a component is defined via " (code "defcomp")
|
||||
", its body is scanned for class strings. The component records which CSS classes "
|
||||
"it uses.")
|
||||
(li (strong "Render time: ") "The rendered output is scanned for any classes the "
|
||||
"static scan missed — dynamically constructed class strings, conditional classes, etc."))
|
||||
(p "This means the CSS registry knows roughly what a page needs before rendering, "
|
||||
"and catches any stragglers after."))
|
||||
|
||||
(~docs/section :title "Trade-offs" :id "tradeoffs"
|
||||
(ul (~tw :tokens "list-disc pl-5 space-y-2 text-stone-700")
|
||||
(li (strong "Full Tailwind in memory: ") "The parsed CSS registry is ~4MB. This is "
|
||||
"a one-time startup cost per app instance.")
|
||||
(li (strong "Regex scanning: ") "Class detection uses regex, so dynamically "
|
||||
"constructed class names (e.g. " (code "(str \"bg-\" color \"-500\")") ") can be "
|
||||
"missed. Use complete class strings in " (code "case") "/" (code "cond") " branches.")
|
||||
(li (strong "No @apply: ") "Tailwind's " (code "@apply") " is a build-time feature. "
|
||||
"On-demand delivery works with utility classes directly.")
|
||||
(li (strong "Tailwind-shaped: ") "The registry parser understands Tailwind's naming "
|
||||
"conventions. Non-Tailwind CSS works via " (code "<style>") " blocks in components.")))))
|
||||
@@ -9,7 +9,7 @@
|
||||
;; "sx:route client+data" — cache miss, fetched from server
|
||||
;; "sx:route client+cache" — cache hit, rendered from cached data
|
||||
|
||||
(defcomp ~data-test/content (&key (server-time :as string) (items :as list) (phase :as string) (transport :as string))
|
||||
(defcomp (&key (server-time :as string) (items :as list) (phase :as string) (transport :as string))
|
||||
(div (~tw :tokens "space-y-8")
|
||||
(div (~tw :tokens "border-b border-stone-200 pb-6")
|
||||
(h1 (~tw :tokens "text-2xl font-bold text-stone-900") "Data Test")
|
||||
@@ -1,99 +0,0 @@
|
||||
;; Docs page content — fully self-contained, no Python intermediaries
|
||||
|
||||
(defcomp ~docs-content/home-content ()
|
||||
(div :id "main-content" (~tw :tokens "max-w-3xl mx-auto px-4 py-6")
|
||||
(~home/stepper)))
|
||||
|
||||
(defcomp ~docs-content/docs-introduction-content ()
|
||||
(~docs/page :title "Introduction"
|
||||
(~docs/section :title "What is sx?" :id "what"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx is an s-expression language for building web UIs. It combines htmx's server-first hypermedia approach with React's component model. The server sends s-expression source code over the wire. The client parses, evaluates, and renders it to DOM.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The same evaluator runs on both server (Python) and client (JavaScript). Components defined once render identically in both environments."))
|
||||
(~docs/section :title "Design decisions" :id "design"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"HTML elements are first-class: (div :class \"card\" (p \"hello\")) renders exactly what you'd expect. Components use defcomp with keyword parameters and optional children. The evaluator supports let bindings, conditionals, lambda, map/filter/reduce, and ~80 primitives.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx replaces the pattern of shipping a JS framework + build step + client-side router + state management library just to render some server data. For most applications, sx eliminates the need for JavaScript entirely — htmx attributes handle interactivity, hyperscript handles small behaviours, and the server handles everything else."))
|
||||
(~docs/section :title "What sx is not" :id "not"
|
||||
(ul (~tw :tokens "space-y-2 text-stone-600")
|
||||
(li "Not a general-purpose programming language — it's a UI rendering language")
|
||||
(li "Not a full Lisp — it has macros, TCO, and delimited continuations, but no full call/cc")
|
||||
(li "Not production-hardened at scale — it runs one website")))))
|
||||
|
||||
(defcomp ~docs-content/docs-getting-started-content ()
|
||||
(~docs/page :title "Getting Started"
|
||||
(~docs/section :title "Minimal example" :id "minimal"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"An sx response is s-expression source code with content type text/sx:")
|
||||
(~docs/code :src (highlight "(div :class \"p-4 bg-white rounded\"\n (h1 :class \"text-2xl font-bold\" \"Hello, world!\")\n (p \"This is rendered from an s-expression.\"))" "lisp"))
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Add sx-get to any element to make it fetch and render sx:"))
|
||||
(~docs/section :title "Hypermedia attributes" :id "attrs"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:")
|
||||
(~docs/code :src (highlight "(button\n :sx-get \"/api/data\"\n :sx-target \"#result\"\n :sx-swap \"innerHTML\"\n \"Load data\")" "lisp"))
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. The response is parsed as sx and rendered into the target element."))))
|
||||
|
||||
(defcomp ~docs-content/docs-components-content ()
|
||||
(~docs/page :title "Components"
|
||||
(~docs/section :title "defcomp" :id "defcomp"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Components are defined with defcomp. They take keyword parameters and optional children:")
|
||||
(~docs/code :src (highlight "(defcomp ~docs-content/card (&key title subtitle &rest children)\n (div :class \"border rounded p-4\"\n (h2 :class \"font-bold\" title)\n (when subtitle (p :class \"text-stone-500\" subtitle))\n (div :class \"mt-3\" children)))" "lisp"))
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Use components with the ~ prefix:")
|
||||
(~docs/code :src (highlight "(~docs-content/card :title \"My Card\" :subtitle \"A description\"\n (p \"First child\")\n (p \"Second child\"))" "lisp")))
|
||||
(~docs/section :title "Component caching" :id "caching"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Component definitions are sent in a <script type=\"text/sx\" data-components> block. The client caches them in localStorage keyed by a content hash. On subsequent page loads, the client sends an SX-Components header listing what it has. The server only sends definitions the client is missing.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"This means the first page load sends all component definitions (~5-15KB). Subsequent navigations send zero component bytes — just the page content."))
|
||||
(~docs/section :title "Parameters" :id "params"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"&key declares keyword parameters. &rest children captures remaining positional arguments. Missing parameters evaluate to nil. Components always receive all declared parameters — use (when param ...) or (if param ... ...) to handle optional values."))))
|
||||
|
||||
(defcomp ~docs-content/docs-evaluator-content ()
|
||||
(~docs/page :title "Evaluator"
|
||||
(~docs/section :title "Special forms" :id "special"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Special forms have lazy evaluation — arguments are not evaluated before the form runs:")
|
||||
(~docs/code :src (highlight ";; Conditionals\n(if condition then-expr else-expr)\n(when condition body...)\n(cond (test1 body1) (test2 body2) (else default))\n\n;; Bindings\n(let ((name value) (name2 value2)) body...)\n(define name value)\n\n;; Functions\n(lambda (x y) (+ x y))\n(fn (x) (* x x))\n\n;; Sequencing\n(do expr1 expr2 expr3)\n(begin expr1 expr2)\n\n;; Threading\n(-> value (fn1 arg) (fn2 arg))" "lisp")))
|
||||
(~docs/section :title "Higher-order forms" :id "higher"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"These operate on collections with function arguments:")
|
||||
(~docs/code :src (highlight "(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true" "lisp")))))
|
||||
|
||||
(defcomp ~docs-content/docs-primitives-content (&key prims)
|
||||
(~docs/page :title "Primitives"
|
||||
(~docs/section :title "Built-in functions" :id "builtins"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx provides ~80 built-in pure functions. They work identically on server (Python) and client (JavaScript).")
|
||||
(div (~tw :tokens "space-y-6") prims))))
|
||||
|
||||
(defcomp ~docs-content/docs-special-forms-content (&key forms)
|
||||
(~docs/page :title "Special Forms"
|
||||
(~docs/section :title "Syntactic constructs" :id "special-forms"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Special forms are syntactic constructs whose arguments are NOT evaluated before dispatch. Each form has its own evaluation rules — unlike primitives, which receive pre-evaluated values. Together with primitives, special forms define the complete language surface.")
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"Forms marked with a tail position enable " (a :href "/sx/(etc.(essay.tail-call-optimization))" (~tw :tokens "text-violet-600 hover:underline") "tail-call optimization") " — recursive calls in tail position use constant stack space.")
|
||||
(div (~tw :tokens "space-y-10") forms))))
|
||||
|
||||
(defcomp ~docs-content/docs-server-rendering-content ()
|
||||
(~docs/page :title "Server Rendering"
|
||||
(~docs/section :title "Python API" :id "python"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"The server-side sx library provides several entry points for rendering:")
|
||||
(~docs/code :src (highlight "from shared.sx.helpers import sx_page, sx_response, sx_call\nfrom shared.sx.parser import SxExpr\n\n# Build a component call from Python kwargs\nsx_call(\"card\", title=\"Hello\", subtitle=\"World\")\n\n# Return an sx wire-format response\nreturn sx_response(sx_call(\"card\", title=\"Hello\"))\n\n# Return a full HTML page shell with sx boot\nreturn sx_page(ctx, page_sx)" "python")))
|
||||
(~docs/section :title "sx_call" :id "sx-call"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx_call converts Python kwargs to an s-expression component call. Snake_case becomes kebab-case. SxExpr values are inlined without quoting. None becomes nil. Bools become true/false."))
|
||||
(~docs/section :title "sx_response" :id "sx-response"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx_response returns a Quart Response with content type text/sx. It prepends missing component definitions, scans for CSS classes, and sets SX-Css-Hash and SX-Css-Add headers."))
|
||||
(~docs/section :title "sx_page" :id "sx-page"
|
||||
(p (~tw :tokens "text-stone-600")
|
||||
"sx_page returns a minimal HTML document that boots the page from sx source. The browser loads component definitions and page sx from inline <script> tags, then sx.js renders everything client-side. CSS rules are pre-scanned and injected."))))
|
||||
274
sx/sx/docs.sx
274
sx/sx/docs.sx
@@ -1,274 +0,0 @@
|
||||
(defcomp
|
||||
~docs/placeholder
|
||||
(&key (id :as string))
|
||||
(div
|
||||
:id id
|
||||
(div
|
||||
(~tw :tokens "bg-stone-100 rounded p-4 mt-3")
|
||||
(p
|
||||
(~tw :tokens "text-stone-400 italic text-sm")
|
||||
"Trigger the demo to see the actual content."))))
|
||||
|
||||
(defcomp
|
||||
~docs/oob-code
|
||||
(&key (target-id :as string) (text :as string))
|
||||
(div
|
||||
:id target-id
|
||||
:sx-swap-oob "innerHTML"
|
||||
(div
|
||||
(~tw :tokens "not-prose bg-stone-100 rounded p-4 mt-3")
|
||||
(pre (~tw :tokens "text-sm whitespace-pre-wrap break-words") (code text)))))
|
||||
|
||||
(defcomp
|
||||
~docs/attr-table
|
||||
(&key (title :as string) rows)
|
||||
(div
|
||||
(~tw :tokens "space-y-3")
|
||||
(h3 (~tw :tokens "text-xl font-semibold text-stone-700") title)
|
||||
(div
|
||||
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||
(table
|
||||
(~tw :tokens "w-full text-left text-sm")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Attribute")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Description")
|
||||
(th
|
||||
(~tw :tokens "px-3 py-2 font-medium text-stone-600 text-center w-20")
|
||||
"In sx?")))
|
||||
(tbody rows)))))
|
||||
|
||||
(defcomp
|
||||
~docs/headers-table
|
||||
(&key (title :as string) rows)
|
||||
(div
|
||||
(~tw :tokens "space-y-3")
|
||||
(h3 (~tw :tokens "text-xl font-semibold text-stone-700") title)
|
||||
(div
|
||||
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||
(table
|
||||
(~tw :tokens "w-full text-left text-sm")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Header")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Value")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Description")))
|
||||
(tbody rows)))))
|
||||
|
||||
(defcomp
|
||||
~docs/headers-row
|
||||
(&key
|
||||
(name :as string)
|
||||
(value :as string)
|
||||
(description :as string)
|
||||
(href :as string?))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-100")
|
||||
(td
|
||||
(~tw :tokens "px-3 py-2 font-mono text-sm whitespace-nowrap")
|
||||
(if
|
||||
href
|
||||
(a
|
||||
:href href
|
||||
:sx-get href
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
(~tw :tokens "text-violet-700 hover:text-violet-900 underline")
|
||||
name)
|
||||
(span (~tw :tokens "text-violet-700") name)))
|
||||
(td (~tw :tokens "px-3 py-2 font-mono text-sm text-stone-500") value)
|
||||
(td (~tw :tokens "px-3 py-2 text-stone-700 text-sm") description)))
|
||||
|
||||
(defcomp
|
||||
~docs/two-col-row
|
||||
(&key (name :as string) (description :as string) (href :as string?))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-100")
|
||||
(td
|
||||
(~tw :tokens "px-3 py-2 font-mono text-sm whitespace-nowrap")
|
||||
(if
|
||||
href
|
||||
(a
|
||||
:href href
|
||||
:sx-get href
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
(~tw :tokens "text-violet-700 hover:text-violet-900 underline")
|
||||
name)
|
||||
(span (~tw :tokens "text-violet-700") name)))
|
||||
(td (~tw :tokens "px-3 py-2 text-stone-700 text-sm") description)))
|
||||
|
||||
(defcomp
|
||||
~docs/two-col-table
|
||||
(&key
|
||||
(title :as string?)
|
||||
(intro :as string?)
|
||||
(col1 :as string?)
|
||||
(col2 :as string?)
|
||||
rows)
|
||||
(div
|
||||
(~tw :tokens "space-y-3")
|
||||
(when title (h3 (~tw :tokens "text-xl font-semibold text-stone-700") title))
|
||||
(when intro (p (~tw :tokens "text-stone-600 mb-6") intro))
|
||||
(div
|
||||
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||
(table
|
||||
(~tw :tokens "w-full text-left text-sm")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||
(th
|
||||
(~tw :tokens "px-3 py-2 font-medium text-stone-600")
|
||||
(or col1 "Name"))
|
||||
(th
|
||||
(~tw :tokens "px-3 py-2 font-medium text-stone-600")
|
||||
(or col2 "Description"))))
|
||||
(tbody rows)))))
|
||||
|
||||
(defcomp ~docs/label () (span (~tw :tokens "font-mono") "(<sx>)"))
|
||||
|
||||
(defcomp
|
||||
~docs/clear-cache-btn
|
||||
()
|
||||
(button
|
||||
:onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)"
|
||||
(~tw :tokens "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors")
|
||||
"Clear component cache"))
|
||||
|
||||
(defcomp
|
||||
~docs/attr-table-from-data
|
||||
(&key (title :as string) (attrs :as list))
|
||||
(~docs/attr-table
|
||||
:title title
|
||||
:rows (<>
|
||||
(map
|
||||
(fn
|
||||
(a)
|
||||
(~docs/attr-row
|
||||
:attr (get a "name")
|
||||
:description (get a "desc")
|
||||
:exists (get a "exists")
|
||||
:href (get a "href")))
|
||||
attrs))))
|
||||
|
||||
(defcomp
|
||||
~docs/headers-table-from-data
|
||||
(&key (title :as string) (headers :as list))
|
||||
(~docs/headers-table
|
||||
:title title
|
||||
:rows (<>
|
||||
(map
|
||||
(fn
|
||||
(h)
|
||||
(~docs/headers-row
|
||||
:name (get h "name")
|
||||
:value (get h "value")
|
||||
:description (get h "desc")
|
||||
:href (get h "href")))
|
||||
headers))))
|
||||
|
||||
(defcomp
|
||||
~docs/two-col-table-from-data
|
||||
(&key
|
||||
(title :as string?)
|
||||
(intro :as string?)
|
||||
(col1 :as string?)
|
||||
(col2 :as string?)
|
||||
(items :as list))
|
||||
(~docs/two-col-table
|
||||
:title title
|
||||
:intro intro
|
||||
:col1 col1
|
||||
:col2 col2
|
||||
:rows (<>
|
||||
(map
|
||||
(fn
|
||||
(item)
|
||||
(~docs/two-col-row
|
||||
:name (get item "name")
|
||||
:description (get item "desc")
|
||||
:href (get item "href")))
|
||||
items))))
|
||||
|
||||
(defcomp
|
||||
~docs/primitives-tables
|
||||
(&key (primitives :as dict))
|
||||
(<>
|
||||
(map
|
||||
(fn
|
||||
(cat)
|
||||
(~docs/primitives-table
|
||||
:category cat
|
||||
:primitives (get primitives cat)))
|
||||
(keys primitives))))
|
||||
|
||||
(defcomp
|
||||
~docs/special-forms-tables
|
||||
(&key (forms :as dict))
|
||||
(<>
|
||||
(map
|
||||
(fn
|
||||
(cat)
|
||||
(~docs/special-forms-category :category cat :forms (get forms cat)))
|
||||
(keys forms))))
|
||||
|
||||
(defcomp
|
||||
~docs/special-forms-category
|
||||
(&key (category :as string) (forms :as list))
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(h3
|
||||
(~tw :tokens "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2")
|
||||
category)
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(map
|
||||
(fn
|
||||
(f)
|
||||
(~docs/special-form-card
|
||||
:name (get f "name")
|
||||
:syntax (get f "syntax")
|
||||
:doc (get f "doc")
|
||||
:tail-position (get f "tail-position")
|
||||
:example (get f "example")))
|
||||
forms))))
|
||||
|
||||
(defcomp
|
||||
~docs/special-form-card
|
||||
(&key
|
||||
(name :as string)
|
||||
(syntax :as string)
|
||||
(doc :as string)
|
||||
(tail-position :as string)
|
||||
(example :as string))
|
||||
(div
|
||||
(~tw :tokens "not-prose border border-stone-200 rounded-lg p-4 space-y-3")
|
||||
(div
|
||||
(~tw :tokens "flex items-baseline gap-3")
|
||||
(code (~tw :tokens "text-lg font-bold text-violet-700") name)
|
||||
(when
|
||||
(not (= tail-position "none"))
|
||||
(span
|
||||
(~tw :tokens "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700")
|
||||
"TCO")))
|
||||
(when
|
||||
(not (= syntax ""))
|
||||
(pre
|
||||
(~tw :tokens "bg-stone-100 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto")
|
||||
syntax))
|
||||
(p (~tw :tokens "text-stone-600 text-sm whitespace-pre-line") doc)
|
||||
(when
|
||||
(not (= tail-position ""))
|
||||
(p
|
||||
(~tw :tokens "text-xs text-stone-500")
|
||||
(span (~tw :tokens "font-semibold") "Tail position: ")
|
||||
tail-position))
|
||||
(when
|
||||
(not (= example ""))
|
||||
(~docs/code :src (highlight example "lisp")))))
|
||||
14
sx/sx/docs/attr-table-from-data.sx
Normal file
14
sx/sx/docs/attr-table-from-data.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
(defcomp
|
||||
(&key (title :as string) (attrs :as list))
|
||||
(~docs/attr-table
|
||||
:title title
|
||||
:rows (<>
|
||||
(map
|
||||
(fn
|
||||
(a)
|
||||
(~docs/attr-row
|
||||
:attr (get a "name")
|
||||
:description (get a "desc")
|
||||
:exists (get a "exists")
|
||||
:href (get a "href")))
|
||||
attrs))))
|
||||
18
sx/sx/docs/attr-table.sx
Normal file
18
sx/sx/docs/attr-table.sx
Normal file
@@ -0,0 +1,18 @@
|
||||
(defcomp
|
||||
(&key (title :as string) rows)
|
||||
(div
|
||||
(~tw :tokens "space-y-3")
|
||||
(h3 (~tw :tokens "text-xl font-semibold text-stone-700") title)
|
||||
(div
|
||||
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||
(table
|
||||
(~tw :tokens "w-full text-left text-sm")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Attribute")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Description")
|
||||
(th
|
||||
(~tw :tokens "px-3 py-2 font-medium text-stone-600 text-center w-20")
|
||||
"In sx?")))
|
||||
(tbody rows)))))
|
||||
6
sx/sx/docs/clear-cache-btn.sx
Normal file
6
sx/sx/docs/clear-cache-btn.sx
Normal file
@@ -0,0 +1,6 @@
|
||||
(defcomp
|
||||
()
|
||||
(button
|
||||
:onclick "localStorage.removeItem('sx-components-hash');localStorage.removeItem('sx-components-src');var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)"
|
||||
(~tw :tokens "text-xs text-stone-400 hover:text-stone-600 border border-stone-200 rounded px-2 py-1 transition-colors")
|
||||
"Clear component cache"))
|
||||
24
sx/sx/docs/headers-row.sx
Normal file
24
sx/sx/docs/headers-row.sx
Normal file
@@ -0,0 +1,24 @@
|
||||
(defcomp
|
||||
(&key
|
||||
(name :as string)
|
||||
(value :as string)
|
||||
(description :as string)
|
||||
(href :as string?))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-100")
|
||||
(td
|
||||
(~tw :tokens "px-3 py-2 font-mono text-sm whitespace-nowrap")
|
||||
(if
|
||||
href
|
||||
(a
|
||||
:href href
|
||||
:sx-get href
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
(~tw :tokens "text-violet-700 hover:text-violet-900 underline")
|
||||
name)
|
||||
(span (~tw :tokens "text-violet-700") name)))
|
||||
(td (~tw :tokens "px-3 py-2 font-mono text-sm text-stone-500") value)
|
||||
(td (~tw :tokens "px-3 py-2 text-stone-700 text-sm") description)))
|
||||
14
sx/sx/docs/headers-table-from-data.sx
Normal file
14
sx/sx/docs/headers-table-from-data.sx
Normal file
@@ -0,0 +1,14 @@
|
||||
(defcomp
|
||||
(&key (title :as string) (headers :as list))
|
||||
(~docs/headers-table
|
||||
:title title
|
||||
:rows (<>
|
||||
(map
|
||||
(fn
|
||||
(h)
|
||||
(~docs/headers-row
|
||||
:name (get h "name")
|
||||
:value (get h "value")
|
||||
:description (get h "desc")
|
||||
:href (get h "href")))
|
||||
headers))))
|
||||
16
sx/sx/docs/headers-table.sx
Normal file
16
sx/sx/docs/headers-table.sx
Normal file
@@ -0,0 +1,16 @@
|
||||
(defcomp
|
||||
(&key (title :as string) rows)
|
||||
(div
|
||||
(~tw :tokens "space-y-3")
|
||||
(h3 (~tw :tokens "text-xl font-semibold text-stone-700") title)
|
||||
(div
|
||||
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||
(table
|
||||
(~tw :tokens "w-full text-left text-sm")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Header")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Value")
|
||||
(th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Description")))
|
||||
(tbody rows)))))
|
||||
1
sx/sx/docs/label.sx
Normal file
1
sx/sx/docs/label.sx
Normal file
@@ -0,0 +1 @@
|
||||
(defcomp () (span (~tw :tokens "font-mono") "(<sx>)"))
|
||||
8
sx/sx/docs/oob-code.sx
Normal file
8
sx/sx/docs/oob-code.sx
Normal file
@@ -0,0 +1,8 @@
|
||||
(defcomp
|
||||
(&key (target-id :as string) (text :as string))
|
||||
(div
|
||||
:id target-id
|
||||
:sx-swap-oob "innerHTML"
|
||||
(div
|
||||
(~tw :tokens "not-prose bg-stone-100 rounded p-4 mt-3")
|
||||
(pre (~tw :tokens "text-sm whitespace-pre-wrap break-words") (code text)))))
|
||||
9
sx/sx/docs/placeholder.sx
Normal file
9
sx/sx/docs/placeholder.sx
Normal file
@@ -0,0 +1,9 @@
|
||||
(defcomp
|
||||
(&key (id :as string))
|
||||
(div
|
||||
:id id
|
||||
(div
|
||||
(~tw :tokens "bg-stone-100 rounded p-4 mt-3")
|
||||
(p
|
||||
(~tw :tokens "text-stone-400 italic text-sm")
|
||||
"Trigger the demo to see the actual content."))))
|
||||
10
sx/sx/docs/primitives-tables.sx
Normal file
10
sx/sx/docs/primitives-tables.sx
Normal file
@@ -0,0 +1,10 @@
|
||||
(defcomp
|
||||
(&key (primitives :as dict))
|
||||
(<>
|
||||
(map
|
||||
(fn
|
||||
(cat)
|
||||
(~docs/primitives-table
|
||||
:category cat
|
||||
:primitives (get primitives cat)))
|
||||
(keys primitives))))
|
||||
32
sx/sx/docs/special-form-card.sx
Normal file
32
sx/sx/docs/special-form-card.sx
Normal file
@@ -0,0 +1,32 @@
|
||||
(defcomp
|
||||
(&key
|
||||
(name :as string)
|
||||
(syntax :as string)
|
||||
(doc :as string)
|
||||
(tail-position :as string)
|
||||
(example :as string))
|
||||
(div
|
||||
(~tw :tokens "not-prose border border-stone-200 rounded-lg p-4 space-y-3")
|
||||
(div
|
||||
(~tw :tokens "flex items-baseline gap-3")
|
||||
(code (~tw :tokens "text-lg font-bold text-violet-700") name)
|
||||
(when
|
||||
(not (= tail-position "none"))
|
||||
(span
|
||||
(~tw :tokens "text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700")
|
||||
"TCO")))
|
||||
(when
|
||||
(not (= syntax ""))
|
||||
(pre
|
||||
(~tw :tokens "bg-stone-100 rounded px-3 py-2 text-sm font-mono text-stone-700 overflow-x-auto")
|
||||
syntax))
|
||||
(p (~tw :tokens "text-stone-600 text-sm whitespace-pre-line") doc)
|
||||
(when
|
||||
(not (= tail-position ""))
|
||||
(p
|
||||
(~tw :tokens "text-xs text-stone-500")
|
||||
(span (~tw :tokens "font-semibold") "Tail position: ")
|
||||
tail-position))
|
||||
(when
|
||||
(not (= example ""))
|
||||
(~docs/code :src (highlight example "lisp")))))
|
||||
19
sx/sx/docs/special-forms-category.sx
Normal file
19
sx/sx/docs/special-forms-category.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
(defcomp
|
||||
(&key (category :as string) (forms :as list))
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(h3
|
||||
(~tw :tokens "text-xl font-semibold text-stone-800 border-b border-stone-200 pb-2")
|
||||
category)
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(map
|
||||
(fn
|
||||
(f)
|
||||
(~docs/special-form-card
|
||||
:name (get f "name")
|
||||
:syntax (get f "syntax")
|
||||
:doc (get f "doc")
|
||||
:tail-position (get f "tail-position")
|
||||
:example (get f "example")))
|
||||
forms))))
|
||||
8
sx/sx/docs/special-forms-tables.sx
Normal file
8
sx/sx/docs/special-forms-tables.sx
Normal file
@@ -0,0 +1,8 @@
|
||||
(defcomp
|
||||
(&key (forms :as dict))
|
||||
(<>
|
||||
(map
|
||||
(fn
|
||||
(cat)
|
||||
(~docs/special-forms-category :category cat :forms (get forms cat)))
|
||||
(keys forms))))
|
||||
19
sx/sx/docs/two-col-row.sx
Normal file
19
sx/sx/docs/two-col-row.sx
Normal file
@@ -0,0 +1,19 @@
|
||||
(defcomp
|
||||
(&key (name :as string) (description :as string) (href :as string?))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-100")
|
||||
(td
|
||||
(~tw :tokens "px-3 py-2 font-mono text-sm whitespace-nowrap")
|
||||
(if
|
||||
href
|
||||
(a
|
||||
:href href
|
||||
:sx-get href
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
(~tw :tokens "text-violet-700 hover:text-violet-900 underline")
|
||||
name)
|
||||
(span (~tw :tokens "text-violet-700") name)))
|
||||
(td (~tw :tokens "px-3 py-2 text-stone-700 text-sm") description)))
|
||||
21
sx/sx/docs/two-col-table-from-data.sx
Normal file
21
sx/sx/docs/two-col-table-from-data.sx
Normal file
@@ -0,0 +1,21 @@
|
||||
(defcomp
|
||||
(&key
|
||||
(title :as string?)
|
||||
(intro :as string?)
|
||||
(col1 :as string?)
|
||||
(col2 :as string?)
|
||||
(items :as list))
|
||||
(~docs/two-col-table
|
||||
:title title
|
||||
:intro intro
|
||||
:col1 col1
|
||||
:col2 col2
|
||||
:rows (<>
|
||||
(map
|
||||
(fn
|
||||
(item)
|
||||
(~docs/two-col-row
|
||||
:name (get item "name")
|
||||
:description (get item "desc")
|
||||
:href (get item "href")))
|
||||
items))))
|
||||
25
sx/sx/docs/two-col-table.sx
Normal file
25
sx/sx/docs/two-col-table.sx
Normal file
@@ -0,0 +1,25 @@
|
||||
(defcomp
|
||||
(&key
|
||||
(title :as string?)
|
||||
(intro :as string?)
|
||||
(col1 :as string?)
|
||||
(col2 :as string?)
|
||||
rows)
|
||||
(div
|
||||
(~tw :tokens "space-y-3")
|
||||
(when title (h3 (~tw :tokens "text-xl font-semibold text-stone-700") title))
|
||||
(when intro (p (~tw :tokens "text-stone-600 mb-6") intro))
|
||||
(div
|
||||
(~tw :tokens "overflow-x-auto rounded border border-stone-200")
|
||||
(table
|
||||
(~tw :tokens "w-full text-left text-sm")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-stone-200 bg-stone-100")
|
||||
(th
|
||||
(~tw :tokens "px-3 py-2 font-medium text-stone-600")
|
||||
(or col1 "Name"))
|
||||
(th
|
||||
(~tw :tokens "px-3 py-2 font-medium text-stone-600")
|
||||
(or col2 "Description"))))
|
||||
(tbody rows)))))
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
;; The Hegelian Synthesis of Hypertext and Reactivity
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~essays/hegelian-synthesis/essay-hegelian-synthesis ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "The Hegelian Synthesis"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"On the dialectical resolution of the hypertext/reactive contradiction.")
|
||||
@@ -1,2 +1,2 @@
|
||||
(defcomp ~essays/htmx-react-hybrid/essay-htmx-react-hybrid ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "The htmx/React Hybrid" (~docs/section :title "Two good ideas" :id "ideas" (p (~tw :tokens "text-stone-600") "htmx: the server should render HTML. The client should swap it in. No client-side routing. No virtual DOM. No state management.") (p (~tw :tokens "text-stone-600") "React: UI should be composed from reusable components with parameters. Components encapsulate structure, style, and behavior.") (p (~tw :tokens "text-stone-600") "sx tries to combine both: server-rendered s-expressions with hypermedia attributes AND a component model with caching and composition.")) (~docs/section :title "What sx keeps from htmx" :id "from-htmx" (ul (~tw :tokens "space-y-2 text-stone-600") (li "Server generates the UI — no client-side data fetching or state") (li "Hypermedia attributes (sx-get, sx-target, sx-swap) on any element") (li "Partial page updates via swap/OOB — no full page reloads") (li "Works with standard HTTP — no WebSocket or custom protocol required"))) (~docs/section :title "What sx adds from React" :id "from-react" (ul (~tw :tokens "space-y-2 text-stone-600") (li "defcomp — named, parameterized, composable components") (li "Client-side rendering — server sends source, client renders DOM") (li "Component caching — definitions cached in localStorage across navigations") (li "On-demand CSS — only ship the rules that are used"))) (~docs/section :title "What sx gives up" :id "gives-up" (ul (~tw :tokens "space-y-2 text-stone-600") (li "No HTML output — sx sends s-expressions, not HTML. JS required.") (li "Custom parser — the client needs sx.js to understand responses") (li "Niche — no ecosystem, no community, no third-party support") (li "Learning curve — s-expression syntax is unfamiliar to most web developers")))))
|
||||
@@ -3,7 +3,7 @@
|
||||
;; A response to Nick Blow's article on JSON hypermedia and LLM agents.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~essays/hypermedia-age-of-ai/essay-hypermedia-age-of-ai ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Hypermedia in the Age of AI"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"Neither JSON nor HTML is hypermedia. There is only the hypermedium — a self-defining representation — and s-expressions are an instance of it.")
|
||||
@@ -1,5 +1,4 @@
|
||||
(defcomp
|
||||
~essays/index/essays-index-content
|
||||
()
|
||||
(~docs/page
|
||||
:title "Essays"
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/no-alternative/essay-no-alternative ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "There Is No Alternative"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"Every attempt to escape s-expressions leads back to s-expressions. This is not an accident.")
|
||||
@@ -1,2 +1,2 @@
|
||||
(defcomp ~essays/on-demand-css/essay-on-demand-css ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "On-Demand CSS: Killing the Tailwind Bundle" (~docs/section :title "The problem" :id "problem" (p (~tw :tokens "text-stone-600") "Tailwind CSS generates a utility class for every possible combination. The full CSS file is ~4MB. The purged output for a typical site is 20-50KB. Purging requires a build step that scans your source files for class names. This means: a build tool, a config file, a CI step, and a prayer that the scanner finds all your dynamic classes.")) (~docs/section :title "The sx approach" :id "approach" (p (~tw :tokens "text-stone-600") "sx takes a different path. At server startup, the full Tailwind CSS file is parsed into a dictionary keyed by class name. When rendering a response, sx scans the s-expression source for :class attribute values and looks up only those classes. The result: exact CSS, zero build step.") (p (~tw :tokens "text-stone-600") "Component definitions are pre-scanned at registration time. Page-specific sx is scanned at request time. The union of classes is resolved to CSS rules.")) (~docs/section :title "Incremental delivery" :id "incremental" (p (~tw :tokens "text-stone-600") "After the first page load, the client tracks which CSS classes it already has. On subsequent navigations, it sends a hash of its known classes in the SX-Css header. The server computes the diff and sends only new rules. A typical navigation adds 0-10 new rules — a few hundred bytes at most.")) (~docs/section :title "The tradeoff" :id "tradeoff" (p (~tw :tokens "text-stone-600") "The server holds ~4MB of parsed CSS in memory. Regex scanning is not perfect — dynamically constructed class names will not be found. In practice this rarely matters because sx components use mostly static class strings."))))
|
||||
@@ -2,7 +2,7 @@
|
||||
;; React is Hypermedia
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~essays/react-is-hypermedia/essay-react-is-hypermedia ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "React is Hypermedia"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"A React Island is a hypermedia control. Its behavior is specified in SX.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/reflexive-web/essay-reflexive-web ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "The Reflexive Web"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"What happens when the web can read, modify, and reason about itself — and AI is a native participant.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/self-defining-medium/essay-self-defining-medium ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "The True Hypermedium Must Define Itself With Itself"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"On ontological uniformity, the metacircular web, and why the address of a thing and the thing itself should be made of the same stuff.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/separation-of-concerns/essay-separation-of-concerns ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Separate your Own Concerns"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"The web's canonical separation — HTML, CSS, JavaScript — separates the framework's concerns, not yours. Real separation of concerns is domain-specific and cannot be prescribed by a platform.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/server-architecture/essay-server-architecture ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Server Architecture"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"How SX enforces the boundary between host language and embedded language, why that boundary matters, and what it looks like across different target languages.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/sx-and-ai/essay-sx-and-ai ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "SX and AI"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"Why s-expressions are the most AI-friendly representation for web interfaces — and what that means for how software gets built.")
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
(defcomp ~essays/sx-sucks/essay-sx-sucks ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "sx sucks" (~docs/section :title "The parentheses" :id "parens" (p (~tw :tokens "text-stone-600") "S-expressions are parentheses. Lots of parentheses. You thought LISP was dead? No, someone just decided to use it for HTML templates. Your IDE will need a parenthesis counter. Your code reviews will be 40% closing parens. Every merge conflict will be about whether a paren belongs on this line or the next.")) (~docs/section :title "Nobody asked for this" :id "nobody-asked" (p (~tw :tokens "text-stone-600") "The JavaScript ecosystem has React, Vue, Svelte, Solid, Qwik, and approximately 47,000 other frameworks. htmx proved you can skip them all. sx looked at this landscape and said: you know what this needs? A Lisp dialect. For HTML. Over HTTP.") (p (~tw :tokens "text-stone-600") "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub.")) (~docs/section :title "The author has never written a line of LISP" :id "no-lisp" (p (~tw :tokens "text-stone-600") "The author of sx has never written a single line of actual LISP. Not Common Lisp. Not Scheme. Not Clojure. Not even Emacs Lisp. The entire s-expression evaluator was written by someone whose mental model of LISP comes from reading the first three chapters of SICP and then closing the tab.") (p (~tw :tokens "text-stone-600") "This is like building a sushi restaurant when your only experience with Japanese cuisine is eating supermarket California rolls.")) (~docs/section :title "AI wrote most of it" :id "ai" (p (~tw :tokens "text-stone-600") "A significant portion of sx — the evaluator, the parser, the primitives, the CSS scanner, this very documentation site — was written with AI assistance. The author typed prompts. Claude typed code. This is not artisanal hand-crafted software. This is the software equivalent of a microwave dinner presented on a nice plate.") (p (~tw :tokens "text-stone-600") "He adds features by typing stuff like \"is there rom for macros within sx.js? what benefits m,ight that bring?\", skim-reading the response, and then entering \"crack on then!\" This is not software engineering. This is improv comedy with a compiler.") (p (~tw :tokens "text-stone-600") "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? You will never know.")) (~docs/section :title "No ecosystem" :id "ecosystem" (p (~tw :tokens "text-stone-600") "npm has 2 million packages. PyPI has 500,000. sx has zero packages, zero plugins, zero middleware, zero community, zero Stack Overflow answers, and zero conference talks. If you get stuck, your options are: read the source, or ask the one person who wrote it.") (p (~tw :tokens "text-stone-600") "That person is busy. Good luck.")) (~docs/section :title "Zero jobs" :id "jobs" (p (~tw :tokens "text-stone-600") "Adding sx to your CV will not get you hired. It will get you questioned.") (p (~tw :tokens "text-stone-600") "The interview will end shortly after.")) (~docs/section :title "The creator thinks s-expressions are a personality trait" :id "personality" (p (~tw :tokens "text-stone-600") "Look at this documentation site. It has a violet colour scheme. It has credits to htmx. It has a future possibilities page about hypothetical sx:// protocol schemes. The creator built an entire microservice — with Docker, Redis, and a custom entrypoint script — just to serve documentation about a rendering engine that runs one website.") (p (~tw :tokens "text-stone-600") "This is not engineering. This is a personality disorder expressed in YAML."))))
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
;; The Art Chain
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~essays/the-art-chain/essay-the-art-chain ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "The Art Chain"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"On making, self-making, and the chain of artifacts that produces itself.")
|
||||
@@ -1,2 +1,2 @@
|
||||
(defcomp ~essays/why-sexps/essay-why-sexps ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Why S-Expressions Over HTML Attributes" (~docs/section :title "The problem with HTML attributes" :id "problem" (p (~tw :tokens "text-stone-600") "HTML attributes are strings. You can put anything in a string. htmx puts DSLs in strings — trigger modifiers, swap strategies, CSS selectors. This works but it means you're parsing a language within a language within a language.") (p (~tw :tokens "text-stone-600") "S-expressions are already structured. Keywords are keywords. Lists are lists. Nested expressions nest naturally. There's no need to invent a trigger modifier syntax because the expression language already handles composition.")) (~docs/section :title "Components without a build step" :id "components" (p (~tw :tokens "text-stone-600") "React showed that components are the right abstraction for UI. The price: a build step, a bundler, JSX transpilation. With s-expressions, defcomp is just another form in the language. No transpiler needed. The same source runs on server and client.")) (~docs/section :title "When attributes are better" :id "better" (p (~tw :tokens "text-stone-600") "HTML attributes work in any HTML document. S-expressions need a runtime. If you want progressive enhancement that works with JS disabled, htmx is better. If you want to write HTML by hand in static files, htmx is better. sx only makes sense when you're already rendering server-side and want components."))))
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/zero-tooling/essay-zero-tooling ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Tools for Fools"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"SX was built without a code editor. No IDE, no manual source edits, no build tools, no linters, no bundlers. The entire codebase — evaluator, renderer, spec, documentation site, test suite — was produced through conversation with an agentic AI. This is what zero-tooling web development looks like.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/sx-and-dennett/essay-sx-and-dennett ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "SX and Dennett"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"Real patterns, multiple drafts, and the intentional stance — a philosopher of mind meets a language that thinks about itself.")
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/s-existentialism/essay-s-existentialism ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "S-Existentialism"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"Existence precedes essence — and s-expressions exist before anything gives them meaning.")
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
|
||||
(defcomp
|
||||
~essays/philosophy-index/content
|
||||
()
|
||||
(~docs/page
|
||||
:title "Philosophy"
|
||||
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/platonic-sx/essay-platonic-sx ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Platonic SX"
|
||||
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
(defcomp ~essays/sx-and-wittgenstein/essay-sx-and-wittgenstein ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "SX and Wittgenstein"
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
"The limits of my language are the limits of my world.")
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Art DAG on SX — SX endpoints as portals into media processing environments
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/art-dag-sx/plan-art-dag-sx-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Art DAG on SX"
|
||||
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Async Evaluator Convergence — Bootstrap async_eval.py from Spec
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/async-eval-convergence/plan-async-eval-convergence-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Async Evaluator Convergence"
|
||||
|
||||
(~docs/section :title "The Problem" :id "problem"
|
||||
@@ -1,7 +1,7 @@
|
||||
;; Deref as Shift — CEK-Based Reactive DOM Renderer
|
||||
;; Phase B: replace explicit effects with implicit continuation capture.
|
||||
|
||||
(defcomp ~plans/cek-reactive/plan-cek-reactive-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Deref as Shift — CEK-Based Reactive DOM Renderer"
|
||||
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Content-Addressed Components
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/content-addressed-components/plan-content-addressed-components-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Content-Addressed Components"
|
||||
|
||||
(~docs/section :title "The Premise" :id "premise"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Content-Addressed Environment Images
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/environment-images/plan-environment-images-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Content-Addressed Environment Images"
|
||||
|
||||
(~docs/section :title "The Idea" :id "idea"
|
||||
@@ -1,7 +1,7 @@
|
||||
;; Foundations — The Computational Floor
|
||||
;; From scoped effects to CEK: what's beneath algebraic effects and why it's the limit.
|
||||
|
||||
(defcomp ~plans/foundations/plan-foundations-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Foundations \u2014 The Computational Floor"
|
||||
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Fragment Protocol
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/fragment-protocol/plan-fragment-protocol-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Fragment Protocol"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Generative SX — programs that write themselves as they run
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/generative-sx/plan-generative-sx-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Generative SX"
|
||||
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Glue Decoupling
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/glue-decoupling/plan-glue-decoupling-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Cross-App Decoupling via Glue Services"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
@@ -1,5 +1,4 @@
|
||||
(defcomp
|
||||
~plans/index/plans-index-content
|
||||
()
|
||||
(~docs/page
|
||||
:title "Plans"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Isolated Evaluator — Shared platform layer, isolated JS, Rust WASM
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/isolated-evaluator/plan-isolated-evaluator-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Isolated Evaluator"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Isomorphic Architecture Roadmap
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/isomorphic/plan-isomorphic-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Isomorphic Architecture Roadmap"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; js.sx — Self-Hosting JavaScript Bootstrapper
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/js-bootstrapper/plan-js-bootstrapper-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "js.sx — JavaScript Bootstrapper"
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Live Streaming — SSE & WebSocket
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/live-streaming/plan-live-streaming-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Live Streaming"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Mother Language — SX as its own compiler, OCaml as the substrate
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/mother-language/plan-mother-language-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Mother Language"
|
||||
|
||||
(p (~tw :tokens "text-stone-500 text-sm italic mb-8")
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Navigation Redesign — SX Docs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/nav-redesign/plan-nav-redesign-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Navigation Redesign"
|
||||
|
||||
(~docs/section :title "The Problem" :id "problem"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Predictive Component Prefetching
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/predictive-prefetch/plan-predictive-prefetch-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Predictive Component Prefetching"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
@@ -2,7 +2,7 @@
|
||||
;; Reader Macros
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~plans/reader-macros/plan-reader-macros-content ()
|
||||
(defcomp ()
|
||||
(~docs/page :title "Reader Macros"
|
||||
|
||||
(~docs/section :title "Context" :id "context"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user