diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 1d89727..4551ffe 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -627,8 +627,9 @@ def sx_page(ctx: dict, page_sx: str, *, component_hash = get_component_hash() # Check if client already has this version cached (via cookie) + # In dev mode, always send full source so edits are visible immediately client_hash = _get_sx_comp_cookie() - if client_hash and client_hash == component_hash: + if not _is_dev_mode() and client_hash and client_hash == component_hash: # Client has current components cached — send empty source component_defs = "" else: @@ -675,7 +676,7 @@ def sx_page(ctx: dict, page_sx: str, *, # Style dictionary for client-side css primitive styles_hash = _get_style_dict_hash() client_styles_hash = _get_sx_styles_cookie() - if client_styles_hash and client_styles_hash == styles_hash: + if not _is_dev_mode() and client_styles_hash and client_styles_hash == styles_hash: styles_json = "" # Client has cached version else: styles_json = _build_style_dict_json() diff --git a/sx/content/highlight.py b/sx/content/highlight.py index 0b48ca4..e8f8ef4 100644 --- a/sx/content/highlight.py +++ b/sx/content/highlight.py @@ -236,14 +236,15 @@ def _tokenize_bash(code: str) -> list[tuple[str, str]]: return tokens -def highlight(code: str, language: str = "lisp") -> str: - """Highlight code in the given language. Returns sx source.""" +def highlight(code: str, language: str = "lisp"): + """Highlight code in the given language. Returns SxExpr for wire format.""" + from shared.sx.parser import SxExpr if language in ("lisp", "sx", "sexp"): - return highlight_sx(code) + return SxExpr(highlight_sx(code)) elif language in ("python", "py"): - return highlight_python(code) + return SxExpr(highlight_python(code)) elif language in ("bash", "sh", "shell"): - return highlight_bash(code) + return SxExpr(highlight_bash(code)) # Fallback: no highlighting, just escaped text escaped = code.replace("\\", "\\\\").replace('"', '\\"') - return f'(span "{escaped}")' + return SxExpr(f'(span "{escaped}")') diff --git a/sx/sx/docs.sx b/sx/sx/docs.sx index 43a9731..6c94165 100644 --- a/sx/sx/docs.sx +++ b/sx/sx/docs.sx @@ -2,14 +2,14 @@ (defcomp ~doc-placeholder (&key id) (div :id id - (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3" + (div :class "bg-stone-100 rounded p-4 mt-3" (p :class "text-stone-400 italic text-sm" "Trigger the demo to see the actual content.")))) (defcomp ~doc-oob-code (&key target-id text) (div :id target-id :sx-swap-oob "innerHTML" - (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto" - (pre :class "text-sm whitespace-pre-wrap" + (div :class "bg-stone-100 rounded p-4 mt-3" + (pre :class "text-sm whitespace-pre-wrap break-words" (code text))))) (defcomp ~doc-attr-table (&key title rows) @@ -17,7 +17,7 @@ (h3 :class "text-xl font-semibold text-stone-700" title) (div :class "overflow-x-auto rounded border border-stone-200" (table :class "w-full text-left text-sm" - (thead (tr :class "border-b border-stone-200 bg-stone-50" + (thead (tr :class "border-b border-stone-200 bg-stone-100" (th :class "px-3 py-2 font-medium text-stone-600" "Attribute") (th :class "px-3 py-2 font-medium text-stone-600" "Description") (th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?"))) @@ -28,7 +28,7 @@ (h3 :class "text-xl font-semibold text-stone-700" title) (div :class "overflow-x-auto rounded border border-stone-200" (table :class "w-full text-left text-sm" - (thead (tr :class "border-b border-stone-200 bg-stone-50" + (thead (tr :class "border-b border-stone-200 bg-stone-100" (th :class "px-3 py-2 font-medium text-stone-600" "Header") (th :class "px-3 py-2 font-medium text-stone-600" "Value") (th :class "px-3 py-2 font-medium text-stone-600" "Description"))) @@ -51,13 +51,13 @@ (when intro (p :class "text-stone-600 mb-6" intro)) (div :class "overflow-x-auto rounded border border-stone-200" (table :class "w-full text-left text-sm" - (thead (tr :class "border-b border-stone-200 bg-stone-50" + (thead (tr :class "border-b border-stone-200 bg-stone-100" (th :class "px-3 py-2 font-medium text-stone-600" (or col1 "Name")) (th :class "px-3 py-2 font-medium text-stone-600" (or col2 "Description")))) (tbody rows))))) (defcomp ~sx-docs-label () - (span :class "font-mono" "()")) + (span :class "font-mono" "()")) (defcomp ~doc-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)" diff --git a/sx/sx/essays.sx b/sx/sx/essays.sx index 3ca3f55..b25cb59 100644 --- a/sx/sx/essays.sx +++ b/sx/sx/essays.sx @@ -29,3 +29,106 @@ (defcomp ~essay-continuations () (~doc-page :title "Continuations and call/cc" (p :class "text-stone-500 text-sm italic mb-8" "What first-class continuations would enable in SX — on both the server (Python) and client (JavaScript).") (~doc-section :title "What is a continuation?" :id "what" (p :class "text-stone-600" "A continuation is the rest of a computation. At any point during evaluation, the continuation is everything that would happen next. call/cc (call-with-current-continuation) captures that \"rest of the computation\" as a first-class function that you can store, pass around, and invoke later — possibly multiple times.") (~doc-code :lang "lisp" :code ";; call/cc captures \"what happens next\" as k\n(+ 1 (call/cc (fn (k)\n (k 41)))) ;; → 42\n\n;; k is \"add 1 to this and return it\"\n;; (k 41) jumps back to that point with 41") (p :class "text-stone-600" "The key property: invoking a continuation abandons the current computation and resumes from where the continuation was captured. It is a controlled, first-class goto.")) (~doc-section :title "Server-side: suspendable rendering" :id "server" (p :class "text-stone-600" "The strongest case for continuations on the server is suspendable rendering — the ability for a component to pause mid-render while waiting for data, then resume exactly where it left off.") (~doc-code :lang "lisp" :code ";; Hypothetical: component suspends at a data boundary\n(defcomp ~user-profile (&key user-id)\n (let ((user (suspend (query :user user-id))))\n (div :class \"p-4\"\n (h2 (get user \"name\"))\n (p (get user \"bio\")))))") (p :class "text-stone-600" "Today, all data must be fetched before render_to_sx is called — Python awaits every query, assembles a complete data dict, then passes it to the evaluator. With continuations, the evaluator could yield at (suspend ...), the server flushes what it has so far, and resumes when the data arrives. This is React Suspense, but for server-side s-expressions.") (p :class "text-stone-600" "Streaming follows naturally. The server renders the page shell immediately, captures continuations at slow data boundaries, and flushes partial SX responses as each resolves. The client receives a stream of s-expression chunks and incrementally builds the DOM.") (p :class "text-stone-600" "Error boundaries also become first-class. Capture a continuation at a component boundary. If any child fails, invoke the continuation with fallback content instead of letting the exception propagate up through Python. The evaluator handles it, not the host language.")) (~doc-section :title "Client-side: linear async flows" :id "client" (p :class "text-stone-600" "On the client, continuations eliminate callback nesting for interactive flows. A confirmation dialog becomes a synchronous-looking expression:") (~doc-code :lang "lisp" :code "(let ((answer (call/cc show-confirm-dialog)))\n (if answer\n (delete-item item-id)\n (noop)))") (p :class "text-stone-600" "show-confirm-dialog receives the continuation, renders a modal, and wires the Yes/No buttons to invoke the continuation with true or false. The let binding reads top-to-bottom. No promises, no callbacks, no state machine.") (p :class "text-stone-600" "Multi-step forms — wizard-style UIs where each step captures a continuation. The back button literally invokes a saved continuation, restoring the exact evaluation state:") (~doc-code :lang "lisp" :code "(define wizard\n (fn ()\n (let* ((name (call/cc (fn (k) (render-step-1 k))))\n (email (call/cc (fn (k) (render-step-2 k name))))\n (plan (call/cc (fn (k) (render-step-3 k name email)))))\n (submit-registration name email plan))))") (p :class "text-stone-600" "Each render-step-N shows a form and wires the \"Next\" button to invoke k with the form value. The \"Back\" button invokes the previous step\'s continuation. The wizard logic is a straight-line let* binding, not a state machine.")) (~doc-section :title "Cooperative scheduling" :id "scheduling" (p :class "text-stone-600" "Delimited continuations (shift/reset rather than full call/cc) enable cooperative multitasking within the evaluator. A long render can yield control:") (~doc-code :lang "lisp" :code ";; Render a large list, yielding every 100 items\n(define render-chunk\n (fn (items n)\n (when (> n 100)\n (yield) ;; delimited continuation — suspends, resumes next frame\n (set! n 0))\n (when (not (empty? items))\n (render-item (first items))\n (render-chunk (rest items) (+ n 1)))))") (p :class "text-stone-600" "This is cooperative concurrency without threads, without promises, without requestAnimationFrame callbacks. The evaluator's trampoline loop already has the right shape — it just needs to be able to park a thunk and resume it later instead of immediately.")) (~doc-section :title "Undo as continuation" :id "undo" (p :class "text-stone-600" "If you capture a continuation before a state mutation, the continuation IS the undo operation. Invoking it restores the computation to exactly the state it was in before the mutation happened.") (~doc-code :lang "lisp" :code "(define with-undo\n (fn (action)\n (let ((restore (call/cc (fn (k) k))))\n (action)\n restore)))\n\n;; Usage:\n(let ((undo (with-undo (fn () (delete-item 42)))))\n ;; later...\n (undo \"anything\")) ;; item 42 is back") (p :class "text-stone-600" "No command pattern, no reverse operations, no state snapshots. The continuation captures the entire computation state. This is the most elegant undo mechanism possible — and the most expensive in memory, which is the trade-off.")) (~doc-section :title "Implementation" :id "implementation" (p :class "text-stone-600" "SX already has the foundation. The TCO trampoline returns thunks from tail positions — a continuation is a thunk that can be stored and resumed later instead of being immediately trampolined.") (p :class "text-stone-600" "The minimal implementation: delimited continuations via shift/reset. These are strictly less powerful than full call/cc but cover the practical use cases (suspense, cooperative scheduling, linear async flows) without the footguns (capturing continuations across async boundaries, re-entering completed computations).") (p :class "text-stone-600" "Full call/cc is also possible. The evaluator is already continuation-passing-style-adjacent — the thunk IS a continuation, just one that's always immediately invoked. Making it first-class means letting user code hold a reference to it.") (p :class "text-stone-600" "The key insight: having the primitive available doesn't make the evaluator harder to reason about. Only code that calls call/cc pays the complexity cost. Components that don't use continuations behave exactly as they do today.") (p :class "text-stone-600" "In fact, continuations can be easier to reason about than the hacks people build to avoid them. Without call/cc, you get callback pyramids, state machines with explicit transition tables, command pattern undo stacks, Promise chains, manual CPS transforms, and framework-specific hooks like React's useEffect/useSuspense/useTransition. Each is a partial, ad-hoc reinvention of continuations — with its own rules, edge cases, and leaky abstractions.") (p :class "text-stone-600" "A wizard form built with continuations is a straight-line let* binding. The same wizard built without them is a state machine with a current-step variable, a data accumulator, forward/backward transition logic, and a render function that switches on step number. The continuation version has fewer moving parts. It is more declarative. It is easier to read.") (p :class "text-stone-600" "The complexity doesn't disappear when you remove continuations from a language. It moves into user code, where it's harder to get right and harder to compose.")) (~doc-section :title "What this means for SX" :id "meaning" (p :class "text-stone-600" "SX started as a rendering language. TCO made it capable of arbitrary recursion. Macros made it extensible. Continuations would make it a full computational substrate — a language where control flow itself is a first-class value.") (p :class "text-stone-600" "The practical benefits are real: streaming server rendering, linear client-side interaction flows, cooperative scheduling, and elegant undo. These aren't theoretical — they're patterns that React, Clojure, and Scheme have proven work.") (p :class "text-stone-600" "The evaluator is already 90% of the way there. The remaining 10% unlocks an entirely new class of UI patterns — and eliminates an entire class of workarounds.")))) + +(defcomp ~essay-reflexive-web () + (~doc-page :title "The Reflexive Web" + (p :class "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.") + + (~doc-section :title "The missing property" :id "missing-property" + (p :class "text-stone-600" + "Every web technology stack shares one structural limitation: the system cannot inspect itself. A " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " component tree is opaque at runtime. An " (a :href "https://en.wikipedia.org/wiki/HTML" :class "text-violet-600 hover:underline" "HTML") " page cannot read its own structure and generate a new page from it. A " (a :href "https://en.wikipedia.org/wiki/JavaScript" :class "text-violet-600 hover:underline" "JavaScript") " bundle is compiled, minified, and sealed — the running code bears no resemblance to the source that produced it.") + (p :class "text-stone-600" + "The property these systems lack has a name: " (a :href "https://en.wikipedia.org/wiki/Reflection_(computer_programming)" :class "text-violet-600 hover:underline" "reflexivity") ". A reflexive system can represent itself, reason about its own structure, and modify itself based on that reasoning. " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " has had this property " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)#History" :class "text-violet-600 hover:underline" "since 1958") ". The web has never had it.") + (p :class "text-stone-600" + "SX is a complete Lisp. It has " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "homoiconicity") " — code is data, data is code. It has a " (a :href "/specs/core" :class "text-violet-600 hover:underline" "self-hosting specification") " — SX defined in SX. It has " (code "eval") " and " (code "quote") " and " (a :href "https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros" :class "text-violet-600 hover:underline" "macros") ". And it runs on the wire — the format that travels between server and client IS the language. This combination has consequences.")) + + (~doc-section :title "What homoiconicity changes" :id "homoiconicity" + (p :class "text-stone-600" + (code "(defcomp ~card (&key title body) (div :class \"p-4\" (h2 title) (p body)))") " — this is simultaneously a program that renders a card AND a list that can be inspected, transformed, and composed by other programs. The " (code "defcomp") " is not compiled away. It is not transpiled into something else. It persists as data at every stage: definition, transmission, evaluation, and rendering.") + (p :class "text-stone-600" + "This means:") + (ul :class "space-y-2 text-stone-600 mt-2" + (li (strong "The component registry is data.") " You can " (code "(map ...)") " over every component in the system, extract their parameter signatures, find all components that render a " (code "(table ...)") ", or generate documentation automatically — because the source IS the runtime representation.") + (li (strong "Programs can write programs.") " A " (a :href "https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros" :class "text-violet-600 hover:underline" "macro") " takes a list and returns a list. The returned list is code. The macro runs at expansion time and produces new components, new page definitions, new routing rules — indistinguishable from hand-written ones.") + (li (strong "The wire format is inspectable.") " What the server sends to the client is not a blob of serialized state. It is s-expressions that any system — browser, AI, another server — can parse, reason about, and act on."))) + + (~doc-section :title "AI as a native speaker" :id "ai-native" + (p :class "text-stone-600" + "Current AI integration with the web is mediated through layers of indirection. An " (a :href "https://en.wikipedia.org/wiki/Large_language_model" :class "text-violet-600 hover:underline" "LLM") " generates " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " components as strings that must be compiled, bundled, and deployed. It interacts with APIs through " (a :href "https://en.wikipedia.org/wiki/JSON" :class "text-violet-600 hover:underline" "JSON") " endpoints that require separate documentation. It reads HTML by scraping, because the markup was never meant to be machine-readable in a computational sense.") + (p :class "text-stone-600" + "In an SX web, the AI reads the same s-expressions the browser reads. The component definitions " (em "are") " the documentation — a " (code "defcomp") " declares its parameters, its structure, and its semantics in one expression. There is no " (a :href "https://en.wikipedia.org/wiki/OpenAPI_Specification" :class "text-violet-600 hover:underline" "Swagger spec") " describing an API. The API " (em "is") " the language, and the language is self-describing.") + (p :class "text-stone-600" + "An AI that understands SX understands the " (a :href "/specs/core" :class "text-violet-600 hover:underline" "spec") ". And the spec is written in SX. So the AI understands the definition of the language it is using, in the language it is using. This " (a :href "https://en.wikipedia.org/wiki/Reflexivity_(social_theory)" :class "text-violet-600 hover:underline" "reflexive") " property means the AI does not need a separate mental model for \"the web\" and \"the language\" — they are the same thing.")) + + (~doc-section :title "Live system modification" :id "live-modification" + (p :class "text-stone-600" + "Because code is data and the wire format is the language, modifying a running system is not deployment — it is evaluation. An AI reads " (code "(defcomp ~checkout-form ...)") ", understands what it does (because the semantics are specified in SX), modifies the expression, and sends it back. The system evaluates the new definition. No build step. No deploy pipeline. No container restart.") + (p :class "text-stone-600" + "This is not theoretical — it is how " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " development has always worked. You modify a function in the running image. The change takes effect immediately. What is new is putting this on the wire, across a network, with the AI as a participant rather than a tool.") + (p :class "text-stone-600" + "The implications for development itself are significant. An AI does not need to " (em "generate code") " that a human then reviews, commits, builds, and deploys. It can propose a modified expression, the human evaluates it in a sandbox, and if it works, the expression becomes the new definition. The feedback loop shrinks from hours to seconds.") + (p :class "text-stone-600" + "More radically: the distinction between \"development\" and \"operation\" dissolves. If the running system is a set of s-expressions, and those expressions can be inspected and modified at runtime, then there is no separate development environment. There is just the system, and agents — human or artificial — that interact with it.")) + + (~doc-section :title "Federated intelligence" :id "federated-intelligence" + (p :class "text-stone-600" + (a :href "https://en.wikipedia.org/wiki/ActivityPub" :class "text-violet-600 hover:underline" "ActivityPub") " carries activities between nodes. If those activities contain s-expressions, then what travels between servers is not just data — it is " (em "behaviour") ". Node A sends a component definition to Node B. Node B evaluates it. The result is rendered. The sender's intent is executable on the receiver's hardware.") + (p :class "text-stone-600" + "This is fundamentally different from sending " (a :href "https://en.wikipedia.org/wiki/JSON" :class "text-violet-600 hover:underline" "JSON") " payloads. JSON says \"here is some data, figure out what to do with it.\" An s-expression says \"here is what to do, and here is the data to do it with.\" The component definition and the data it operates on travel together.") + (p :class "text-stone-600" + "For AI agents in a federated network, this means an agent on one node can send " (em "capabilities") " to another node, not just requests. A component that renders a specific visualization. A macro that transforms data into a particular format. A function that implements a protocol. The network becomes a shared computational substrate where intelligence is distributed as executable expressions.")) + + (~doc-section :title "Programs writing programs writing programs" :id "meta-programs" + (p :class "text-stone-600" + "A macro is a function that takes code and returns code. An AI generating macros is writing programs that write programs. With " (code "eval") ", those generated programs can generate more programs at runtime. This is not a metaphor — it is the literal mechanism.") + (p :class "text-stone-600" + "The " (a :href "/essays/godel-escher-bach" :class "text-violet-600 hover:underline" "Gödel numbering") " parallel is not incidental. " (a :href "https://en.wikipedia.org/wiki/Kurt_G%C3%B6del" :class "text-violet-600 hover:underline" "Gödel") " showed that any sufficiently powerful formal system can encode statements about itself. A complete Lisp on the wire is a sufficiently powerful formal system. The web can make statements about itself — components that inspect other components, macros that rewrite the page structure, expressions that generate new expressions based on the current state of the system.") + (p :class "text-stone-600" + "Consider what this enables for AI:") + (ul :class "space-y-2 text-stone-600 mt-2" + (li (strong "Self-improving interfaces.") " An AI observes how users interact with a component (click patterns, error rates, abandonment). It reads the component definition — because it is data. It modifies the definition — because data is code. It evaluates the result. The interface adapts without human intervention.") + (li (strong "Generative composition.") " Given a data schema and a design intent, an AI generates not just a component but the " (em "macros") " that generate families of components. The macro is a template for templates. The output scales combinatorially.") + (li (strong "Cross-system reasoning.") " An AI reads component definitions from multiple federated nodes, identifies common patterns, and synthesizes abstractions that work across all of them. The shared language makes cross-system analysis trivial — it is all s-expressions."))) + + (~doc-section :title "The sandbox is everything" :id "sandbox" + (p :class "text-stone-600" + "The same " (a :href "https://en.wikipedia.org/wiki/Homoiconicity" :class "text-violet-600 hover:underline" "homoiconicity") " that makes this powerful makes it dangerous. Code-as-data means an AI can inject " (em "behaviour") ", not just content. A malicious expression evaluated in the wrong context could exfiltrate data, modify other components, or disrupt the system.") + (p :class "text-stone-600" + "This is why the " (a :href "/specs/primitives" :class "text-violet-600 hover:underline" "primitive set") " is the critical security boundary. The spec defines exactly which operations are available. A sandboxed evaluator that only exposes pure primitives (arithmetic, string operations, list manipulation) cannot perform I/O. Cannot access the network. Cannot modify the DOM outside its designated target. The language is " (a :href "https://en.wikipedia.org/wiki/Turing_completeness" :class "text-violet-600 hover:underline" "Turing-complete") " within the sandbox and powerless outside it.") + (p :class "text-stone-600" + "Different contexts grant different primitive sets. A component evaluated in a page slot gets rendering primitives. A macro gets code-transformation primitives. A federated expression from an untrusted node gets the minimal safe set. The sandbox is not bolted on — it is inherent in the language's architecture. What you can do depends on which primitives are in scope.") + (p :class "text-stone-600" + "This matters enormously for AI. An AI agent that can modify the running system must be constrained by the same sandbox mechanism that constrains any other expression. The security model does not distinguish between human-authored code and AI-generated code — both are s-expressions, both are evaluated by the same evaluator, both are subject to the same primitive restrictions.")) + + (~doc-section :title "Not self-aware — reflexive" :id "reflexive" + (p :class "text-stone-600" + "Is this a \"self-aware web\"? Probably not in the " (a :href "https://en.wikipedia.org/wiki/Consciousness" :class "text-violet-600 hover:underline" "consciousness") " sense. But the word we keep reaching for has a precise meaning: " (a :href "https://en.wikipedia.org/wiki/Reflexivity_(social_theory)" :class "text-violet-600 hover:underline" "reflexivity") ". A reflexive system can represent itself, reason about its own structure, and modify itself based on that reasoning.") + (p :class "text-stone-600" + "A " (a :href "https://en.wikipedia.org/wiki/React_(software)" :class "text-violet-600 hover:underline" "React") " app cannot read its own component tree as data and rewrite it. An HTML page cannot inspect its own structure and generate new pages. A JSON API cannot describe its own semantics in a way that is both human-readable and machine-executable.") + (p :class "text-stone-600" + "SX can do all of these things — because there is no distinction between the program and its representation. The source code, the wire format, the runtime state, and the data model are all the same thing: " (a :href "https://en.wikipedia.org/wiki/S-expression" :class "text-violet-600 hover:underline" "s-expressions") ".") + (p :class "text-stone-600" + "What AI adds to this is not awareness but " (em "agency") ". The system has always been reflexive — Lisp has been reflexive for seven decades. What is new is having an agent that can exploit that reflexivity at scale: reading the entire system state as data, reasoning about it, generating modifications, and evaluating the results — all in the native language of the system itself.")) + + (~doc-section :title "The Lisp that escaped the REPL" :id "escaped-repl" + (p :class "text-stone-600" + (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " has been reflexive since " (a :href "https://en.wikipedia.org/wiki/John_McCarthy_(computer_scientist)" :class "text-violet-600 hover:underline" "McCarthy") ". What kept it contained was the boundary of the " (a :href "https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop" :class "text-violet-600 hover:underline" "REPL") " — a single process, a single machine, a single user. The s-expressions lived inside Emacs, inside a Clojure JVM, inside a Scheme interpreter. They did not travel.") + (p :class "text-stone-600" + "SX puts s-expressions on the wire. Between server and client. Between federated nodes. Between human and AI. The reflexive property escapes the process boundary and becomes a property of the " (em "network") ".") + (p :class "text-stone-600" + "A network of nodes that share a reflexive language is a qualitatively different system from a network of nodes that exchange inert data. The former can reason about itself, modify itself, and evolve. The latter can only shuttle payloads.") + (p :class "text-stone-600" + "Whether this constitutes anything approaching awareness is a philosophical question. What is not philosophical is the engineering consequence: a web built on s-expressions is a web that AI can participate in as a " (em "native citizen") ", not as a tool bolted onto the side. The language is the interface. The interface is the language. And the language can describe itself.")) + + (~doc-section :title "What this opens up" :id "possibilities" + (p :class "text-stone-600" + "Concretely:") + (ul :class "space-y-3 text-stone-600 mt-2" + (li (strong "AI-native development environments.") " The IDE is a web page. The web page is s-expressions. The AI reads and writes s-expressions. There is no translation layer between what the AI thinks and what the system executes. " (a :href "https://en.wikipedia.org/wiki/Pair_programming" :class "text-violet-600 hover:underline" "Pair programming") " with an AI becomes pair evaluation.") + (li (strong "Adaptive interfaces.") " Components that observe their own usage patterns and propose modifications. The AI reads the component (data), the interaction logs (data), and generates a modified component (data). Human approves or rejects. The loop is native to the system.") + (li (strong "Semantic federation.") " Nodes exchange not just content but " (em "understanding") ". A component definition carries its own semantics. An AI on a receiving node can reason about what a foreign component does without documentation, because the definition is self-describing.") + (li (strong "Emergent protocols.") " Two AI agents on different nodes, speaking SX, can negotiate new interaction patterns by exchanging macros. The protocol is not predefined — it emerges from the conversation between agents, expressed in the shared language.") + (li (strong "Composable trust.") " The sandbox mechanism means you can give an AI agent " (em "exactly") " the capabilities it needs — no more. Trust is expressed as a set of available primitives, not as an all-or-nothing API key.")) + (p :class "text-stone-600" + "None of these require breakthroughs in AI. They require a web that speaks a reflexive language. " (a :href "https://en.wikipedia.org/wiki/Lisp_(programming_language)" :class "text-violet-600 hover:underline" "Lisp") " solved the language problem in 1958. SX solves the distribution problem. AI provides the agency. The three together produce something that none of them achieves alone: a web that can reason about itself.")))) diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index 26395e4..0cc5f2b 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -11,7 +11,8 @@ (dict :label "Reference" :href "/reference/") (dict :label "Protocols" :href "/protocols/wire-format") (dict :label "Examples" :href "/examples/click-to-load") - (dict :label "Essays" :href "/essays/sx-sucks")))) + (dict :label "Essays" :href "/essays/sx-sucks") + (dict :label "Specs" :href "/specs/core")))) (<> (map (lambda (item) (~nav-link :href (get item "href") diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 6e77796..162d746 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -64,7 +64,15 @@ (dict :label "The SX Manifesto" :href "/essays/sx-manifesto") (dict :label "Tail-Call Optimization" :href "/essays/tail-call-optimization") (dict :label "Continuations" :href "/essays/continuations") - (dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach"))) + (dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach") + (dict :label "The Reflexive Web" :href "/essays/reflexive-web"))) + +(define specs-nav-items (list + (dict :label "Core" :href "/specs/core") + (dict :label "Parser" :href "/specs/parser") + (dict :label "Evaluator" :href "/specs/evaluator") + (dict :label "Primitives" :href "/specs/primitives") + (dict :label "Renderer" :href "/specs/renderer"))) ;; Find the current nav label for a slug by matching href suffix. ;; Returns the label string or nil if no match. diff --git a/sx/sx/specs.sx b/sx/sx/specs.sx new file mode 100644 index 0000000..6e83467 --- /dev/null +++ b/sx/sx/specs.sx @@ -0,0 +1,36 @@ +;; Spec viewer components — display canonical SX specification source + +(defcomp ~spec-core-content (&key spec-files) + (~doc-page :title "SX Core Specification" + (p :class "text-stone-600 mb-6" + "SX is defined in SX. These four files constitute the canonical, self-hosting specification of the language. Each file is both documentation and executable definition — bootstrap compilers read them to generate native implementations.") + (div :class "space-y-8" + (map (fn (spec) + (div :class "space-y-3" + (div :class "flex items-baseline gap-3" + (h2 :class "text-2xl font-semibold text-stone-800" + (a :href (get spec "href") + :sx-get (get spec "href") :sx-target "#main-panel" :sx-select "#main-panel" + :sx-swap "outerHTML" :sx-push-url "true" + :class "text-violet-700 hover:text-violet-900 underline" + (get spec "title"))) + (span :class "text-sm text-stone-400 font-mono" (get spec "filename"))) + (p :class "text-stone-600" (get spec "desc")) + (div :class "bg-stone-100 rounded-lg p-5 max-h-72 overflow-y-auto" + (pre :class "text-xs leading-relaxed whitespace-pre-wrap break-words" + (code (highlight (get spec "source") "sx")))))) + spec-files)))) + +(defcomp ~spec-detail-content (&key spec-title spec-desc spec-filename spec-source) + (~doc-page :title spec-title + (div :class "flex items-baseline gap-3 mb-4" + (span :class "text-sm text-stone-400 font-mono" spec-filename) + (span :class "text-sm text-stone-500" spec-desc)) + (div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" + (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" + (code (highlight spec-source "sx")))))) + +(defcomp ~spec-not-found (&key slug) + (~doc-page :title "Spec Not Found" + (p :class "text-stone-600" + "No specification found for \"" slug "\". This spec may not exist yet."))) diff --git a/sx/sxc/docs.sx b/sx/sxc/docs.sx index 93d8208..3beece4 100644 --- a/sx/sxc/docs.sx +++ b/sx/sxc/docs.sx @@ -16,8 +16,8 @@ children)) (defcomp ~doc-code (&key code) - (div :class "bg-stone-50 border border-stone-200 rounded-lg p-4 overflow-x-auto" - (pre :class "text-sm" (code code)))) + (div :class "bg-stone-100 rounded-lg p-5 mx-auto max-w-3xl" + (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))) (defcomp ~doc-note (&key &rest children) (div :class "border-l-4 border-violet-400 bg-violet-50 p-4 text-stone-700 text-sm" @@ -27,7 +27,7 @@ (div :class "overflow-x-auto rounded border border-stone-200" (table :class "w-full text-left text-sm" (thead - (tr :class "border-b border-stone-200 bg-stone-50" + (tr :class "border-b border-stone-200 bg-stone-100" (map (fn (h) (th :class "px-3 py-2 font-medium text-stone-600" h)) headers))) (tbody (map (fn (row) diff --git a/sx/sxc/examples.sx b/sx/sxc/examples.sx index 5df7402..d4f9e5e 100644 --- a/sx/sxc/examples.sx +++ b/sx/sxc/examples.sx @@ -2,24 +2,24 @@ (defcomp ~example-card (&key title description &rest children) (div :class "border border-stone-200 rounded-lg overflow-hidden" - (div :class "bg-stone-50 px-4 py-3 border-b border-stone-200" + (div :class "bg-stone-100 px-4 py-3 border-b border-stone-200" (h3 :class "font-semibold text-stone-800" title) (when description (p :class "text-sm text-stone-500 mt-1" description))) (div :class "p-4" children))) (defcomp ~example-demo (&key &rest children) - (div :class "border border-dashed border-stone-300 rounded p-4 bg-white" children)) + (div :class "border border-dashed border-stone-300 rounded p-4 bg-stone-100" children)) (defcomp ~example-source (&key code) - (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto" - (pre :class "text-sm" (code code)))) + (div :class "bg-stone-100 rounded p-5 mt-3 mx-auto max-w-3xl" + (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code)))) ;; --- Click to load demo --- (defcomp ~click-to-load-demo () (div :class "space-y-4" - (div :id "click-result" :class "p-4 rounded bg-stone-50 text-stone-500 text-center" + (div :id "click-result" :class "p-4 rounded bg-stone-100 text-stone-500 text-center" "Click the button to load content.") (button :sx-get "/examples/api/click" @@ -50,7 +50,7 @@ (button :type "submit" :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Submit")) - (div :id "form-result" :class "p-3 rounded bg-stone-50 text-stone-500 text-sm text-center" + (div :id "form-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm text-center" "Submit the form to see the result."))) (defcomp ~form-result (&key name) @@ -66,7 +66,7 @@ :sx-get "/examples/api/poll" :sx-trigger "load, every 2s" :sx-swap "innerHTML" - :class "p-4 rounded border border-stone-200 bg-white text-center font-mono" + :class "p-4 rounded border border-stone-200 bg-stone-100 text-center font-mono" "Loading..."))) (defcomp ~poll-result (&key time count) @@ -145,10 +145,10 @@ (defcomp ~oob-demo () (div :class "space-y-4" (div :class "grid grid-cols-2 gap-4" - (div :id "oob-box-a" :class "p-4 rounded border border-stone-200 bg-white text-center" + (div :id "oob-box-a" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center" (p :class "text-stone-500" "Box A") (p :class "text-sm text-stone-400" "Waiting...")) - (div :id "oob-box-b" :class "p-4 rounded border border-stone-200 bg-white text-center" + (div :id "oob-box-b" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center" (p :class "text-stone-500" "Box B") (p :class "text-sm text-stone-400" "Waiting..."))) (button @@ -167,7 +167,7 @@ :sx-get "/examples/api/lazy" :sx-trigger "load" :sx-swap "innerHTML" - :class "p-6 rounded border border-stone-200 bg-stone-50 text-center" + :class "p-6 rounded border border-stone-200 bg-stone-100 text-center" (div :class "animate-pulse space-y-2" (div :class "h-4 bg-stone-200 rounded w-3/4 mx-auto") (div :class "h-4 bg-stone-200 rounded w-1/2 mx-auto"))))) @@ -328,7 +328,7 @@ (p :class "text-sm text-stone-400" "Messages will appear here.")))) (defcomp ~reset-message (&key message time) - (div :class "px-3 py-2 bg-stone-50 rounded text-sm text-stone-700" + (div :class "px-3 py-2 bg-stone-100 rounded text-sm text-stone-700" (str "[" time "] " message))) ;; --- Edit row demo --- @@ -488,7 +488,7 @@ :sx-swap "innerHTML" :class "px-3 py-1.5 bg-stone-600 text-white rounded text-sm hover:bg-stone-700" "Full Dashboard")) - (div :id "filter-target" :class "border border-stone-200 rounded p-4 bg-white" + (div :id "filter-target" :class "border border-stone-200 rounded p-4 bg-stone-100" (p :class "text-sm text-stone-400" "Click a button to load content.")))) ;; --- Tabs demo --- @@ -525,7 +525,7 @@ :sx-swap "innerHTML" :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Load with animation") - (div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-white text-center" + (div :id "anim-target" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center" (p :class "text-stone-400" "Content will fade in here.")))) (defcomp ~anim-result (&key color time) @@ -552,7 +552,7 @@ :sx-get "/examples/api/dialog/close" :sx-target "#dialog-container" :sx-swap "innerHTML") - (div :class "relative bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4" + (div :class "relative bg-stone-100 rounded-lg shadow-xl p-6 max-w-md w-full mx-4 space-y-4" (h3 :class "text-lg font-semibold text-stone-800" title) (p :class "text-stone-600" message) (div :class "flex justify-end gap-2" @@ -573,23 +573,23 @@ (defcomp ~keyboard-shortcuts-demo () (div :class "space-y-4" - (div :class "p-4 rounded border border-stone-200 bg-stone-50" + (div :class "p-4 rounded border border-stone-200 bg-stone-100" (p :class "text-sm text-stone-600 font-medium mb-2" "Keyboard shortcuts:") (div :class "flex gap-4" (div :class "flex items-center gap-1" - (kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "s") + (kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "s") (span :class "text-sm text-stone-500" "Search")) (div :class "flex items-center gap-1" - (kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "n") + (kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "n") (span :class "text-sm text-stone-500" "New item")) (div :class "flex items-center gap-1" - (kbd :class "px-2 py-0.5 bg-white border border-stone-300 rounded text-xs font-mono" "h") + (kbd :class "px-2 py-0.5 bg-stone-100 border border-stone-300 rounded text-xs font-mono" "h") (span :class "text-sm text-stone-500" "Help")))) (div :id "kbd-target" :sx-get "/examples/api/keyboard?key=s" :sx-trigger "keyup[key=='s'&&!event.target.matches('input,textarea')] from:body" :sx-swap "innerHTML" - :class "p-4 rounded border border-stone-200 bg-white text-center" + :class "p-4 rounded border border-stone-200 bg-stone-100 text-center" (p :class "text-stone-400 text-sm" "Press a shortcut key...")) (div :sx-get "/examples/api/keyboard?key=n" :sx-trigger "keyup[key=='n'&&!event.target.matches('input,textarea')] from:body" @@ -675,7 +675,7 @@ (button :type "submit" :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Submit as JSON")) - (div :id "json-result" :class "p-3 rounded bg-stone-50 text-stone-500 text-sm" + (div :id "json-result" :class "p-3 rounded bg-stone-100 text-stone-500 text-sm" "Submit the form to see the server echo the parsed JSON."))) (defcomp ~json-result (&key body content-type) @@ -697,7 +697,7 @@ :sx-vals "{\"source\": \"button\", \"version\": \"2.0\"}" :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Send with vals") - (div :id "vals-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400" + (div :id "vals-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400" "Click to see server-received values.")) (div :class "space-y-2" (h4 :class "text-sm font-semibold text-stone-700" "sx-headers — send custom headers") @@ -708,7 +708,7 @@ :sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"} :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Send with headers") - (div :id "headers-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400" + (div :id "headers-result" :class "p-3 rounded bg-stone-100 text-sm text-stone-400" "Click to see server-received headers.")))) (defcomp ~echo-result (&key label items) @@ -729,7 +729,7 @@ :class "sx-loading-btn px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm flex items-center gap-2" (span :class "sx-spinner w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin") (span "Load slow endpoint")) - (div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-white text-center" + (div :id "loading-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center" (p :class "text-stone-400 text-sm" "Click the button — it takes 2 seconds.")))) (defcomp ~loading-result (&key time) @@ -749,7 +749,7 @@ :sx-sync "replace" :placeholder "Type to search (random delay 0.5-2s)..." :class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500") - (div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-white" + (div :id "sync-result" :class "p-4 rounded border border-stone-200 bg-stone-100" (p :class "text-sm text-stone-400" "Type to trigger requests — stale ones get aborted.")))) (defcomp ~sync-result (&key query delay) @@ -768,7 +768,7 @@ :sx-retry "exponential:1000:8000" :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Call flaky endpoint") - (div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-white text-center" + (div :id "retry-result" :class "p-4 rounded border border-stone-200 bg-stone-100 text-center" (p :class "text-stone-400 text-sm" "Endpoint fails twice, succeeds on 3rd attempt.")))) (defcomp ~retry-result (&key attempt message) diff --git a/sx/sxc/handlers/examples.sx b/sx/sxc/handlers/examples.sx index b61c25b..bac4eea 100644 --- a/sx/sxc/handlers/examples.sx +++ b/sx/sxc/handlers/examples.sx @@ -235,7 +235,7 @@ (div :class "p-3 bg-amber-50 rounded text-center" (p :class "text-2xl font-bold text-amber-700" "$4.2k") (p :class "text-xs text-amber-600" "Revenue"))) - (div :id "dash-footer" :class "p-3 bg-stone-50 rounded" + (div :id "dash-footer" :class "p-3 bg-stone-100 rounded" (p :class "text-sm text-stone-500" "Last updated: " now))))) ;; --------------------------------------------------------------------------- diff --git a/sx/sxc/home.sx b/sx/sxc/home.sx index c5899da..0a6bfdf 100644 --- a/sx/sxc/home.sx +++ b/sx/sxc/home.sx @@ -3,14 +3,14 @@ (defcomp ~sx-hero (&key &rest children) (div :class "max-w-4xl mx-auto px-6 py-16 text-center" (h1 :class "text-5xl font-bold text-stone-900 mb-4" - (span :class "text-violet-600 font-mono" "()")) + (span :class "text-violet-600 font-mono" "()")) (p :class "text-2xl text-stone-600 mb-8" "s-expressions for the web") (p :class "text-lg text-stone-500 max-w-2xl mx-auto mb-12" "A hypermedia-driven UI engine that combines htmx's server-first philosophy " "with React's component model. S-expressions over the wire — no HTML, no JavaScript frameworks.") - (div :class "bg-stone-50 border border-stone-200 rounded-lg p-6 text-left font-mono text-sm overflow-x-auto" - (pre :class "leading-relaxed" children)))) + (div :class "bg-stone-100 rounded-lg p-6 text-left font-mono text-sm mx-auto max-w-2xl" + (pre :class "leading-relaxed whitespace-pre-wrap" children)))) (defcomp ~sx-philosophy () (div :class "max-w-4xl mx-auto px-6 py-12" diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index eafa11f..6f46692 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -240,4 +240,42 @@ "tail-call-optimization" (~essay-tail-call-optimization) "continuations" (~essay-continuations) "godel-escher-bach" (~essay-godel-escher-bach) + "reflexive-web" (~essay-reflexive-web) :else (~essay-sx-sucks))) + +;; --------------------------------------------------------------------------- +;; Specs section +;; --------------------------------------------------------------------------- + +(defpage specs-index + :path "/specs/" + :auth :public + :layout (:sx-section + :section "Specs" + :sub-label "Specs" + :sub-href "/specs/core" + :sub-nav (~section-nav :items specs-nav-items :current "Core") + :selected "Core") + :data (spec-data "core") + :content (~spec-core-content :spec-files spec-files)) + +(defpage specs-page + :path "/specs/" + :auth :public + :layout (:sx-section + :section "Specs" + :sub-label "Specs" + :sub-href "/specs/core" + :sub-nav (~section-nav :items specs-nav-items + :current (find-current specs-nav-items slug)) + :selected (or (find-current specs-nav-items slug) "")) + :data (spec-data slug) + :content (if spec-not-found + (~spec-not-found :slug slug) + (case slug + "core" (~spec-core-content :spec-files spec-files) + :else (~spec-detail-content + :spec-title spec-title + :spec-desc spec-desc + :spec-filename spec-filename + :spec-source spec-source)))) diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 0ac82f9..f7ce796 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -16,6 +16,7 @@ def _register_sx_helpers() -> None: "primitives-data": _primitives_data, "reference-data": _reference_data, "attr-detail-data": _attr_detail_data, + "spec-data": _spec_data, }) @@ -103,6 +104,63 @@ def _reference_data(slug: str) -> dict: } +_SPEC_FILES = { + "parser": ("parser.sx", "Parser", "Tokenization and parsing of SX source text into AST."), + "evaluator": ("eval.sx", "Evaluator", "Tree-walking evaluation of SX expressions."), + "primitives": ("primitives.sx", "Primitives", "All built-in pure functions and their signatures."), + "renderer": ("render.sx", "Renderer", "Rendering evaluated expressions to DOM, HTML, or SX wire format."), +} + + +def _spec_data(slug: str) -> dict: + """Return spec file source and highlighted version for display.""" + import os + from content.highlight import highlight as _highlight + + ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref") + # Normalise — inside container shared is at /app/shared + if not os.path.isdir(ref_dir): + ref_dir = "/app/shared/sx/ref" + + base = {"spec-not-found": None, "spec-title": None, "spec-desc": None, + "spec-filename": None, "spec-source": None, "spec-files": None} + + if slug == "core": + specs = [] + for key in ("parser", "evaluator", "primitives", "renderer"): + filename, title, desc = _SPEC_FILES[key] + filepath = os.path.join(ref_dir, filename) + source = _read_spec(filepath) + specs.append({ + "title": title, + "desc": desc, + "filename": filename, + "source": source, + "href": f"/specs/{key}", + }) + return {**base, "spec-title": "SX Core Specification", "spec-files": specs} + + info = _SPEC_FILES.get(slug) + if not info: + return {**base, "spec-not-found": True} + + filename, title, desc = info + filepath = os.path.join(ref_dir, filename) + source = _read_spec(filepath) + return {**base, + "spec-title": title, "spec-desc": desc, + "spec-filename": filename, "spec-source": source} + + +def _read_spec(filepath: str) -> str: + """Read a spec file, returning empty string if missing.""" + try: + with open(filepath, encoding="utf-8") as f: + return f.read() + except FileNotFoundError: + return ";; spec file not found" + + def _attr_detail_data(slug: str) -> dict: """Return attribute detail data for a specific attribute slug. diff --git a/sx/sxc/reference.sx b/sx/sxc/reference.sx index 1fa16e8..bf36f65 100644 --- a/sx/sxc/reference.sx +++ b/sx/sxc/reference.sx @@ -16,7 +16,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Load server time") (div :id "ref-get-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click to load."))) ;; --------------------------------------------------------------------------- @@ -36,7 +36,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Greet")) (div :id "ref-post-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Submit to see greeting."))) ;; --------------------------------------------------------------------------- @@ -45,7 +45,7 @@ (defcomp ~ref-put-demo () (div :id "ref-put-view" - (div :class "flex items-center justify-between p-3 bg-stone-50 rounded" + (div :class "flex items-center justify-between p-3 bg-stone-100 rounded" (span :class "text-stone-700 text-sm" "Status: " (strong "draft")) (button :sx-put "/reference/api/status" @@ -83,7 +83,7 @@ (defcomp ~ref-patch-demo () (div :id "ref-patch-view" :class "space-y-2" - (div :class "p-3 bg-stone-50 rounded" + (div :class "p-3 bg-stone-100 rounded" (span :class "text-stone-700 text-sm" "Theme: " (strong :id "ref-patch-val" "light"))) (div :class "flex gap-2" (button :sx-patch "/reference/api/theme" @@ -93,7 +93,7 @@ (button :sx-patch "/reference/api/theme" :sx-vals "{\"theme\": \"light\"}" :sx-target "#ref-patch-val" :sx-swap "innerHTML" - :class "px-3 py-1 bg-white border border-stone-300 text-stone-700 rounded text-sm" "Light")))) + :class "px-3 py-1 bg-stone-100 border border-stone-300 text-stone-700 rounded text-sm" "Light")))) ;; --------------------------------------------------------------------------- ;; sx-trigger @@ -108,7 +108,7 @@ :sx-swap "innerHTML" :class "w-full px-3 py-2 border border-stone-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-violet-500") (div :id "ref-trigger-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Start typing to trigger a search."))) ;; --------------------------------------------------------------------------- @@ -186,7 +186,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Load (selecting #the-content)") (div :id "ref-select-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Only the selected fragment will appear here."))) ;; --------------------------------------------------------------------------- @@ -242,7 +242,7 @@ (p :class "text-xs text-stone-400" "With sync:replace, each new keystroke aborts the in-flight request.") (div :id "ref-sync-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Type to see only the latest result."))) ;; --------------------------------------------------------------------------- @@ -262,7 +262,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Upload")) (div :id "ref-encoding-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Select a file and submit."))) ;; --------------------------------------------------------------------------- @@ -278,7 +278,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Send with custom headers") (div :id "ref-headers-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click to see echoed headers."))) ;; --------------------------------------------------------------------------- @@ -302,7 +302,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Filter")) (div :id "ref-include-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click Filter — the select value is included in the request."))) ;; --------------------------------------------------------------------------- @@ -318,7 +318,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Send with extra values") (div :id "ref-vals-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click to see echoed values."))) ;; --------------------------------------------------------------------------- @@ -369,7 +369,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Click me") (div :id "ref-on-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click the button — runs JavaScript, no server request."))) ;; --------------------------------------------------------------------------- @@ -385,7 +385,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700" "Call flaky endpoint") (div :id "ref-retry-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "This endpoint fails 2 out of 3 times. sx-retry retries automatically."))) ;; --------------------------------------------------------------------------- @@ -440,7 +440,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Hover then click (preloaded)") (div :id "ref-preload-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Hover over the button first, then click — the response is instant."))) ;; --------------------------------------------------------------------------- @@ -461,7 +461,7 @@ (input :id "ref-preserved-input" :sx-preserve "true" :type "text" :placeholder "Type here — preserved across swaps" :class "w-full px-3 py-2 border border-stone-300 rounded text-sm") - (div :class "p-2 bg-stone-50 rounded text-sm text-stone-600" + (div :class "p-2 bg-stone-100 rounded text-sm text-stone-600" "This text will be replaced on swap.")))) ;; --------------------------------------------------------------------------- @@ -484,7 +484,7 @@ :style "display: none" "Loading...")) (div :id "ref-indicator-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click to load (indicator shows during request)."))) ;; --------------------------------------------------------------------------- @@ -506,7 +506,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Submit")) (div :id "ref-validate-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Submit with invalid/empty email to see validation."))) ;; --------------------------------------------------------------------------- @@ -526,7 +526,7 @@ (p :class "text-sm text-amber-800" "This subtree has sx-ignore — it won't change.") (input :type "text" :placeholder "Type here — ignored during swap" :class "mt-1 w-full px-2 py-1 border border-amber-300 rounded text-sm")) - (div :class "p-2 bg-stone-50 rounded text-sm text-stone-600" + (div :class "p-2 bg-stone-100 rounded text-sm text-stone-600" "This text WILL be replaced on swap.")))) ;; --------------------------------------------------------------------------- @@ -566,7 +566,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Load (replaces URL)") (div :id "ref-replurl-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click to load — URL changes but no new history entry."))) ;; --------------------------------------------------------------------------- @@ -586,7 +586,7 @@ "Click (disables during request)") (span :class "text-xs text-stone-400" "Button is disabled while request is in-flight.")) (div :id "ref-diselt-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click the button to see it disable during the request."))) ;; --------------------------------------------------------------------------- @@ -603,7 +603,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Prompt & send") (div :id "ref-prompt-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Click to enter a name via prompt — it is sent as the SX-Prompt header."))) ;; --------------------------------------------------------------------------- @@ -626,7 +626,7 @@ :class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm" "Submit")) (div :id "ref-params-result" - :class "p-3 rounded bg-stone-50 text-stone-400 text-sm" + :class "p-3 rounded bg-stone-100 text-stone-400 text-sm" "Only 'name' will be sent — 'secret' is filtered by sx-params."))) ;; --------------------------------------------------------------------------- @@ -639,7 +639,7 @@ :sx-sse-swap "time" :sx-swap "innerHTML" (div :id "ref-sse-result" - :class "p-3 rounded bg-stone-50 text-stone-600 text-sm font-mono" + :class "p-3 rounded bg-stone-100 text-stone-600 text-sm font-mono" "Connecting to SSE stream...")) (p :class "text-xs text-stone-400" "Server pushes time updates every 2 seconds via Server-Sent Events.")))