- Re-read verb URL from element attributes at execution time so morphed
nav links navigate to the correct destination
- Reset retry backoff on fresh requests; skip error modal when sx-retry
handles the failure
- Strip attribute selectors in CSS registry so aria-selected:* classes
resolve correctly for on-demand CSS
- Add @css annotations for dynamic aria-selected variant classes
- Add SX docs integration test suite (102 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add dict recursion to _convert_result for service methods returning dict[K, list[DTO]]
- New container-cards.sx: parses post_ids/slugs, calls confirmed-entries-for-posts, emits card-widget markers
- New account-page.sx: dispatches on slug for tickets/bookings panels with status pills and empty states
- Fix blog _parse_card_fragments to handle SxExpr via str() cast
- Remove events Python fragment handlers and simplify app.py to plain auto_mount
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fragment read API is now fully declarative — every handler is a defhandler
s-expression dispatched through one shared auto_mount_fragment_handlers()
function. Replaces 8 near-identical blueprint files (~35 lines each) with
a single function call per service. Events Python handlers (container-cards,
account-page) extracted to a standalone module.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defpages are now declared with absolute paths in .sx files and auto-mounted
directly on the Quart app, removing ~850 lines of blueprint mount_pages calls,
before_request hooks, and g.* wrapper boilerplate. A new page = one defpage
declaration, nothing else.
Infrastructure:
- async_eval awaits coroutine results from callable dispatch
- auto_mount_pages() mounts all registered defpages on the app
- g._defpage_ctx pattern passes helper data to layout context
Migrated: sx, account, orders, federation, cart, market, events, blog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents why sx stays server-driven by default, maps React features
to sx equivalents, and defines targeted escape hatches for the few
interactions that genuinely need client-side state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add primitives (replace, strip-tags, slice, csrf-token), convert all
social blueprint routes and federation profile to SX content builders,
delete 12 unused Jinja templates and social_lite layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per-attribute documentation pages at /reference/attributes/<slug> with:
- Live interactive demos (demo components in reference.sx)
- S-expression source code display
- Server handler code shown as s-expressions (defhandlers in handlers/reference.sx)
- Wire response display via OOB swaps on demo interaction
- Linked attribute names in the reference table
Covers all 20 implemented attributes (sx-get/post/put/delete/patch,
sx-trigger/target/swap/swap-oob/select/confirm/push-url/sync/encoding/
headers/include/vals/media/disable/on:*, sx-retry, data-sx, data-sx-env).
Also adds sx-on:* to BEHAVIOR_ATTRS, updates REFERENCE_NAV to link
/reference/attributes, and makes /reference/ an index page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the 5-phase plan for making the sx s-expression layer a
universal view language that renders on either client or server, with
pages as cached components and data-only navigation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Nest admin header inside post-header-child (layouts.py/helpers.py) so
full-page DOM matches OOB swap structure, eliminating duplicate headers
- Clear post-header-child on post layout OOB to remove stale admin rows
- Read SX initial content from #sx-content-input instead of
window.__SX_INITIAL__ to avoid escaping issues through SX pipeline
- Fix client-side SX parser RE_STRING to handle escaped newlines
- Clear root element in SxEditor.mount() to prevent double content on
HTMX re-mount
- Remove unused ~blog-editor-sx-initial component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace _post_edit_content_sx raw HTML builder with sx_call() pattern
matching render_editor_panel. Add ~blog-editor-edit-form,
~blog-editor-publish-js, ~blog-editor-sx-initial components to
editor.sx. Fixes (~sx-editor-styles) rendering as literal text on
the edit page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All app-level templates have been replaced by native sx component builders
and defpage declarative routes. Removes ~15,200 lines of dead HTML.
Kept: shared/browser templates (errors, ap_social, macros, root layout),
account + federation _email/magic_link, federation profile.html chain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Python GET page handlers with declarative defpage definitions in .sx
files across all 8 apps (sx docs, orders, account, market, cart, federation,
events, blog). Each app now has sxc/pages/ with setup functions, layout
registrations, page helpers, and .sx defpage declarations.
Core infrastructure: add g I/O primitive, PageDef support for auth/layout/
data/content/filter/aside/menu slots, post_author auth level, and custom
layout registration. Remove ~1400 lines of render_*_page/render_*_oob
boilerplate. Update all endpoint references in routes, sx_components, and
templates to defpage_* naming.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strip NIL values at I/O primitive boundaries (frag, query, action, service)
to prevent _Nil objects from reaching Python code that expects None. Add
mobile_nav_sx() helper that auto-populates the hamburger menu from nav_tree
and auth_menu context fragments when no menu slot is provided.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add sx_content to _post_to_edit_dict so edit page receives existing content
- Add SX/Koenig editor tabs, sx-editor mount point, and SxEditor.mount init
- Only pass sx_content to writer_update when form field is present (prevents
accidental clearing when editing via Koenig-only path)
- Add csrf_exempt to example API POST/DELETE/PUT demo endpoints
- Add defpage infrastructure (pages.py, layouts.py) and sx docs page definitions
- Add defhandler definitions for example API handlers (examples.sx)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Converts data inspector, entries browser, calendar view, settings form,
and WYSIWYG editor panels from Jinja templates to Python content builders.
Zero render_template() calls remain across blog, events, and orders services.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sx.js only appended INPUT values to GET request URLs. SELECT and
TEXTAREA elements with a name attribute were silently ignored,
so the category parameter was never sent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ~value-options component wrapped options in a fragment that didn't
render correctly inside a <select> innerHTML swap. Return plain
(option) elements directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1: Wire 16 events routes to existing sx render functions
- slot, slots, ticket_types, ticket_type, calendar_entries,
calendar_entry, calendar_entry/admin
Phase 2: Orders checkout return (2 calls)
- New orders/sx/checkout.sx with return page components
- New render_checkout_return_page() in orders/sx/sx_components.py
Phase 3: Blog menu items (3 calls)
- New blog/sx/menu_items.sx with search result components
- New render_menu_item_form() and render_page_search_results()
in blog/sx/sx_components.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move .sx-fade-in and .sx-loading-btn CSS from inline (style) tags to
basics.css so they go through the on-demand CSS registry
- Pretty-print sx source in wire response display (not all on one line)
- Change sx logo from </> icon to (</>) text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents remaining 24 render_template() calls across events, blog,
and orders services with phased conversion strategy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All 6 examples show Component and Wire response as placeholders that
fill with actual content when the demo is triggered (via OOB swaps)
- Wire response shows full wire content including component definitions
(when not cached) and CSS style block
- Component display only includes defs the client doesn't already have,
matching real sx_response() behaviour
- Add "Clear component cache" button to reset localStorage + in-memory
component env so next interaction shows component download
- Rebuild tw.css with Tailwind v3.4.19 including sx content paths
- Optimize sx_response() CSS scanning to only scan sent comp_defs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
highlight.py handles syntax coloring with Tailwind classes —
Prism classes were conflicting and are not needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass :code keyword to ~doc-code and ~example-source components
(highlighted content was positional but components use &key code)
- Rebuild tw.css (v3.4.19) with sx/sxc and sx/content in content paths
so highlight.py classes (text-violet-600, text-rose-600, etc.) are included
- Add bg-violet-{100-500} classes for the sx app's violet menu bar
- Add highlight.py custom syntax highlighter (sx, python, bash)
IMPORTANT: tw.css must contain bg-violet-{100-500} rules for the sx
app's menu bar. Do not rebuild tw.css without ensuring violet classes
are included (via safelist or content paths).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Events fragment routes now call render_fragment_container_cards(),
render_fragment_account_tickets(), and render_fragment_account_bookings()
from sx_components instead of render_template(). Account sx_components
handles both SxExpr (text/sx) and HTML (text/html) fragment responses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
execute_handler was using async_render() which renders all the way to
HTML. Fragment providers need to return sx source (s-expression strings)
that consuming apps parse and render client-side.
Added async_eval_to_sx() — a new execution mode that evaluates I/O
primitives and control flow but serializes component/tag calls as sx
source instead of rendering them to HTML. This mirrors how the old
Python handlers used sx_call() to build sx strings.
Also fixed: _ASER_FORMS checked after HTML_TAGS, causing "map" (which
is both an HTML tag and an sx special form) to be serialized as a tag
instead of evaluated. Moved _ASER_FORMS check before HTML_TAGS.
Also fixed: empty? primitive now handles non-len()-able types gracefully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return _RawHTML wrapper so pre-rendered HTML in let bindings isn't
escaped when used in render context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- OOB nav updates: AJAX navigation now swaps both menu bar levels
(main nav highlighting + sub-nav with current page) using the same
oob_header_sx/oob_page_sx pattern as blog/market/events
- Enable OAuth for sx and test apps (removed from _NO_OAUTH, added sx
to ALLOWED_CLIENTS, added app_urls for sx/test/orders)
- Fetch real cross-service fragments (cart-mini, auth-menu, nav-tree)
instead of hardcoding empty values
- Add :selected param to ~menu-row-sx for white text current-page label
- Fix duplicate element IDs: use menu-row-sx child_id/child mechanism
instead of manual header_child_sx wrappers
- Fix home page copy: "Server-rendered DOM over the wire (no HTML)"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The existing prism.css sets color:black on code elements. Dark
bg-stone-900 backgrounds made text invisible. Switched to bg-stone-50
with a border to work with the light Prism theme.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New public-facing service documenting the s-expression rendering engine.
Modelled on four.htmx.org with violet theme, all content rendered via sx.
Sections: docs, reference, protocols, examples (live demos), essays
(including "sx sucks"). No database — purely static documentation.
Port 8012, Redis DB 10. CI and deploy.sh updated with app_dir() mapping
for sx_docs -> sx/ directory. Caddy reverse proxy entry added separately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap 15 call sites in events/sx_components.py where sx-generating
functions were passed as plain strings to sx_call(), causing raw
s-expression source to leak into the rendered page.
- Add dev-mode pretty-printing (RELOAD=true) for sx responses and
full page sx source — indented output in Network tab and View Source.
- Fix Sx.render to handle multiple top-level expressions by falling
back to parseAll and returning a DocumentFragment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- sx-editor prints version on init: [sx-editor] v2026-03-02b-exorcism
- Add Markdown to card insert menu with /markdown and /md slash commands
- Add YouTube, X/Twitter, Vimeo, Spotify, CodePen as dedicated embed
menu items with brand icons (all create ~kg-embed cards)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add shared/sx/html_to_sx.py (HTMLParser-based HTML→sx converter) and
update lexical_to_sx.py so HTML cards, markdown cards, and captions all
produce native sx expressions instead of opaque HTML strings.
- ~kg-html now wraps native sx children (editor can identify the block)
- New ~kg-md component for markdown card blocks
- Captions are sx expressions, not escaped HTML strings
- kg_cards.sx: replace (raw! caption) with direct caption rendering
- sx-editor.js: htmlToSx() via DOMParser, serializeInline for captions,
_childrenSx for ~kg-html/~kg-md, new kg-md edit UI
- Migration script (blog/scripts/migrate_sx_html.py) to re-convert
stored sx_content from lexical source
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both Python and JS parsers used next_token() which returns plain strings
for both delimiter characters and string values, making them
indistinguishable. A string whose value is ")" or "(" would be
misinterpreted as a structural delimiter, causing parse errors.
Fix: use peek() (raw character) for all structural decisions in
parseExpr before consuming via next_token(). Also add enhanced error
logging to sx.js mount/loadComponents for easier future debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When server omits component source (cache hit), the script tag has
empty textContent. The early `if (!text.trim()) continue` was
skipping the data-components handler entirely, so components never
loaded from localStorage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server computes SHA-256 hash of all component source at startup.
Client signals its cached hash via cookie (sx-comp-hash). On full
page load: cookie match → server sends empty script tag with just
the hash; mismatch → sends full source. Client loads from
localStorage on hit, parses inline + caches on miss.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix _markdown() in lexical_to_sx.py: render markdown to HTML with
mistune.html() before storing in ~kg-html
- Add shared/sx/prettify.py: sx_to_pretty_sx and json_to_pretty_sx
produce sx AST for syntax-highlighted DOM (uses canonical serialize())
- Add preview tab to admin header nav
- Add GET /preview/ route with 4 views: prettified sx, prettified
lexical JSON, sx rendered HTML, lexical rendered HTML
- Add ~blog-preview-panel and ~blog-preview-section components
- Add syntax highlight CSS for sx/JSON tokens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ghost's extended-text node can appear both as a block (with children) and
inline (with text field). When used as a child of a link node, it has a
text field and should produce a text literal, not a (p ...) wrapper.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Computes file content hash at process startup, cached for lifetime.
Removes manual cache-busting instruction from CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Model: add sx_content column to Post. Writer: accept sx_content in
create_post, create_page, update_post. Routes: read sx_content from form
data in new post, new page, and edit routes. Read pipeline: ghost_db
includes sx_content in public dict, detail/home views prefer sx_content
over html when available, PostDTO includes sx_content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pure s-expression block editor replacing React/Koenig: single hover + button,
slash commands, full card edit modes (image/gallery/video/audio/file/embed/
bookmark/callout/toggle/button/HTML/code), inline format toolbar, keyboard
shortcuts, drag-drop uploads, oEmbed/bookmark metadata fetching.
Includes lexical_to_sx converter for backfilling existing posts, KG card
components matching Ghost's card CSS, migration for sx_content column, and
31 converter tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Process sx-swap-oob and hx-swap-oob elements in the popstate handler
so sidebar, filter, menu, and headers are restored on back navigation
- Disable the 62.5% base font-size hack that leaked globally and caused
all fonts to shrink when navigating to/from the editor
- Cache-bust sx.js to v=20260301d
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse tw.css into per-class lookup registry at startup
- Pre-scan component CSS classes at registration time (avoid per-request regex)
- Compress SX-Css header: 8-char hash replaces full class list (LRU cache)
- Add ;@css comment annotation for dynamically constructed class names
- Safelist bg-sky-{100..400} in Tailwind config for menu-row-sx dynamic shades
- Client sends/receives hash, falls back gracefully on cache miss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>