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>
Replaces placeholder "Market admin" text with a functional admin panel
that lists marketplaces for a page and supports create/delete via sx,
mirroring the events calendar admin pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add shared/static/styles/tailwind.css as Tailwind v4 input with
explicit @source paths for all service templates and safelisted
dynamic classes (bg-{colour}-{shade}, text-{size})
- Build to shared/static/styles/tw.css (93KB minified)
- Replace <script src="cdn.tailwindcss.com"> with <link> to tw.css
in sx page shell, Jinja _head.html, and ~base-shell component
- Add build-tw.sh convenience script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Always re-fetch on popstate (drop LRU cache) for fresh content on back/forward
- Save/restore scroll position via pushState
- Add id="root-header-child" to ~app-body so OOB swaps can target it
- Fix OOB renderers: nest root-row inside root-header-child swap instead of
separate OOB that clobbers it
- Fix 3+ header rows dropped: wrap all headers in single fragment instead of
concatenating outside (<> ...)
- Strip <script data-components> from text/sx responses before renderToString
- Fall back to location.assign for cross-origin pushState (SecurityError)
- Move blog/sx/nav.sx to shared/sx/templates/ so all services have nav components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scripts inserted via innerHTML/insertAdjacentHTML don't execute.
Add _activateScripts() to _swapContent that recreates script tags
(without type or type=text/javascript) as live elements. This fixes
editor.js not loading when navigating to edit pages via sx-get.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scripts inserted via innerHTML (template.content) don't execute.
When raw! renders HTML containing <script> tags, recreate them as
live elements so the browser fetches and executes them. Fixes
editor.js not loading on HTMX navigation to edit pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Post edit, data, entries, and settings pages pass raw Jinja HTML
as content to full_page_sx/oob_page_sx, which wraps it in SxExpr().
This injects unescaped HTML directly into sx source, breaking the
parser. Fix by serializing the HTML into a (raw! "...") expression
that the sx evaluator renders unescaped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple sx expressions are concatenated and passed as a kwarg
value via SxExpr(), the parser only sees the first as the value — the
rest become extra args silently dropped by the component. Wrap in (<>)
fragments in render_editor_panel() and _page_cards_sx().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues with the eager kwarg evaluation in renderComponentDOM and
renderStrComponent:
1. Data arrays (e.g. tags list of dicts) were being passed to sxEval
which tried to call a dict as a function — causing blank pages.
Fix: only evaluate arrays with a Symbol head (actual expressions);
pass data arrays through as-is.
2. Expression arrays like (get t "src") inside map lambdas lost their
scope when deferred — causing "get,t,src" URLs. Fix: eagerly evaluate
these Symbol-headed expressions in the caller's env.
3. Bare symbol `t` used as boolean in editor.sx threw "Undefined symbol".
Fix: use `true` literal instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
renderComponentDOM now eagerly renders kwarg values that are render
expressions (HTML tags, <>, ~components) into DOM nodes. But renderDOM
treated any non-array object as a dict and returned an empty fragment,
silently discarding pre-rendered content. Add a nodeType check to pass
DOM nodes through unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The string renderer's component call had the same deferred-evaluation
bug — and this is the path actually used for blog card rendering via
renderToString. Apply the same _isRenderExpr check to route render-only
forms through renderStr while data expressions go through sxEval.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix eagerly evaluated all kwarg expressions via sxEval,
which broke render-only forms (<>, raw!, HTML tags, ~components) that
only exist in the render pipeline. Now detect render expressions by
checking if the head symbol is an HTML/SVG tag, <>, raw!, or ~component,
and route those through renderDOM while data expressions still go
through sxEval for correct scope resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
renderComponentDOM was deferring evaluation of complex expressions
(arrays) passed as component kwargs, storing raw AST instead. When the
component body later used these values as attributes, the caller's env
(with lambda params like t, a) was no longer available, producing
stringified arrays like "get,t,src" as attribute values — which browsers
interpreted as relative URLs.
Evaluate all non-literal kwarg values eagerly in the caller's env,
matching the behavior of callComponent and the Python-side renderer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Post/Author/Tag/PostAuthor/PostTag/PostUser models from
shared/models/ghost_content.py to blog/models/content.py so blog-domain
models no longer live in the shared layer. Replace the shared
SqlBlogService + BlogService protocol with a blog-local singleton
(blog_service), and switch entry_associations.py from direct DB access
to HTTP fetch_data("blog", "post-by-id") to respect the inter-service
boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Final sync script with HTML verification + author→user migration
- Make ghost_id nullable on posts/authors/tags, add UUID/timestamp defaults
- Add user profile fields (bio, slug, profile_image, etc.) to User model
- New PostUser M2M table (replaces post_authors for new posts)
- PostWriter service: direct DB CRUD with Lexical rendering, optimistic
locking, AP federation, tag upsert
- Rewrite create/edit/settings routes to use PostWriter (no Ghost API calls)
- Neuter Ghost webhooks (post/page/author/tag → 204 no-op)
- Disable Ghost startup sync
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>