Merge branch 'worktree-api-urls' into macros
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
// =========================================================================
|
||||
|
||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||
var SX_VERSION = "2026-03-13T04:50:39Z";
|
||||
var SX_VERSION = "2026-03-13T05:11:12Z";
|
||||
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -1934,7 +1934,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
return assoc(state, "skip", true, "i", (get(state, "i") + 1));
|
||||
})() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? (function() {
|
||||
var child = renderToDom(arg, env, newNs);
|
||||
return (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() {
|
||||
return (isSxTruthy((isSxTruthy(isSpread(child)) && _islandScope)) ? reactiveSpread(el, function() { return renderToDom(arg, env, newNs); }) : (isSxTruthy(isSpread(child)) ? forEach(function(key) { return (function() {
|
||||
var val = dictGet(spreadAttrs(child), key);
|
||||
return (isSxTruthy((key == "class")) ? (function() {
|
||||
var existing = domGetAttr(el, "class");
|
||||
@@ -1943,7 +1943,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
|
||||
var existing = domGetAttr(el, "style");
|
||||
return domSetAttr(el, "style", (isSxTruthy((isSxTruthy(existing) && !isSxTruthy((existing == "")))) ? (String(existing) + String(";") + String(val)) : val));
|
||||
})() : domSetAttr(el, key, (String(val)))));
|
||||
})(); }, keys(spreadAttrs(child))) : domAppend(el, child));
|
||||
})(); }, keys(spreadAttrs(child))) : domAppend(el, child)));
|
||||
})() : NIL), assoc(state, "i", (get(state, "i") + 1)))));
|
||||
})(); }, {["i"]: 0, ["skip"]: false}, args);
|
||||
return el;
|
||||
@@ -2269,6 +2269,44 @@ return effect(function() { return (function() {
|
||||
})();
|
||||
})(); }); };
|
||||
|
||||
// reactive-spread
|
||||
var reactiveSpread = function(el, renderFn) { return (function() {
|
||||
var prevClasses = [];
|
||||
var prevExtraKeys = [];
|
||||
(function() {
|
||||
var existing = sxOr(domGetAttr(el, "data-sx-reactive-attrs"), "");
|
||||
return domSetAttr(el, "data-sx-reactive-attrs", (isSxTruthy(isEmpty(existing)) ? "_spread" : (String(existing) + String(",_spread"))));
|
||||
})();
|
||||
return effect(function() { if (isSxTruthy(!isSxTruthy(isEmpty(prevClasses)))) {
|
||||
(function() {
|
||||
var current = sxOr(domGetAttr(el, "class"), "");
|
||||
var tokens = filter(function(c) { return !isSxTruthy((c == "")); }, split(current, " "));
|
||||
var kept = filter(function(c) { return !isSxTruthy(some(function(pc) { return (pc == c); }, prevClasses)); }, tokens);
|
||||
return (isSxTruthy(isEmpty(kept)) ? domRemoveAttr(el, "class") : domSetAttr(el, "class", join(" ", kept)));
|
||||
})();
|
||||
}
|
||||
{ var _c = prevExtraKeys; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; domRemoveAttr(el, k); } }
|
||||
return (function() {
|
||||
var result = renderFn();
|
||||
return (isSxTruthy(isSpread(result)) ? (function() {
|
||||
var attrs = spreadAttrs(result);
|
||||
var clsStr = sxOr(dictGet(attrs, "class"), "");
|
||||
var newClasses = filter(function(c) { return !isSxTruthy((c == "")); }, split(clsStr, " "));
|
||||
var extraKeys = filter(function(k) { return !isSxTruthy((k == "class")); }, keys(attrs));
|
||||
prevClasses = newClasses;
|
||||
prevExtraKeys = extraKeys;
|
||||
if (isSxTruthy(!isSxTruthy(isEmpty(newClasses)))) {
|
||||
(function() {
|
||||
var current = sxOr(domGetAttr(el, "class"), "");
|
||||
return domSetAttr(el, "class", (isSxTruthy((isSxTruthy(current) && !isSxTruthy((current == "")))) ? (String(current) + String(" ") + String(clsStr)) : clsStr));
|
||||
})();
|
||||
}
|
||||
{ var _c = extraKeys; for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; domSetAttr(el, k, (String(dictGet(attrs, k)))); } }
|
||||
return flushCssxToDom();
|
||||
})() : ((prevClasses = []), (prevExtraKeys = [])));
|
||||
})(); });
|
||||
})(); };
|
||||
|
||||
// reactive-fragment
|
||||
var reactiveFragment = function(testFn, renderFn, env, ns) { return (function() {
|
||||
var marker = createComment("island-fragment");
|
||||
|
||||
@@ -232,30 +232,35 @@
|
||||
(do
|
||||
(when (not (contains? VOID_ELEMENTS tag))
|
||||
(let ((child (render-to-dom arg env new-ns)))
|
||||
(if (spread? child)
|
||||
;; Spread: merge attrs onto parent element
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(let ((val (dict-get (spread-attrs child) key)))
|
||||
(if (= key "class")
|
||||
;; Class: append to existing
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing " " val)
|
||||
val)))
|
||||
(if (= key "style")
|
||||
;; Style: append with semicolon
|
||||
(let ((existing (dom-get-attr el "style")))
|
||||
(dom-set-attr el "style"
|
||||
(cond
|
||||
;; Reactive spread: track signal deps, update attrs on change
|
||||
(and (spread? child) *island-scope*)
|
||||
(reactive-spread el (fn () (render-to-dom arg env new-ns)))
|
||||
;; Static spread: one-shot merge attrs onto parent element
|
||||
(spread? child)
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(let ((val (dict-get (spread-attrs child) key)))
|
||||
(if (= key "class")
|
||||
;; Class: append to existing
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing ";" val)
|
||||
(str existing " " val)
|
||||
val)))
|
||||
;; Other attrs: overwrite
|
||||
(dom-set-attr el key (str val))))))
|
||||
(keys (spread-attrs child)))
|
||||
(if (= key "style")
|
||||
;; Style: append with semicolon
|
||||
(let ((existing (dom-get-attr el "style")))
|
||||
(dom-set-attr el "style"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing ";" val)
|
||||
val)))
|
||||
;; Other attrs: overwrite
|
||||
(dom-set-attr el key (str val))))))
|
||||
(keys (spread-attrs child)))
|
||||
;; Normal child: append to element
|
||||
(dom-append el child))))
|
||||
:else
|
||||
(dom-append el child))))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
@@ -867,6 +872,64 @@
|
||||
:else
|
||||
(dom-set-attr el attr-name (str val)))))))))
|
||||
|
||||
;; reactive-spread — reactively bind spread attrs to parent element.
|
||||
;; Used when a child of an element produces a spread inside an island.
|
||||
;; Tracks signal deps in the spread expression. When signals change:
|
||||
;; old classes are removed, new ones applied. Non-class attrs (data-tw etc.)
|
||||
;; are overwritten. Flushes newly collected CSS rules to live stylesheet.
|
||||
;;
|
||||
;; Multiple reactive spreads on the same element are safe — each tracks
|
||||
;; its own class contribution and only removes/adds its own tokens.
|
||||
(define reactive-spread :effects [render mutation]
|
||||
(fn (el (render-fn :as lambda))
|
||||
(let ((prev-classes (list))
|
||||
(prev-extra-keys (list)))
|
||||
;; Mark for morph protection
|
||||
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") "")))
|
||||
(dom-set-attr el "data-sx-reactive-attrs"
|
||||
(if (empty? existing) "_spread" (str existing ",_spread"))))
|
||||
(effect (fn ()
|
||||
;; 1. Remove previously applied classes from element's class list
|
||||
(when (not (empty? prev-classes))
|
||||
(let ((current (or (dom-get-attr el "class") ""))
|
||||
(tokens (filter (fn (c) (not (= c ""))) (split current " ")))
|
||||
(kept (filter (fn (c)
|
||||
(not (some (fn (pc) (= pc c)) prev-classes)))
|
||||
tokens)))
|
||||
(if (empty? kept)
|
||||
(dom-remove-attr el "class")
|
||||
(dom-set-attr el "class" (join " " kept)))))
|
||||
;; 2. Remove previously applied extra attrs
|
||||
(for-each (fn (k) (dom-remove-attr el k)) prev-extra-keys)
|
||||
;; 3. Re-evaluate the spread expression (tracks signal deps)
|
||||
(let ((result (render-fn)))
|
||||
(if (spread? result)
|
||||
(let ((attrs (spread-attrs result))
|
||||
(cls-str (or (dict-get attrs "class") ""))
|
||||
(new-classes (filter (fn (c) (not (= c "")))
|
||||
(split cls-str " ")))
|
||||
(extra-keys (filter (fn (k) (not (= k "class")))
|
||||
(keys attrs))))
|
||||
(set! prev-classes new-classes)
|
||||
(set! prev-extra-keys extra-keys)
|
||||
;; Append new classes to element
|
||||
(when (not (empty? new-classes))
|
||||
(let ((current (or (dom-get-attr el "class") "")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and current (not (= current "")))
|
||||
(str current " " cls-str)
|
||||
cls-str))))
|
||||
;; Set extra attrs (data-tw, etc.) — simple overwrite
|
||||
(for-each (fn (k)
|
||||
(dom-set-attr el k (str (dict-get attrs k))))
|
||||
extra-keys)
|
||||
;; Flush any newly collected CSS rules to live stylesheet
|
||||
(flush-cssx-to-dom))
|
||||
;; No longer a spread — clear tracked state
|
||||
(do
|
||||
(set! prev-classes (list))
|
||||
(set! prev-extra-keys (list))))))))))
|
||||
|
||||
;; reactive-fragment — conditionally render a fragment based on a signal
|
||||
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
|
||||
(define reactive-fragment :effects [render mutation]
|
||||
|
||||
@@ -370,6 +370,8 @@
|
||||
:children (list
|
||||
{:label "Reference" :href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items}
|
||||
{:label "Examples" :href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items})}
|
||||
{:label "Spreads" :href "/sx/(geography.(spreads))"
|
||||
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, and the path to provide/context/emit!."}
|
||||
{:label "Marshes" :href "/sx/(geography.(marshes))"
|
||||
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
|
||||
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}
|
||||
|
||||
235
sx/sx/spreads.sx
Normal file
235
sx/sx/spreads.sx
Normal file
@@ -0,0 +1,235 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Spreads — child-to-parent communication across render boundaries
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~geography/spreads-content ()
|
||||
(~docs/page :title "Spreads"
|
||||
|
||||
(p :class "text-stone-500 text-sm italic mb-8"
|
||||
"A spread is a value that, when returned as a child of an element, "
|
||||
"injects attributes onto its parent instead of rendering as content. "
|
||||
"This inverts the normal direction of data flow: children tell parents how to look.")
|
||||
|
||||
;; =====================================================================
|
||||
;; I. The primitives
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Three primitives" :id "primitives"
|
||||
(p "The spread system has three orthogonal primitives. Each operates at a "
|
||||
"different level of the render pipeline.")
|
||||
|
||||
(~docs/subsection :title "1. make-spread / spread? / spread-attrs"
|
||||
(p "A spread is a value type. " (code "make-spread") " creates one from a dict of "
|
||||
"attributes. When the renderer encounters a spread as a child of an element, "
|
||||
"it merges the attrs onto the parent element instead of appending a DOM node.")
|
||||
(~docs/code :code "(defcomp ~highlight (&key colour)
|
||||
(make-spread {\"class\" (str \"highlight-\" colour)
|
||||
\"data-highlight\" colour}))")
|
||||
(p "Use it as a child of any element:")
|
||||
(~docs/code :code "(div (~highlight :colour \"yellow\")
|
||||
\"This div gets class=highlight-yellow\")")
|
||||
(p (code "class") " values are appended (space-joined). "
|
||||
(code "style") " values are appended (semicolon-joined). "
|
||||
"All other attributes overwrite."))
|
||||
|
||||
(~docs/subsection :title "2. collect! / collected / clear-collected!"
|
||||
(p "Render-time accumulators. Values are collected into named buckets "
|
||||
"during rendering and retrieved at flush points. Deduplication is automatic.")
|
||||
(~docs/code :code ";; Deep inside a component tree:
|
||||
(collect! \"cssx\" \".sx-bg-red-500{background-color:hsl(0,72%,53%)}\")
|
||||
|
||||
;; At the flush point (once, in the layout):
|
||||
(let ((rules (collected \"cssx\")))
|
||||
(clear-collected! \"cssx\")
|
||||
(raw! (str \"<style>\" (join \"\" rules) \"</style>\")))")
|
||||
(p "This is upward communication through the render tree: "
|
||||
"a deeply nested component contributes a CSS rule, and the layout "
|
||||
"emits all accumulated rules as a single " (code "<style>") " tag. "
|
||||
"No prop threading, no context providers, no global state."))
|
||||
|
||||
(~docs/subsection :title "3. reactive-spread (islands)"
|
||||
(p "Inside an island, when a spread's value depends on signals, "
|
||||
(code "reactive-spread") " tracks signal dependencies and surgically "
|
||||
"updates the parent element's attributes when signals change.")
|
||||
(~docs/code :code "(defisland ~themed-card ()
|
||||
(let ((theme (signal \"violet\")))
|
||||
(div (~cssx/tw :tokens (str \"bg-\" (deref theme) \"-500 p-4\"))
|
||||
(button :on-click (fn (e) (reset! theme \"rose\"))
|
||||
\"change theme\"))))")
|
||||
(p "When " (code "theme") " changes from " (code "\"violet\"") " to "
|
||||
(code "\"rose\"") ":")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li "Old classes (" (code "sx-bg-violet-500") ") are removed from the element")
|
||||
(li "New classes (" (code "sx-bg-rose-500") ") are added")
|
||||
(li "New CSS rules are JIT-generated and flushed to the live stylesheet")
|
||||
(li "No re-render. No VDOM. No diffing. Just attr surgery."))))
|
||||
|
||||
;; =====================================================================
|
||||
;; II. Orthogonality
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Orthogonality across boundaries" :id "orthogonality"
|
||||
(p "The three primitives operate at different levels of the render pipeline. "
|
||||
"They compose without knowing about each other.")
|
||||
|
||||
(~docs/table
|
||||
:headers (list "Primitive" "Direction" "Boundary" "When")
|
||||
:rows (list
|
||||
(list "spread" "child \u2192 parent" "element boundary" "render time")
|
||||
(list "collect!" "child \u2192 ancestor" "render tree" "render time")
|
||||
(list "reactive-spread" "child \u2192 parent" "element boundary" "signal change")))
|
||||
|
||||
(~docs/subsection :title "Server rendering (HTML adapter)"
|
||||
(p "On the server, " (code "~cssx/tw") " returns a spread. The HTML renderer "
|
||||
"merges the class onto the parent element. " (code "collect!") " accumulates "
|
||||
"CSS rules. " (code "~cssx/flush") " emits a single " (code "<style>") " tag. "
|
||||
"Result: static HTML with all classes and styles baked in."))
|
||||
|
||||
(~docs/subsection :title "Client hydration (DOM adapter)"
|
||||
(p "On the client, the same " (code "~cssx/tw") " call inside an island "
|
||||
"produces a spread that the DOM adapter merges onto the parent element. "
|
||||
"If the spread depends on signals, " (code "reactive-spread") " wraps it "
|
||||
"in an effect. Static spreads are applied once; reactive ones track deps."))
|
||||
|
||||
(~docs/subsection :title "Morph (navigation)"
|
||||
(p "When the server sends new HTML during SX navigation, the morph algorithm "
|
||||
"enters islands, finds lakes, updates server content. Reactive spreads are "
|
||||
"protected: " (code "data-sx-reactive-attrs") " tells the morph to skip "
|
||||
"attributes managed by signal effects. The server water flows through; "
|
||||
"the reactive rocks stay put."))
|
||||
|
||||
(~docs/subsection :title "The matrix"
|
||||
(~docs/table
|
||||
:headers (list "" "Server state" "Client state (signals)")
|
||||
:rows (list
|
||||
(list "Server rendering" "spread + collect! (pure hypermedia)" "SSR + hydrated reactive-spread")
|
||||
(list "Client rendering" "spread in wire format (aser)" "reactive-spread (live)")))))
|
||||
|
||||
;; =====================================================================
|
||||
;; III. CSSX as use case
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "CSSX: the first application" :id "cssx"
|
||||
(p (code "~cssx/tw") " is a component that uses all three primitives:")
|
||||
(~docs/code :code "(defcomp ~cssx/tw (tokens)
|
||||
(let ((token-list (filter (fn (t) (not (= t \"\")))
|
||||
(split (or tokens \"\") \" \")))
|
||||
(results (map cssx-process-token token-list))
|
||||
(valid (filter (fn (r) (not (nil? r))) results))
|
||||
(classes (map (fn (r) (get r \"cls\")) valid))
|
||||
(rules (map (fn (r) (get r \"rule\")) valid))
|
||||
(_ (for-each (fn (rule) (collect! \"cssx\" rule)) rules)))
|
||||
(if (empty? classes)
|
||||
nil
|
||||
(make-spread {\"class\" (join \" \" classes)
|
||||
\"data-tw\" (or tokens \"\")}))))")
|
||||
(p "It's a regular " (code "defcomp") ". It uses " (code "make-spread") " to "
|
||||
"inject classes onto its parent, " (code "collect!") " to accumulate CSS rules "
|
||||
"for batch flushing, and when called inside an island with signal-dependent "
|
||||
"tokens, " (code "reactive-spread") " makes it live.")
|
||||
(p "But " (code "~cssx/tw") " is just one instance. The same primitives enable:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li (code "~aria") " \u2014 reactive accessibility attributes driven by UI state")
|
||||
(li (code "~data-attrs") " \u2014 signal-driven data attributes for coordination")
|
||||
(li (code "~conditional-attrs") " \u2014 presence/absence of attributes based on state")
|
||||
(li "Any component that needs to inject attributes onto its parent")))
|
||||
|
||||
;; =====================================================================
|
||||
;; IV. Semantic variables
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "Semantic style variables" :id "variables"
|
||||
(p "Because " (code "~cssx/tw") " returns a spread, and spreads are values, "
|
||||
"you can bind them to names:")
|
||||
(~docs/code :code ";; Define once
|
||||
(define heading-style (~cssx/tw :tokens \"text-violet-700 text-2xl font-bold\"))
|
||||
(define nav-link (~cssx/tw :tokens \"text-stone-500 text-sm\"))
|
||||
(define card-base (~cssx/tw :tokens \"bg-stone-50 rounded-lg p-4\"))
|
||||
|
||||
;; Use everywhere
|
||||
(div card-base
|
||||
(h1 heading-style \"Title\")
|
||||
(a nav-link :href \"/\" \"Home\"))")
|
||||
(p "These are semantic names wrapping utility tokens. Change the definition, "
|
||||
"every use updates. No build step, no CSS-in-JS runtime. Just " (code "define") ".")
|
||||
(p "Namespacing prevents clashes \u2014 " (code "~app/heading") " vs "
|
||||
(code "~admin/heading") " are different components in different namespaces."))
|
||||
|
||||
;; =====================================================================
|
||||
;; V. What nothing else does
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "What nothing else does" :id "unique"
|
||||
(p "React can't do this. In React, attributes live in the parent's JSX. "
|
||||
"A child component cannot inject attrs onto its parent element. You'd need "
|
||||
"to lift state up, pass callbacks, use context, or reach for " (code "forwardRef")
|
||||
" \u2014 all of which couple the child to the parent's rendering logic.")
|
||||
(p "CSS-in-JS libraries (styled-components, Emotion) create " (em "wrapper elements")
|
||||
". They don't inject attrs onto an existing element from a child position. "
|
||||
"And they need a build step, a runtime, a theme provider.")
|
||||
(p "SX does it with three orthogonal primitives that already existed for other reasons:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li (strong "spread") " \u2014 child-to-parent attr injection (existed for component composition)")
|
||||
(li (strong "collect!") " \u2014 render-time accumulation (existed for CSS rule batching)")
|
||||
(li (strong "reactive-spread") " \u2014 just the obvious combination of spread + effect"))
|
||||
(p "No new concepts. No new runtime. No new API surface."))
|
||||
|
||||
;; =====================================================================
|
||||
;; VI. The deeper primitive
|
||||
;; =====================================================================
|
||||
|
||||
(~docs/section :title "The deeper primitive: provide / context / emit!" :id "provide"
|
||||
(p "Spread and collect are both instances of the same pattern: "
|
||||
(strong "child communicates upward through the render tree") ". "
|
||||
"The general form is " (code "provide") "/" (code "context") "/" (code "emit!")
|
||||
" \u2014 render-time dynamic scope.")
|
||||
|
||||
(~docs/subsection :title "The unification"
|
||||
(~docs/table
|
||||
:headers (list "Current" "General form" "Direction")
|
||||
:rows (list
|
||||
(list (code "collect! / collected") (code "emit! / emitted") "upward (child \u2192 scope)")
|
||||
(list (code "make-spread") (str "emit! into implicit " "\"parent-attrs\" provider") "upward (child \u2192 parent)")
|
||||
(list "(nothing yet)" (code "context") "downward (scope \u2192 child)")))
|
||||
(p (code "provide") " creates a named scope with a value (downward) and an accumulator (upward). "
|
||||
(code "context") " reads the value. " (code "emit!") " appends to the accumulator. "
|
||||
(code "emitted") " retrieves accumulated values.")
|
||||
(~docs/code :code ";; Downward: theme context
|
||||
(provide \"theme\" {:primary \"violet\" :font \"serif\"}
|
||||
(h1 :style (str \"color:\" (get (context \"theme\") :primary))
|
||||
\"Themed heading\"))
|
||||
|
||||
;; Upward: script accumulation (like collect!)
|
||||
(provide \"scripts\" nil
|
||||
(div
|
||||
(emit! \"scripts\" \"analytics.js\")
|
||||
(div (emit! \"scripts\" \"charts.js\") \"chart\"))
|
||||
(for-each (fn (s) (script :src s)) (emitted \"scripts\")))
|
||||
|
||||
;; Both at once
|
||||
(provide \"page\" {:title \"Home\"}
|
||||
(h1 (context \"page\" :title))
|
||||
(emit! \"page\" {:meta \"og:title\" :content \"Home\"})
|
||||
(for-each (fn (m) (meta :name (get m :meta) :content (get m :content)))
|
||||
(emitted \"page\")))"))
|
||||
|
||||
(~docs/subsection :title "What this means"
|
||||
(p "Three mechanisms collapse into one:")
|
||||
(ul :class "list-disc pl-5 space-y-1 text-stone-600"
|
||||
(li (code "collect!") " = " (code "emit!") " with deduplication")
|
||||
(li (code "spread") " = " (code "emit!") " into implicit parent-attrs provider")
|
||||
(li (code "collected") " = " (code "emitted"))
|
||||
(li (code "context") " = downward data flow (new capability \u2014 no current equivalent)"))
|
||||
(p "The reactive-spread we just built would naturally follow \u2014 the effect tracks "
|
||||
"signal deps in the " (code "emit!") " call, the provider accumulates, the element applies.")
|
||||
(p :class "text-stone-500 italic"
|
||||
"This is the planned next step. The current primitives (spread, collect, reactive-spread) "
|
||||
"work and are fully orthogonal. " (code "provide/context/emit!") " will be the deeper "
|
||||
"foundation they are reimplemented on top of."))
|
||||
|
||||
(~docs/note
|
||||
(p (strong "Plan: ") (code "provide") "/" (code "context") "/" (code "emit!") " is specced "
|
||||
"and ready to implement. Per-name stacks. Each entry has a value and an emitted list. "
|
||||
"Four primitives: " (code "provide") " (special form), " (code "context") ", "
|
||||
(code "emit!") ", " (code "emitted") ". Platform provides " (code "provide-push!")
|
||||
"/" (code "provide-pop!") ". See the implementation plan for details.")))))
|
||||
@@ -611,6 +611,16 @@
|
||||
"phase2" (~reactive-islands/phase2/reactive-islands-phase2-content)
|
||||
:else (~reactive-islands/index/reactive-islands-index-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Spreads section (under Geography)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage spreads-index
|
||||
:path "/geography/spreads/"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(spreads))" (~geography/spreads-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Marshes section (under Geography)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -267,6 +267,8 @@ _REDIRECT_PATTERNS = [
|
||||
lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/isomorphism/(.+?)/?$"),
|
||||
lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/spreads/?$"),
|
||||
"/sx/(geography.(spreads))"),
|
||||
(re.compile(r"^/geography/marshes/?$"),
|
||||
"/sx/(geography.(marshes))"),
|
||||
(re.compile(r"^/applications/cssx/(.+?)/?$"),
|
||||
|
||||
Reference in New Issue
Block a user