Files
rose-ash/sx/sx/plans/nav-redesign.sx
giles 6f96452f70 Fix empty code blocks: rename ~docs/code param, fix batched IO dispatch
Two bugs caused code blocks to render empty across the site:

1. ~docs/code component had parameter named `code` which collided with
   the HTML <code> tag name. Renamed to `src` and updated all 57
   callers. Added font-mono class for explicit monospace.

2. Batched IO dispatch in ocaml_bridge.py only skipped one leading
   number (batch ID) but the format has two (epoch + ID):
   (io-request EPOCH ID "name" args...). Changed to skip all leading
   numbers so the string name is correctly found. This fixes highlight
   and other batchable helpers returning empty results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:08:40 +00:00

246 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;; ---------------------------------------------------------------------------
;; Navigation Redesign — SX Docs
;; ---------------------------------------------------------------------------
(defcomp ~plans/nav-redesign/plan-nav-redesign-content ()
(~docs/page :title "Navigation Redesign"
(~docs/section :title "The Problem" :id "problem"
(p "The current navigation is a horizontal menu bar system: root bar, sx bar, sub-section bar. 13 top-level sections crammed into a scrolling horizontal row. Hover to see dropdowns. Click a section, get a second bar underneath. Click a page, get a third bar. Three stacked bars eating vertical space on every page.")
(p "It's a conventional web pattern and it's bad for this site. SX docs has a deep hierarchy — sections contain subsections contain pages. Horizontal bars can't express depth. They flatten everything into one level and hide the rest behind hover states that don't work on mobile, that obscure content, that require spatial memory of where things are.")
(p "The new nav is vertical, hierarchical, and infinite. No dropdowns. No menu bars. Just a centered breadcrumb trail that expands downward as you drill in."))
;; -----------------------------------------------------------------------
;; Design
;; -----------------------------------------------------------------------
(~docs/section :title "Design" :id "design"
(~docs/subsection :title "Structure"
(p "One vertical column, centered. Each level is a row.")
(~docs/code :src (highlight ";; Home (nothing selected)\n;;\n;; [ sx ]\n;;\n;; Docs CSSX Reference Protocols Examples\n;; Essays Philosophy Specs Bootstrappers\n;; Testing Isomorphism Plans Reactive Islands\n\n\n;; Section selected (e.g. Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; Status Reader Macros Theorem Prover\n;; Self-Hosting JS Bootstrapper SX-Activity\n;; Predictive Prefetching Content-Addressed\n;; Environment Images Runtime Slicing Typed SX\n;; Fragment Protocol ...\n\n\n;; Page selected (e.g. Typed SX under Plans)\n;;\n;; [ sx ]\n;;\n;; < Plans >\n;;\n;; < Typed SX >\n;;\n;; [ page content here ]" "lisp")))
(~docs/subsection :title "Rules"
(ol :class "list-decimal pl-5 text-stone-700 space-y-2"
(li (strong "Logo at top, centered.") " Always visible. Click = home. The only fixed element.")
(li (strong "Level 1: section list.") " Shown on home page as a wrapped, centered list of links. This is the full menu — no hiding, no hamburger.")
(li (strong "When a section is selected:") " Section name replaces the list. Left arrow and right arrow for sibling navigation (previous/next section). The section's children appear as a new list below.")
(li (strong "When a child is selected:") " Same pattern — child name replaces the list, arrows for siblings, sub-children appear below. Recurse ad infinitum.")
(li (strong "Breadcrumb trail.") " Each selected level stays visible as a single row above the current level. The trail is: logo → section → subsection → page. Each row has arrows. Click any ancestor to navigate up.")
(li (strong "No dropdowns.") " Never. Hover does nothing special. The hierarchy is always visible in the breadcrumb trail.")
(li (strong "No hamburger menu.") " The nav IS the page on the home/index views. On content pages, the breadcrumb trail is compact enough to show without hiding.")
(li (strong "Responsive by default.") " Vertical + centered + wrapped = works at any width. No breakpoint-specific layout needed."))))
;; -----------------------------------------------------------------------
;; Visual language
;; -----------------------------------------------------------------------
(~docs/section :title "Visual Language" :id "visual"
(~docs/subsection :title "Levels"
(p "Each level has decreasing visual weight:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Level")
(th :class "px-3 py-2 font-medium text-stone-600" "Selected state")
(th :class "px-3 py-2 font-medium text-stone-600" "List state")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Logo")
(td :class "px-3 py-2 text-stone-700" "Large, violet, always visible")
(td :class "px-3 py-2 text-stone-600" "—"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Section")
(td :class "px-3 py-2 text-stone-700" "Medium text, violet-700, arrows")
(td :class "px-3 py-2 text-stone-600" "Medium text, stone-600, wrapped inline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Subsection")
(td :class "px-3 py-2 text-stone-700" "Smaller text, violet-600, arrows")
(td :class "px-3 py-2 text-stone-600" "Small text, stone-500, wrapped inline"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-semibold text-stone-800" "Level 3+")
(td :class "px-3 py-2 text-stone-700" "Same as subsection")
(td :class "px-3 py-2 text-stone-600" "Same as subsection"))))))
(~docs/subsection :title "Arrows"
(p "Left and right arrows are inline with the selected item name. They navigate to the previous/next sibling in the current list. Keyboard accessible: left/right arrow keys when the row is focused.")
(~docs/code :src (highlight ";; Arrow rendering\n;;\n;; < Plans >\n;;\n;; < is a link to /plans/content-addressed-components\n;; (the previous sibling in plans-nav-items)\n;; > is a link to /plans/fragment-protocol\n;; (the next sibling)\n;; \"Plans\" is a link to /plans/ (the section index)\n;;\n;; At the edges, the arrow wraps:\n;; first item: < wraps to last\n;; last item: > wraps to first" "lisp")))
(~docs/subsection :title "Transitions"
(p "Selecting an item: the list fades/collapses, the selected item moves to breadcrumb position, children appear below. This is an L0 morph — the server renders the new state, the client morphs. No JS animation library needed, just CSS transitions on the morph targets.")
(p "Going up: click an ancestor in the breadcrumb. Its children (the level below) expand back into a list. Reverse of the drill-down.")))
;; -----------------------------------------------------------------------
;; Data model
;; -----------------------------------------------------------------------
(~docs/section :title "Data Model" :id "data"
(p "The current nav data is flat — each section has its own " (code "define") ". The new model is a single tree:")
(~docs/code :src (highlight "(define sx-nav-tree\n {:label \"sx\"\n :href \"/\"\n :children (list\n {:label \"Docs\"\n :href \"/language/docs/introduction\"\n :children docs-nav-items}\n {:label \"CSSX\"\n :href \"/applications/cssx/\"\n :children cssx-nav-items}\n {:label \"Reference\"\n :href \"/reference/\"\n :children reference-nav-items}\n {:label \"Protocols\"\n :href \"/applications/protocols/wire-format\"\n :children protocols-nav-items}\n {:label \"Examples\"\n :href \"/examples/click-to-load\"\n :children examples-nav-items}\n {:label \"Essays\"\n :href \"/etc/essays/\"\n :children essays-nav-items}\n {:label \"Philosophy\"\n :href \"/etc/philosophy/sx-manifesto\"\n :children philosophy-nav-items}\n {:label \"Specs\"\n :href \"/language/specs/\"\n :children specs-nav-items}\n {:label \"Bootstrappers\"\n :href \"/language/bootstrappers/\"\n :children bootstrappers-nav-items}\n {:label \"Testing\"\n :href \"/language/testing/\"\n :children testing-nav-items}\n {:label \"Isomorphism\"\n :href \"/geography/isomorphism/\"\n :children isomorphism-nav-items}\n {:label \"Plans\"\n :href \"/etc/plans/\"\n :children plans-nav-items}\n {:label \"Reactive Islands\"\n :href \"/reactive-islands/\"\n :children reactive-islands-nav-items})})" "lisp"))
(p "The existing per-section lists (" (code "docs-nav-items") ", " (code "plans-nav-items") ", etc.) remain unchanged — they just become the " (code ":children") " of tree nodes. Sub-sections that have their own sub-items can nest further:")
(~docs/code :src (highlight ";; Future: deeper nesting\n{:label \"Plans\"\n :href \"/etc/plans/\"\n :children (list\n {:label \"Status\" :href \"/etc/plans/status\"}\n {:label \"Bootstrappers\" :href \"/etc/plans/self-hosting-bootstrapper\"\n :children (list\n {:label \"py.sx\" :href \"/etc/plans/self-hosting-bootstrapper\"}\n {:label \"js.sx\" :href \"/etc/plans/js-bootstrapper\"})}\n ;; ...\n )}" "lisp"))
(p "The tree depth is unlimited. The nav component recurses."))
;; -----------------------------------------------------------------------
;; Components
;; -----------------------------------------------------------------------
(~docs/section :title "Components" :id "components"
(p "Three new components replace the entire menu bar system:")
(~docs/subsection :title "~plans/nav-redesign/logo"
(~docs/code :src (highlight "(defcomp ~plans/nav-redesign/logo ()\n (a :href \"/\"\n :sx-get \"/\" :sx-target \"#main-panel\" :sx-select \"#main-panel\"\n :sx-swap \"outerHTML\" :sx-push-url \"true\"\n :class \"block text-center py-4\"\n (span :class \"text-2xl font-bold text-violet-700\" \"sx\")))" "lisp"))
(p "Always at the top. Always centered. The anchor."))
(~docs/subsection :title "~plans/nav-redesign/nav-breadcrumb"
(~docs/code :src (highlight "(defcomp ~plans/nav-redesign/nav-breadcrumb (&key path siblings level)\n ;; Renders one breadcrumb row: < Label >\n ;; path = the nav tree node for this level\n ;; siblings = list of sibling nodes (for arrow nav)\n ;; level = depth (controls text size/color)\n (let ((idx (find-index siblings path))\n (prev (nth siblings (mod (- idx 1) (len siblings))))\n (next (nth siblings (mod (+ idx 1) (len siblings)))))\n (div :class (str \"flex items-center justify-center gap-3 py-1\"\n (nav-level-classes level))\n (a :href (get prev \"href\")\n :sx-get (get prev \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Previous\"\n \"<\")\n (a :href (get path \"href\")\n :sx-get (get path \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"font-medium\"\n (get path \"label\"))\n (a :href (get next \"href\")\n :sx-get (get next \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"text-stone-400 hover:text-violet-600\"\n :aria-label \"Next\"\n \">\"))))" "lisp"))
(p "One row per selected level. Shows the current node with left/right arrows to siblings."))
(~docs/subsection :title "~plans/nav-redesign/nav-list"
(~docs/code :src (highlight "(defcomp ~plans/nav-redesign/nav-list (&key items level)\n ;; Renders a wrapped list of links — the children of the current level\n (div :class (str \"flex flex-wrap justify-center gap-x-4 gap-y-2 py-2\"\n (nav-level-classes level))\n (map (fn (item)\n (a :href (get item \"href\")\n :sx-get (get item \"href\") :sx-target \"#main-panel\"\n :sx-select \"#main-panel\" :sx-swap \"outerHTML\"\n :sx-push-url \"true\"\n :class \"hover:text-violet-700 transition-colors\"\n (get item \"label\")))\n items)))" "lisp"))
(p "The children of the current level, rendered as a centered wrapped list of plain links."))
(~docs/subsection :title "~plans/nav-redesign/nav — the composition"
(~docs/code :src (highlight "(defcomp ~plans/nav-redesign/nav (&key trail children-items level)\n ;; trail = list of {node, siblings} from root to current\n ;; children-items = children of the deepest selected node\n ;; level = depth of children\n (div :class \"max-w-3xl mx-auto px-4\"\n ;; Logo\n (~plans/nav-redesign/logo)\n ;; Breadcrumb trail (one row per selected ancestor)\n (map-indexed (fn (i crumb)\n (~nav-breadcrumb\n :path (get crumb \"node\")\n :siblings (get crumb \"siblings\")\n :level (+ i 1)))\n trail)\n ;; Children of the deepest selected node\n (when children-items\n (~plans/nav-redesign/nav-list :items children-items :level level))))" "lisp"))
(p "That's the entire navigation. Three small components composed. No bars, no dropdowns, no mobile variants.")))
;; -----------------------------------------------------------------------
;; Path resolution
;; -----------------------------------------------------------------------
(~docs/section :title "Path Resolution" :id "resolution"
(p "Given a URL path, compute the breadcrumb trail and children. This is a tree walk:")
(~docs/code :src (highlight "(define resolve-nav-path\n (fn (tree current-href)\n ;; Walk sx-nav-tree, find the node matching current-href,\n ;; return the trail of ancestors + current children.\n ;;\n ;; Returns: {:trail (list of {:node N :siblings S})\n ;; :children (list) or nil\n ;; :depth number}\n ;;\n ;; Example: current-href = \"/etc/plans/typed-sx\"\n ;; → trail: [{:node Plans :siblings [Docs, CSSX, ...]}\n ;; {:node Typed-SX :siblings [Status, Reader-Macros, ...]}]\n ;; → children: nil (leaf node)\n ;; → depth: 2\n (let ((result (walk-nav-tree tree current-href (list))))\n result)))" "lisp"))
(p "This runs server-side (it's a pure function, no IO). The layout component calls it with the current URL and passes the result to " (code "~plans/nav-redesign/nav") ". Same pattern as the current " (code "find-current") " but produces a richer result.")
(p "For sx-get navigations (HTMX swaps), the server re-renders the nav with the new path. The morph diffs the old and new nav — breadcrumb rows appear/disappear, the list changes. CSS transitions handle the visual."))
;; -----------------------------------------------------------------------
;; What goes away
;; -----------------------------------------------------------------------
(~docs/section :title "What Goes Away" :id "removal"
(p "Significant deletion:")
(div :class "overflow-x-auto rounded border border-stone-200 mb-4"
(table :class "w-full text-left text-sm"
(thead (tr :class "border-b border-stone-200 bg-stone-100"
(th :class "px-3 py-2 font-medium text-stone-600" "Component")
(th :class "px-3 py-2 font-medium text-stone-600" "File")
(th :class "px-3 py-2 font-medium text-stone-600" "Why")))
(tbody
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~shared:layout/menu-row-sx")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Horizontal bar with colour levels — replaced by breadcrumb rows"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-header-row")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Top menu bar — replaced by logo + breadcrumb"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-sub-row")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Sub-section bar — replaced by second breadcrumb row"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~sx-main-nav")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "Horizontal nav list — replaced by ~plans/nav-redesign/nav-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~nav-data/section-nav")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/nav-data.sx")
(td :class "px-3 py-2 text-stone-600" "Sub-nav builder — replaced by ~plans/nav-redesign/nav-list"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~shared:layout/nav-link")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Complex link with aria-selected + submenu wrapper — replaced by plain a tags"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "~shared:layout/mobile-menu-section")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/layout.sx")
(td :class "px-3 py-2 text-stone-600" "Separate mobile menu — new nav is inherently responsive"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" "6 layout variants")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "sx/sx/layouts.sx")
(td :class "px-3 py-2 text-stone-600" "full/oob/mobile × home/section — replaced by one layout with ~plans/nav-redesign/nav"))
(tr :class "border-b border-stone-100"
(td :class "px-3 py-2 font-mono text-sm text-stone-700" ".nav-group CSS")
(td :class "px-3 py-2 font-mono text-sm text-violet-700" "shared/sx/templates/shell.sx")
(td :class "px-3 py-2 text-stone-600" "Hover submenu CSS — no submenus to hover")))))
(p "The layout variants collapse from 6 (full/oob/mobile × home/section) to 2 (full/oob). No mobile variant needed — the nav is one column, it works everywhere."))
;; -----------------------------------------------------------------------
;; Layout simplification
;; -----------------------------------------------------------------------
(~docs/section :title "Layout Simplification" :id "layout"
(p "The defpage layout declarations currently specify section, sub-label, sub-href, sub-nav, selected — five params to configure two menu bars. The new layout takes one param: the nav trail.")
(~docs/code :src (highlight ";; Current (verbose, configures two bars)\n(defpage plan-page\n :path \"/etc/plans/<slug>\"\n :layout (:sx-section\n :section \"Plans\"\n :sub-label \"Plans\"\n :sub-href \"/etc/plans/\"\n :sub-nav (~nav-data/section-nav :items plans-nav-items\n :current (find-current plans-nav-items slug))\n :selected (or (find-current plans-nav-items slug) \"\"))\n :content (...))\n\n;; New (one param, nav computed from URL)\n(defpage plan-page\n :path \"/etc/plans/<slug>\"\n :layout (:sx-docs :path (str \"/etc/plans/\" slug))\n :content (...))" "lisp"))
(p "The layout component computes the nav trail internally from the path and the nav tree. No more passing section names, sub-labels, or pre-built nav components through layout params.")
(~docs/code :src (highlight "(defcomp ~plans/nav-redesign/docs-layout-full (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~root-header-auto)\n (~sx-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\")))))\n\n(defcomp ~plans/nav-redesign/docs-layout-oob (&key path)\n (let ((nav-state (resolve-nav-path sx-nav-tree path)))\n (<> (~oob-nav\n :trail (get nav-state \"trail\")\n :children-items (get nav-state \"children\")\n :level (get nav-state \"depth\"))\n (~root-header-auto true))))" "lisp"))
(p "Two layout components instead of twelve. Every defpage in docs.sx simplifies from five layout params to one."))
;; -----------------------------------------------------------------------
;; Scope
;; -----------------------------------------------------------------------
(~docs/section :title "Scope" :id "scope"
(div :class "rounded border border-amber-200 bg-amber-50 p-4 mb-4"
(p :class "text-amber-900 font-medium" "SX docs only — for now")
(p :class "text-amber-800" "This redesign applies to the SX docs app (" (code "sx/") "). The other services (blog, market, events, etc.) keep their current navigation. If the pattern proves out, it can migrate to shared infrastructure and replace the root menu system too."))
(p "What changes:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "sx/sx/nav-data.sx") " — add " (code "sx-nav-tree") " (wraps existing lists, no content change)")
(li (code "sx/sx/layouts.sx") " — rewrite: delete 12 components, add 5 (logo, breadcrumb, list, nav, 2 layouts)")
(li (code "sx/sxc/pages/docs.sx") " — simplify every defpage's " (code ":layout") " declaration")
(li (code "sx/sxc/pages/layouts.py") " — register new layout names")
(li (code "shared/sx/templates/layout.sx") " — no changes needed (shared components untouched, only SX-docs-specific ones change)"))
(p "What doesn't change:")
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Page content — all " (code "~plan-*-content") ", " (code "~doc-*-content") ", etc. are untouched")
(li "Nav data — all " (code "*-nav-items") " lists are unchanged, just composed into a tree")
(li "Routing — all defpage paths stay the same")
(li "Other services — blog, market, etc. unaffected")))
;; -----------------------------------------------------------------------
;; Implementation
;; -----------------------------------------------------------------------
(~docs/section :title "Implementation" :id "implementation"
(~docs/subsection :title "Phase 1: Nav tree + resolution"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Add " (code "sx-nav-tree") " to " (code "nav-data.sx") " — compose existing " (code "*-nav-items") " lists into a tree")
(li "Write " (code "resolve-nav-path") " — pure function, tree walk, returns trail + children")
(li "Test: given a path, produces the correct breadcrumb trail and child list")))
(~docs/subsection :title "Phase 2: New components"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Write " (code "~plans/nav-redesign/logo") ", " (code "~plans/nav-redesign/nav-breadcrumb") ", " (code "~plans/nav-redesign/nav-list") ", " (code "~plans/nav-redesign/nav"))
(li "Write " (code "~plans/nav-redesign/docs-layout-full") " and " (code "~plans/nav-redesign/docs-layout-oob"))
(li "Register new layout in " (code "layouts.py"))
(li "Test with one defpage first — verify morph transitions work")))
(~docs/subsection :title "Phase 3: Migrate all defpages"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Update every defpage in " (code "docs.sx") " to use " (code ":layout (:sx-docs :path ...)"))
(li "This is mechanical — replace the 5-param layout block with 1-param")))
(~docs/subsection :title "Phase 4: Delete old components"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "Delete " (code "~sx-main-nav") ", " (code "~sx-header-row") ", " (code "~sx-sub-row") ", " (code "~nav-data/section-nav"))
(li "Delete all 12 SX layout variants from " (code "layouts.sx"))
(li "Delete old layout registrations from " (code "layouts.py"))
(li "Remove " (code ".nav-group") " CSS if no other service uses it"))))))