;; ---------------------------------------------------------------------------
;; Phase 2 Plan — remaining reactive features
;; ---------------------------------------------------------------------------

(defcomp ()
  (~docs/page :title "Phase 2: Completing the Reactive Toolkit"

    (~docs/section :title "Where we are" :id "where"
      (p "Phase 1 delivered the core reactive primitive: signals, effects, computed values, islands, disposal, stores, event bridges, and reactive DOM rendering. These are sufficient for any isolated interactive widget.")
      (p "Phase 2 fills the gaps that appear when you try to build " (em "real application UI") " with islands — forms, modals, dynamic styling, efficient lists, error handling, and async loading. Each feature is independently valuable and independently shippable. None requires changes to the signal runtime.")

      (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mt-4")
        (table (~tw :tokens "w-full text-left text-sm")
          (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Feature")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "React equiv.")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Priority")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Spec file")))
          (tbody
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Input binding")
              (td (~tw :tokens "px-3 py-2 text-stone-500 text-xs") "controlled inputs")
              (td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
              (td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500") "adapter-dom.sx"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Keyed reconciliation")
              (td (~tw :tokens "px-3 py-2 text-stone-500 text-xs") "key prop")
              (td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
              (td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500") "adapter-dom.sx"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Portals")
              (td (~tw :tokens "px-3 py-2 text-stone-500 text-xs") "createPortal")
              (td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
              (td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500") "adapter-dom.sx"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Error boundaries")
              (td (~tw :tokens "px-3 py-2 text-stone-500 text-xs") "componentDidCatch")
              (td (~tw :tokens "px-3 py-2 text-green-700 font-medium") "Done")
              (td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500") "adapter-dom.sx"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Suspense")
              (td (~tw :tokens "px-3 py-2 text-stone-500 text-xs") "Suspense + lazy")
              (td (~tw :tokens "px-3 py-2 text-stone-500 font-medium") "N/A")
              (td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500") "covered by existing primitives"))
            (tr
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Transitions")
              (td (~tw :tokens "px-3 py-2 text-stone-500 text-xs") "startTransition")
              (td (~tw :tokens "px-3 py-2 text-stone-500 font-medium") "N/A")
              (td (~tw :tokens "px-3 py-2 font-mono text-xs text-stone-500") "covered by existing primitives"))))))

    ;; -----------------------------------------------------------------------
    ;; P0 — must have
    ;; -----------------------------------------------------------------------

    (~docs/section :title "P0: Input Binding" :id "input-binding"
      (p "You cannot build a form without two-way input binding. React uses controlled components — value is always driven by state, onChange feeds back. SX needs the same pattern but with signals instead of setState.")

      (~docs/subsection :title "Design"
        (p "A new " (code ":bind") " attribute on " (code "input") ", " (code "textarea") ", and " (code "select") " elements. It takes a signal and creates a bidirectional link: signal value flows into the element, user input flows back into the signal.")

        (~docs/code :src (highlight ";; Bind a signal to an input\n(defisland ~reactive-islands/phase2/login-form ()\n  (let ((email (signal \"\"))\n        (password (signal \"\")))\n    (form :on-submit (fn (e)\n            (dom-prevent-default e)\n            (fetch-json \"POST\" \"/api/login\"\n              (dict \"email\" (deref email)\n                    \"password\" (deref password))))\n      (input :type \"email\" :bind email\n             :placeholder \"Email\")\n      (input :type \"password\" :bind password\n             :placeholder \"Password\")\n      (button :type \"submit\" \"Log in\"))))" "lisp"))

        (p "The " (code ":bind") " attribute is handled in " (code "adapter-dom.sx") "'s element rendering. For a signal " (code "s") ":")
        (ol (~tw :tokens "space-y-1 text-stone-600 list-decimal list-inside text-sm")
          (li "Set the element's " (code "value") " to " (code "(deref s)") " initially")
          (li "Create an effect: when " (code "s") " changes externally, update " (code "el.value"))
          (li "Add an " (code "input") " event listener: on user input, call " (code "(reset! s el.value)"))
          (li "For checkboxes/radios: bind to " (code "checked") " instead of " (code "value"))
          (li "For select: bind to " (code "value") ", handle " (code "change") " event")))

      (~docs/subsection :title "Spec changes"
        (~docs/code :src (highlight ";; In adapter-dom.sx, inside render-dom-element:\n;; After processing :on-* event attrs, check for :bind\n(when (dict-has? kwargs \"bind\")\n  (let ((sig (dict-get kwargs \"bind\")))\n    (when (signal? sig)\n      (bind-input el sig))))\n\n;; New function in adapter-dom.sx:\n(define bind-input\n  (fn (el sig)\n    (let ((tag (lower (dom-tag-name el)))\n          (is-checkbox (or (= (dom-get-attr el \"type\") \"checkbox\")\n                          (= (dom-get-attr el \"type\") \"radio\"))))\n      ;; Set initial value\n      (if is-checkbox\n        (dom-set-prop el \"checked\" (deref sig))\n        (dom-set-prop el \"value\" (str (deref sig))))\n      ;; Signal → element (effect, auto-tracked)\n      (effect (fn ()\n        (if is-checkbox\n          (dom-set-prop el \"checked\" (deref sig))\n          (let ((v (str (deref sig))))\n            (when (!= (dom-get-prop el \"value\") v)\n              (dom-set-prop el \"value\" v))))))\n      ;; Element → signal (event listener)\n      (dom-listen el (if is-checkbox \"change\" \"input\")\n        (fn (e)\n          (if is-checkbox\n            (reset! sig (dom-get-prop el \"checked\"))\n            (reset! sig (dom-get-prop el \"value\"))))))))" "lisp"))

        (p "Platform additions: " (code "dom-set-prop") " and " (code "dom-get-prop") " (property access, not attribute — " (code ".value") " not " (code "getAttribute") "). These go in the boundary as IO primitives."))

      (~docs/subsection :title "Derived patterns"
        (p "Input binding composes with everything already built:")
        (ul (~tw :tokens "space-y-1 text-stone-600 list-disc pl-5 text-sm")
          (li (strong "Validation: ") (code "(computed (fn () (>= (len (deref email)) 3)))") " — derived from the bound signal")
          (li (strong "Debounced search: ") "Effect with " (code "set-timeout") " cleanup, reading the bound signal")
          (li (strong "Form submission: ") (code "(deref email)") " in the submit handler gives the current value")
          (li (strong "Stores: ") "Bind to a store signal — multiple islands share the same form state"))))

    (~docs/section :title "P0: Keyed List Reconciliation" :id "keyed-list"
      (p (code "reactive-list") " currently clears all DOM nodes and re-renders from scratch on every signal change. This works for small lists but breaks down for large ones — focus is lost, animations restart, scroll position resets.")

      (~docs/subsection :title "Design"
        (p "When items have a " (code ":key") " attribute (or a key function), " (code "reactive-list") " should reconcile by key instead of clearing.")

        (~docs/code :src (highlight ";; Keyed list — items matched by :key, reused across updates\n(defisland ~reactive-islands/phase2/todo-list ()\n  (let ((items (signal (list\n          (dict \"id\" 1 \"text\" \"Buy milk\")\n          (dict \"id\" 2 \"text\" \"Write spec\")\n          (dict \"id\" 3 \"text\" \"Ship it\")))))\n    (ul\n      (map (fn (item)\n        (li :key (get item \"id\")\n          (span (get item \"text\"))\n          (button :on-click (fn (e) ...)\n            \"Remove\")))\n        (deref items)))))" "lisp"))

        (p "The reconciliation algorithm:")
        (ol (~tw :tokens "space-y-1 text-stone-600 list-decimal list-inside text-sm")
          (li "Extract key from each rendered child (from " (code ":key") " attr or item identity)")
          (li "Build a map of " (code "old-key → DOM node") " from previous render")
          (li "Walk new items: if key exists in old map, " (strong "reuse") " the DOM node (move to correct position). If not, render fresh.")
          (li "Remove DOM nodes whose keys are absent from the new list")
          (li "Result: minimum DOM mutations. Focus, scroll, animations preserved.")))

      (~docs/subsection :title "Spec changes"
        (~docs/code :src (highlight ";; In adapter-dom.sx, replace reactive-list's effect body:\n(define reactive-list\n  (fn (map-fn items-sig env ns)\n    (let ((marker (create-comment \"island-list\"))\n          (key-map (dict))     ;; key → DOM node\n          (key-order (list)))  ;; current key order\n      (effect (fn ()\n        (let ((parent (dom-parent marker))\n              (items (deref items-sig)))\n          (when parent\n            (let ((new-map (dict))\n                  (new-keys (list)))\n              ;; Render or reuse each item\n              (for-each (fn (item)\n                (let ((rendered (render-item map-fn item env ns))\n                      (key (or (dom-get-attr rendered \"key\")\n                               (dom-get-data rendered \"key\")\n                               (identity-key item))))\n                  (dom-remove-attr rendered \"key\")\n                  (if (dict-has? key-map key)\n                    ;; Reuse existing\n                    (dict-set! new-map key (dict-get key-map key))\n                    ;; New node\n                    (dict-set! new-map key rendered))\n                  (append! new-keys key)))\n                items)\n              ;; Remove stale nodes\n              (for-each (fn (k)\n                (when (not (dict-has? new-map k))\n                  (dom-remove (dict-get key-map k))))\n                key-order)\n              ;; Reorder to match new-keys\n              (let ((cursor marker))\n                (for-each (fn (k)\n                  (let ((node (dict-get new-map k)))\n                    (when (not (= node (dom-next-sibling cursor)))\n                      (dom-insert-after cursor node))\n                    (set! cursor node)))\n                  new-keys))\n              ;; Update state\n              (set! key-map new-map)\n              (set! key-order new-keys))))))\n      marker)))" "lisp"))

        (p "Falls back to current clear-and-rerender when no keys are present.")))

    ;; -----------------------------------------------------------------------
    ;; P1 — important
    ;; -----------------------------------------------------------------------

    (~docs/section :title "P1: Portals" :id "portals"
      (p "A portal renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, dropdown menus, and toast notifications — anything that must escape overflow:hidden, z-index stacking, or layout constraints.")

      (~docs/subsection :title "Design"
        (~docs/code :src (highlight ";; portal — render children into a target element\n(defisland ~reactive-islands/phase2/modal-trigger ()\n  (let ((open? (signal false)))\n    (div\n      (button :on-click (fn (e) (swap! open? not))\n        \"Open Modal\")\n\n      ;; Portal: children rendered into #modal-root,\n      ;; not into this island's DOM\n      (portal \"#modal-root\"\n        (when (deref open?)\n          (div :class \"fixed inset-0 bg-black/50 flex items-center justify-center\"\n            (div :class \"bg-white rounded-lg p-6 max-w-md\"\n              (h2 \"Modal Title\")\n              (p \"This is rendered outside the island's DOM subtree.\")\n              (button :on-click (fn (e) (reset! open? false))\n                \"Close\"))))))))" "lisp"))

        (p "Implementation in " (code "adapter-dom.sx") ":")
        (ol (~tw :tokens "space-y-1 text-stone-600 list-decimal list-inside text-sm")
          (li (code "portal") " is a new render-dom form (add to " (code "RENDER_DOM_FORMS") " and " (code "dispatch-render-form") ")")
          (li "First arg is a CSS selector string for the target container")
          (li "Remaining args are children, rendered normally via " (code "render-to-dom"))
          (li "Instead of returning the fragment, append it to the resolved target element")
          (li "Return a comment marker in the original position (for disposal tracking)")
          (li "On island disposal, portal content is removed from the target")))

      (~docs/subsection :title "Disposal"
        (p "Portals must participate in island disposal. When the island is destroyed, portal content must be removed from its remote target. The " (code "with-island-scope") " mechanism handles this — the portal registers a disposer that removes its children from the target element.")))

    ;; -----------------------------------------------------------------------
    ;; P2 — nice to have
    ;; -----------------------------------------------------------------------

    (~docs/section :title "P2: Error Boundaries" :id "error-boundaries"
      (p "When an island's rendering or effect throws, the error currently propagates to the top level and may crash other islands. An error boundary catches the error and renders a fallback UI.")

      (~docs/subsection :title "Design"
        (~docs/code :src (highlight ";; error-boundary — catch errors in island subtrees\n(defisland ~reactive-islands/phase2/resilient-widget ()\n  (error-boundary\n    ;; Fallback: shown when children throw\n    (fn (err)\n      (div :class \"p-4 bg-red-50 border border-red-200 rounded\"\n        (p :class \"text-red-700 font-medium\" \"Something went wrong\")\n        (p :class \"text-red-500 text-sm\" (error-message err))))\n    ;; Children: the happy path\n    (do\n      (~risky-component)\n      (~another-component))))" "lisp"))

        (p "Implementation:")
        (ol (~tw :tokens "space-y-1 text-stone-600 list-decimal list-inside text-sm")
          (li (code "error-boundary") " is a new render-dom form")
          (li "First arg: fallback function " (code "(fn (error) ...)") " that returns DOM")
          (li "Remaining args: children rendered inside a try/catch")
          (li "On error: clear the boundary container, render fallback with the caught error")
          (li "Effects within the boundary are disposed on error")
          (li "A " (code "retry") " function is passed to the fallback for recovery"))))

    (~docs/section :title "P2: Suspense" :id "suspense"
      (p "Suspense handles async operations in the render path — data fetching, lazy-loaded components, code splitting. Show a loading placeholder until the async work completes, then swap in the result.")

      (~docs/subsection :title "Design"
        (~docs/code :src (highlight ";; suspense — async-aware rendering boundary\n(defisland ~reactive-islands/phase2/user-profile (&key user-id)\n  (suspense\n    ;; Fallback: shown during loading\n    (div :class \"animate-pulse\"\n      (div :class \"h-4 bg-stone-200 rounded w-3/4\")\n      (div :class \"h-4 bg-stone-200 rounded w-1/2 mt-2\"))\n    ;; Children: may contain async operations\n    (let ((user (await (fetch-json (str \"/api/users/\" user-id)))))\n      (div\n        (h2 (get user \"name\"))\n        (p (get user \"email\"))))))" "lisp"))

        (p "This requires a new primitive concept: a " (strong "resource") " — an async signal that transitions through loading → resolved → error states.")

        (~docs/code :src (highlight ";; resource — async signal\n(define resource\n  (fn (fetch-fn)\n    ;; Returns a signal-like value:\n    ;; {:loading true :data nil :error nil} initially\n    ;; {:loading false :data result :error nil} on success\n    ;; {:loading false :data nil :error err} on failure\n    (let ((state (signal (dict \"loading\" true\n                               \"data\" nil\n                               \"error\" nil))))\n      ;; Kick off the async operation\n      (promise-then (fetch-fn)\n        (fn (data) (reset! state (dict \"loading\" false\n                                       \"data\" data\n                                       \"error\" nil)))\n        (fn (err) (reset! state (dict \"loading\" false\n                                      \"data\" nil\n                                      \"error\" err))))\n      state)))" "lisp"))

        (p "Suspense is the rendering boundary; resource is the data primitive. Together they give a clean async data story without effects-that-fetch (React's " (code "useEffect") " + " (code "useState") " anti-pattern).")))

    (~docs/section :title "P2: Transitions" :id "transitions"
      (p "Transitions mark updates as non-urgent. The UI stays interactive during expensive re-renders. React's " (code "startTransition") " defers state updates so that urgent updates (typing, clicking) aren't blocked by slow ones (filtering a large list, rendering a complex subtree).")

      (~docs/subsection :title "Design"
        (~docs/code :src (highlight ";; transition — non-urgent signal update\n(defisland ~reactive-islands/phase2/search-results (&key items)\n  (let ((query (signal \"\"))\n        (filtered (signal items))\n        (is-pending (signal false)))\n    ;; Typing is urgent — updates immediately\n    ;; Filtering is deferred — doesn't block input\n    (effect (fn ()\n      (let ((q (deref query)))\n        (transition is-pending\n          (fn ()\n            (reset! filtered\n              (filter (fn (item)\n                (contains? (lower (get item \"name\")) (lower q)))\n                items)))))))\n    (div\n      (input :bind query :placeholder \"Search...\")\n      (when (deref is-pending)\n        (span :class \"text-stone-400\" \"Filtering...\"))\n      (ul (map (fn (item) (li (get item \"name\")))\n            (deref filtered))))))" "lisp"))

        (p (code "transition") " takes a pending-signal and a thunk. It sets pending to true, schedules the thunk via " (code "requestIdleCallback") " (or " (code "setTimeout 0") " as fallback), then sets pending to false when complete. Signal writes inside the thunk are batched and applied asynchronously.")
        (p "This is lower priority because SX's fine-grained updates already avoid the re-render-everything problem that makes transitions critical in React. But for truly large lists or expensive computations, deferral is still valuable.")))

    ;; -----------------------------------------------------------------------
    ;; Implementation order
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Implementation Order" :id "order"
      (p "Each feature is independent. Suggested order based on dependency and value:")
      (ol (~tw :tokens "space-y-3 text-stone-600 list-decimal list-inside")
        (li (strong "Input binding") " (P0) — unlocks forms. Smallest change, biggest impact. One new function in adapter-dom.sx, two platform primitives (" (code "dom-set-prop") ", " (code "dom-get-prop") "). Add to demo page immediately.")
        (li (strong "Keyed reconciliation") " (P0) — unlocks efficient dynamic lists. Replace reactive-list's effect body. Add " (code ":key") " extraction. No new primitives needed.")
        (li (strong "Portals") " (P1) — one new render-dom form. Needs disposal integration. Unlocks modals, tooltips, toasts.")
        (li (strong "Error boundaries") " (P2) — one new render-dom form with try/catch. Independent of everything else."))

      (p (~tw :tokens "mt-4 text-stone-600") "Every feature follows the same pattern: spec in " (code ".sx") " → bootstrap to JS/Python → add platform primitives → add demo island. No feature requires changes to the signal runtime, the evaluator, or the rendering pipeline. They are all additive."))

    (~docs/section :title "What we are NOT building" :id "not-building"
      (p "Some React features are deliberately excluded:")
      (ul (~tw :tokens "space-y-2 text-stone-600 list-disc pl-5")
        (li (strong "Virtual DOM / diffing") " — SX uses fine-grained signals. There is no component re-render to diff against. The " (code "reactive-text") ", " (code "reactive-attr") ", " (code "reactive-fragment") ", and " (code "reactive-list") " primitives update the exact DOM nodes that changed.")
        (li (strong "JSX / template compilation") " — SX is interpreted at runtime. No build step. The s-expression syntax " (em "is") " the component tree — there is nothing to compile.")
        (li (strong "Server components (React-style)") " — SX already has a richer version. The " (code "aser") " mode evaluates server-side logic and serializes the result as SX wire format. Components can be expanded on the server or deferred to the client. This is more flexible than React's server/client component split.")
        (li (strong "Concurrent rendering / fiber") " — React's fiber architecture exists to time-slice component re-renders. SX has no component re-renders to slice. Fine-grained updates are inherently incremental.")
        (li (strong "Hooks rules") " — Signals are values, not hooks. No rules about ordering, no conditional creation restrictions, no dependency arrays. This is a feature, not a gap.")))))
